services/presence/: remove. Use projects/presence-service git repo instead
This commit is contained in:
parent
dee7537462
commit
129ada9101
@ -43,7 +43,6 @@ data/Makefile
|
||||
lib/Makefile
|
||||
lib/xdgmime/Makefile
|
||||
services/Makefile
|
||||
services/presence/Makefile
|
||||
services/clipboard/Makefile
|
||||
shell/Makefile
|
||||
shell/extensions/Makefile
|
||||
|
@ -1 +1 @@
|
||||
SUBDIRS = presence clipboard console
|
||||
SUBDIRS = clipboard console
|
||||
|
@ -1,31 +0,0 @@
|
||||
servicedir = $(datadir)/dbus-1/services
|
||||
service_in_files = org.laptop.Sugar.Presence.service.in
|
||||
service_DATA = $(service_in_files:.service.in=.service)
|
||||
|
||||
$(service_DATA): $(service_in_files) Makefile
|
||||
@sed -e "s|\@bindir\@|$(bindir)|" $< > $@
|
||||
|
||||
sugardir = $(pkgdatadir)/services/presence
|
||||
sugar_PYTHON = \
|
||||
__init__.py \
|
||||
activity.py \
|
||||
buddy.py \
|
||||
buddyiconcache.py \
|
||||
linklocal_plugin.py \
|
||||
presenceservice.py \
|
||||
pstest.py \
|
||||
psutils.py \
|
||||
server_plugin.py
|
||||
|
||||
dist_bin_SCRIPTS = sugar-presence-service
|
||||
|
||||
DISTCLEANFILES = $(service_DATA)
|
||||
|
||||
EXTRA_DIST = $(service_in_files)
|
||||
|
||||
dist_check_SCRIPTS = test_psutils.py
|
||||
|
||||
TESTS_ENVIRONMENT = \
|
||||
PYTHONPATH=$(top_srcdir):$(top_srcdir)/services/presence \
|
||||
$(PYTHON)
|
||||
TESTS = $(dist_check_SCRIPTS)
|
@ -1,36 +0,0 @@
|
||||
"""Service to track buddies and activities on the network
|
||||
|
||||
Model objects:
|
||||
|
||||
activity.Activity -- tracks a (shared/shareable) activity
|
||||
with many properties and observable events
|
||||
|
||||
buddy.Buddy -- tracks a reference to a particular actor
|
||||
on the network
|
||||
|
||||
buddy.GenericOwner -- actor who owns a particular
|
||||
activity on the network
|
||||
|
||||
buddy.ShellOwner -- actor who owns the local machine
|
||||
connects to the owner module (on the server)
|
||||
|
||||
Controller objects:
|
||||
|
||||
presenceservice.PresenceService -- controller which connects
|
||||
a networking plugin to a DBUS service. Generates events
|
||||
for networking events, forwards updates/requests to the
|
||||
server plugin.
|
||||
|
||||
server_plugin.ServerPlugin -- implementation of networking
|
||||
plugin using telepathy Python (Jabber) to provide the
|
||||
underlying communications layer. Generates GObject
|
||||
events that the PresenceService observes to forward onto
|
||||
the DBUS clients.
|
||||
|
||||
Utility machinery:
|
||||
|
||||
buddyiconcache.BuddyIconCache -- caches buddy icons on disk
|
||||
based on the "jid" XXX Jabber ID? of the buddy.
|
||||
|
||||
psutils -- trivial function to decode int-list to characters
|
||||
"""
|
@ -1,715 +0,0 @@
|
||||
# Copyright (C) 2007, Red Hat, Inc.
|
||||
# Copyright (C) 2007, Collabora Ltd.
|
||||
#
|
||||
# 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 sugar import util
|
||||
import logging
|
||||
|
||||
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"
|
||||
|
||||
_PROP_ID = "id"
|
||||
_PROP_NAME = "name"
|
||||
_PROP_COLOR = "color"
|
||||
_PROP_TYPE = "type"
|
||||
_PROP_VALID = "valid"
|
||||
_PROP_LOCAL = "local"
|
||||
_PROP_JOINED = "joined"
|
||||
_PROP_CUSTOM_PROPS = "custom-props"
|
||||
|
||||
_logger = logging.getLogger('s-p-s.activity')
|
||||
|
||||
class Activity(ExportedGObject):
|
||||
"""Represents a shared activity seen on the network, or a local activity
|
||||
that has been, or will be, shared onto the network.
|
||||
|
||||
The activity might be public, restricted to a group, or invite-only.
|
||||
"""
|
||||
|
||||
__gtype_name__ = "Activity"
|
||||
|
||||
__gsignals__ = {
|
||||
'validity-changed':
|
||||
# The activity's validity has changed.
|
||||
# An activity is valid if its name, color, type and ID have been
|
||||
# set.
|
||||
# Arguments:
|
||||
# validity: bool
|
||||
(gobject.SIGNAL_RUN_FIRST, None, [bool]),
|
||||
'disappeared':
|
||||
# Nobody is in this activity any more.
|
||||
# No arguments.
|
||||
(gobject.SIGNAL_RUN_FIRST, None, []),
|
||||
}
|
||||
|
||||
__gproperties__ = {
|
||||
_PROP_ID : (str, None, None, None,
|
||||
gobject.PARAM_READWRITE |
|
||||
gobject.PARAM_CONSTRUCT_ONLY),
|
||||
_PROP_NAME : (str, None, None, None, gobject.PARAM_READWRITE),
|
||||
_PROP_COLOR : (str, None, None, None, gobject.PARAM_READWRITE),
|
||||
_PROP_TYPE : (str, None, None, None, gobject.PARAM_READWRITE),
|
||||
_PROP_VALID : (bool, None, None, False, gobject.PARAM_READABLE),
|
||||
_PROP_LOCAL : (bool, None, None, False,
|
||||
gobject.PARAM_READWRITE |
|
||||
gobject.PARAM_CONSTRUCT_ONLY),
|
||||
_PROP_JOINED : (bool, None, None, False, gobject.PARAM_READABLE),
|
||||
_PROP_CUSTOM_PROPS : (object, None, None,
|
||||
gobject.PARAM_READWRITE |
|
||||
gobject.PARAM_CONSTRUCT_ONLY)
|
||||
}
|
||||
|
||||
_RESERVED_PROPNAMES = __gproperties__.keys()
|
||||
|
||||
def __init__(self, bus, object_id, ps, tp, **kwargs):
|
||||
"""Initializes the activity and sets its properties to default values.
|
||||
|
||||
:Parameters:
|
||||
`bus` : dbus.bus.BusConnection
|
||||
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:
|
||||
`id` : str
|
||||
The globally unique activity ID (required)
|
||||
`name` : str
|
||||
Human-readable title for the activity
|
||||
`color` : str
|
||||
Activity color in #RRGGBB,#RRGGBB (stroke,fill) format
|
||||
`type` : str
|
||||
D-Bus service name representing the activity type
|
||||
`local : bool
|
||||
If True, this activity was initiated locally and is not
|
||||
(yet) advertised on the network
|
||||
(FIXME: is this description right?)
|
||||
`custom-props` : dict
|
||||
Activity-specific properties
|
||||
"""
|
||||
|
||||
if not object_id or not isinstance(object_id, int):
|
||||
raise ValueError("object id must be a valid number")
|
||||
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 = 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
|
||||
self._actname = None
|
||||
self._color = None
|
||||
self._local = False
|
||||
self._type = None
|
||||
self._custom_props = {}
|
||||
|
||||
# ensure no reserved property names are in custom properties
|
||||
cprops = kwargs.get(_PROP_CUSTOM_PROPS)
|
||||
if cprops is not None:
|
||||
(rprops, cprops) = self._split_properties(cprops)
|
||||
if len(rprops.keys()) > 0:
|
||||
raise ValueError("Cannot use reserved property names '%s'"
|
||||
% ", ".join(rprops.keys()))
|
||||
|
||||
if not kwargs.get(_PROP_ID):
|
||||
raise ValueError("activity id is required")
|
||||
if not util.validate_activity_id(kwargs[_PROP_ID]):
|
||||
raise ValueError("Invalid activity id '%s'" % kwargs[_PROP_ID])
|
||||
|
||||
ExportedGObject.__init__(self, bus, self._object_path,
|
||||
gobject_properties=kwargs)
|
||||
if self.props.local and not self.props.valid:
|
||||
raise RuntimeError("local activities require color, type, and "
|
||||
"name")
|
||||
|
||||
# If not yet valid, query activity properties
|
||||
if not self.props.valid:
|
||||
tp.update_activity_properties(self._id)
|
||||
|
||||
def do_get_property(self, pspec):
|
||||
"""Gets the value of a property associated with this activity.
|
||||
|
||||
pspec -- Property specifier
|
||||
|
||||
returns The value of the given property.
|
||||
"""
|
||||
|
||||
if pspec.name == _PROP_ID:
|
||||
return self._id
|
||||
elif pspec.name == _PROP_NAME:
|
||||
return self._actname
|
||||
elif pspec.name == _PROP_COLOR:
|
||||
return self._color
|
||||
elif pspec.name == _PROP_TYPE:
|
||||
return self._type
|
||||
elif pspec.name == _PROP_VALID:
|
||||
return self._valid
|
||||
elif pspec.name == _PROP_JOINED:
|
||||
return self._joined
|
||||
elif pspec.name == _PROP_LOCAL:
|
||||
return self._local
|
||||
|
||||
def do_set_property(self, pspec, value):
|
||||
"""Sets the value of a property associated with this activity.
|
||||
|
||||
pspec -- Property specifier
|
||||
value -- Desired value
|
||||
|
||||
Note that the "type" property can be set only once; attempting to set
|
||||
it to something different later will raise a RuntimeError.
|
||||
|
||||
"""
|
||||
if pspec.name == _PROP_ID:
|
||||
if self._id:
|
||||
raise RuntimeError("activity ID is already set")
|
||||
self._id = value
|
||||
elif pspec.name == _PROP_NAME:
|
||||
self._actname = value
|
||||
elif pspec.name == _PROP_COLOR:
|
||||
self._color = value
|
||||
elif pspec.name == _PROP_TYPE:
|
||||
if self._type:
|
||||
raise RuntimeError("activity type is already set")
|
||||
self._type = value
|
||||
elif pspec.name == _PROP_JOINED:
|
||||
self._joined = value
|
||||
elif pspec.name == _PROP_LOCAL:
|
||||
self._local = value
|
||||
elif pspec.name == _PROP_CUSTOM_PROPS:
|
||||
if not value:
|
||||
value = {}
|
||||
(rprops, cprops) = self._split_properties(value)
|
||||
self._custom_props = {}
|
||||
for (key, dvalue) in cprops.items():
|
||||
self._custom_props[str(key)] = str(dvalue)
|
||||
|
||||
self._update_validity()
|
||||
|
||||
def _update_validity(self):
|
||||
"""Sends a "validity-changed" signal if this activity's validity has
|
||||
changed.
|
||||
|
||||
Determines whether this activity's status has changed from valid to
|
||||
invalid, or invalid to valid, and emits a "validity-changed" signal
|
||||
if either is true. "Valid" means that the object's type, ID, name,
|
||||
colour and type properties have all been set to something valid
|
||||
(i.e., not "None").
|
||||
|
||||
"""
|
||||
try:
|
||||
old_valid = self._valid
|
||||
if self._color and self._actname and self._id and self._type:
|
||||
self._valid = True
|
||||
else:
|
||||
self._valid = False
|
||||
|
||||
if old_valid != self._valid:
|
||||
self.emit("validity-changed", self._valid)
|
||||
except AttributeError:
|
||||
self._valid = False
|
||||
|
||||
# dbus signals
|
||||
@dbus.service.signal(_ACTIVITY_INTERFACE,
|
||||
signature="o")
|
||||
def BuddyJoined(self, buddy_path):
|
||||
"""Generates DBUS signal when a buddy joins this activity.
|
||||
|
||||
buddy_path -- DBUS path to buddy object
|
||||
"""
|
||||
pass
|
||||
|
||||
@dbus.service.signal(_ACTIVITY_INTERFACE,
|
||||
signature="o")
|
||||
def BuddyLeft(self, buddy_path):
|
||||
"""Generates DBUS signal when a buddy leaves this activity.
|
||||
|
||||
buddy_path -- DBUS path to buddy object
|
||||
"""
|
||||
pass
|
||||
|
||||
@dbus.service.signal(_ACTIVITY_INTERFACE,
|
||||
signature="o")
|
||||
def NewChannel(self, channel_path):
|
||||
"""Generates DBUS signal when a new channel is created for this
|
||||
activity.
|
||||
|
||||
channel_path -- DBUS path to new channel
|
||||
|
||||
XXX - what is this supposed to do? Who is supposed to call it?
|
||||
What is the channel path? Right now this is never called.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
# dbus methods
|
||||
@dbus.service.method(_ACTIVITY_INTERFACE,
|
||||
in_signature="", out_signature="s")
|
||||
def GetId(self):
|
||||
"""DBUS method to get this activity's (randomly generated) unique ID
|
||||
|
||||
:Returns: Activity ID as a string
|
||||
"""
|
||||
return self.props.id
|
||||
|
||||
@dbus.service.method(_ACTIVITY_INTERFACE,
|
||||
in_signature="", out_signature="s")
|
||||
def GetColor(self):
|
||||
"""DBUS method to get this activity's colour
|
||||
|
||||
:Returns: Activity colour as a string in the format #RRGGBB,#RRGGBB
|
||||
"""
|
||||
return self.props.color
|
||||
|
||||
@dbus.service.method(_ACTIVITY_INTERFACE,
|
||||
in_signature="", out_signature="s")
|
||||
def GetType(self):
|
||||
"""DBUS method to get this activity's type
|
||||
|
||||
:Returns: Activity type as a string, in the same form as a D-Bus
|
||||
well-known name
|
||||
"""
|
||||
return self.props.type
|
||||
|
||||
@dbus.service.method(_ACTIVITY_INTERFACE,
|
||||
in_signature="", out_signature="",
|
||||
async_callbacks=('async_cb', 'async_err_cb'))
|
||||
def Join(self, async_cb, async_err_cb):
|
||||
"""DBUS method to for the local user to attempt to join the activity
|
||||
|
||||
async_cb -- Callback method to be called if join attempt is successful
|
||||
async_err_cb -- Callback method to be called if join attempt is
|
||||
unsuccessful
|
||||
|
||||
"""
|
||||
self.join(async_cb, async_err_cb)
|
||||
|
||||
@dbus.service.method(_ACTIVITY_INTERFACE,
|
||||
in_signature="", out_signature="ao")
|
||||
def GetJoinedBuddies(self):
|
||||
"""DBUS method to return a list of valid buddies who are joined in
|
||||
this activity
|
||||
|
||||
:Returns:
|
||||
A list of buddy object paths corresponding to those buddies
|
||||
in this activity who are 'valid' (i.e. for whom we have complete
|
||||
information)
|
||||
"""
|
||||
ret = []
|
||||
for buddy in self._buddies:
|
||||
if buddy.props.valid:
|
||||
ret.append(buddy.object_path())
|
||||
return ret
|
||||
|
||||
@dbus.service.method(_ACTIVITY_INTERFACE,
|
||||
in_signature="", out_signature="soao")
|
||||
def GetChannels(self):
|
||||
"""DBUS method to get the list of channels associated with this
|
||||
activity
|
||||
|
||||
:Returns:
|
||||
a tuple containing:
|
||||
- the D-Bus well-known service name of the connection
|
||||
(FIXME: this is redundant; in Telepathy it can be derived
|
||||
from that of the connection)
|
||||
- the D-Bus object path of the connection
|
||||
- a list of D-Bus object paths representing the channels
|
||||
associated with this activity
|
||||
"""
|
||||
return self.get_channels()
|
||||
|
||||
@dbus.service.method(_ACTIVITY_INTERFACE,
|
||||
in_signature="", out_signature="s")
|
||||
def GetName(self):
|
||||
"""DBUS method to get this activity's name
|
||||
|
||||
returns Activity name
|
||||
"""
|
||||
return self.props.name
|
||||
|
||||
# methods
|
||||
def object_path(self):
|
||||
"""Retrieves our dbus.ObjectPath object
|
||||
|
||||
returns DBUS ObjectPath object
|
||||
"""
|
||||
return self._object_path
|
||||
|
||||
def get_joined_buddies(self):
|
||||
"""Local method to return a list of valid buddies who are joined in
|
||||
this activity
|
||||
|
||||
This method is called by the PresenceService on the local machine.
|
||||
|
||||
returns A list of buddy objects
|
||||
"""
|
||||
ret = []
|
||||
for buddy in self._buddies:
|
||||
if buddy.props.valid:
|
||||
ret.append(buddy)
|
||||
return ret
|
||||
|
||||
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
|
||||
|
||||
Adds a buddy to this activity if the buddy is not already in the
|
||||
buddy list.
|
||||
|
||||
If this activity is "valid", a BuddyJoined signal is also sent.
|
||||
This method is called by the PresenceService on the local machine.
|
||||
|
||||
"""
|
||||
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 _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 not self._joined:
|
||||
self._remove_buddies((buddy,))
|
||||
|
||||
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.
|
||||
|
||||
Called by the _shared_cb and _joined_cb methods.
|
||||
"""
|
||||
if not text_channel:
|
||||
_logger.debug("Error sharing: text channel was None, shouldn't "
|
||||
"happen")
|
||||
raise RuntimeError("Plugin returned invalid text channel")
|
||||
|
||||
self._text_channel = text_channel
|
||||
self._text_channel[CHANNEL_INTERFACE].connect_to_signal('Closed',
|
||||
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
|
||||
|
||||
return True
|
||||
|
||||
def _shared_cb(self, tp, activity_id, text_channel, exc, userdata):
|
||||
"""XXX - not documented yet
|
||||
"""
|
||||
if activity_id != self.props.id:
|
||||
# Not for us
|
||||
return
|
||||
|
||||
(sigid, owner, async_cb, async_err_cb) = userdata
|
||||
self._tp.disconnect(sigid)
|
||||
|
||||
if exc:
|
||||
_logger.debug("Share of activity %s failed: %s" % (self._id, exc))
|
||||
async_err_cb(exc)
|
||||
else:
|
||||
self._handle_share_join(tp, text_channel)
|
||||
self.send_properties()
|
||||
owner.add_activity(self)
|
||||
async_cb(dbus.ObjectPath(self._object_path))
|
||||
_logger.debug("Share of activity %s succeeded." % self._id)
|
||||
|
||||
def _share(self, (async_cb, async_err_cb), owner):
|
||||
"""XXX - not documented yet
|
||||
|
||||
XXX - This method is called externally by the PresenceService
|
||||
despite the fact that this is supposed to be an internal method!
|
||||
"""
|
||||
_logger.debug("Starting share of activity %s" % self._id)
|
||||
if self._joined:
|
||||
async_err_cb(RuntimeError("Already shared activity %s"
|
||||
% self.props.id))
|
||||
return
|
||||
sigid = self._tp.connect('activity-shared', self._shared_cb)
|
||||
self._tp.share_activity(self.props.id, (sigid, owner, async_cb,
|
||||
async_err_cb))
|
||||
_logger.debug("done with share attempt %s" % self._id)
|
||||
|
||||
def _joined_cb(self, tp, activity_id, text_channel, exc, userdata):
|
||||
"""XXX - not documented yet
|
||||
"""
|
||||
if activity_id != self.props.id:
|
||||
# Not for us
|
||||
return
|
||||
|
||||
(sigid, async_cb, async_err_cb) = userdata
|
||||
self._tp.disconnect(sigid)
|
||||
|
||||
if exc:
|
||||
async_err_cb(exc)
|
||||
else:
|
||||
self._handle_share_join(tp, text_channel)
|
||||
async_cb()
|
||||
|
||||
def join(self, async_cb, async_err_cb):
|
||||
"""Local method for the local user to attempt to join the activity.
|
||||
|
||||
async_cb -- Callback method to be called if join attempt is successful
|
||||
async_err_cb -- Callback method to be called if join attempt is
|
||||
unsuccessful
|
||||
|
||||
The two callbacks are passed to the server_plugin ("tp") object,
|
||||
which in turn passes them back as parameters in a callback to the
|
||||
_joined_cb method; this callback is set up within this method.
|
||||
"""
|
||||
if self._joined:
|
||||
async_err_cb(RuntimeError("Already joined activity %s"
|
||||
% self.props.id))
|
||||
return
|
||||
sigid = self._tp.connect('activity-joined', self._joined_cb)
|
||||
self._tp.join_activity(self.props.id, (sigid, async_cb, async_err_cb))
|
||||
|
||||
def get_channels(self):
|
||||
"""Local method to get the list of channels associated with this
|
||||
activity
|
||||
|
||||
returns XXX - expected a list of channels, instead returning a tuple?
|
||||
"""
|
||||
conn = self._tp.get_connection()
|
||||
# FIXME add tubes and others channels
|
||||
return (str(conn.service_name), conn.object_path,
|
||||
[self._text_channel.object_path])
|
||||
|
||||
def leave(self):
|
||||
"""Local method called when the user wants to leave the activity.
|
||||
|
||||
(XXX - doesn't appear to be called anywhere!)
|
||||
|
||||
"""
|
||||
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):
|
||||
"""Tells the Telepathy server what the properties of this activity are.
|
||||
|
||||
"""
|
||||
props = {}
|
||||
props['name'] = self._actname
|
||||
props['color'] = self._color
|
||||
props['type'] = self._type
|
||||
|
||||
# Add custom properties
|
||||
for (key, value) in self._custom_props.items():
|
||||
props[key] = value
|
||||
|
||||
self._tp.set_activity_properties(self.props.id, props)
|
||||
|
||||
def set_properties(self, properties):
|
||||
"""Sets name, colour and/or type properties for this activity all
|
||||
at once.
|
||||
|
||||
properties - Dictionary object containing properties keyed by
|
||||
property names
|
||||
|
||||
Note that if any of the name, colour and/or type property values is
|
||||
changed from what it originally was, the update_validity method will
|
||||
be called, resulting in a "validity-changed" signal being generated.
|
||||
Called by the PresenceService on the local machine.
|
||||
"""
|
||||
changed = False
|
||||
# split reserved properties from activity-custom properties
|
||||
(rprops, cprops) = self._split_properties(properties)
|
||||
if _PROP_NAME in rprops.keys():
|
||||
name = rprops[_PROP_NAME]
|
||||
if name != self._actname:
|
||||
self._actname = name
|
||||
changed = True
|
||||
|
||||
if _PROP_COLOR in rprops.keys():
|
||||
color = rprops[_PROP_COLOR]
|
||||
if color != self._color:
|
||||
self._color = color
|
||||
changed = True
|
||||
|
||||
if _PROP_TYPE in rprops.keys():
|
||||
type = rprops[_PROP_TYPE]
|
||||
if type != self._type:
|
||||
# Type can never be changed after first set
|
||||
if self._type:
|
||||
_logger.debug("Activity type changed by network; this "
|
||||
"is illegal")
|
||||
else:
|
||||
self._type = type
|
||||
changed = True
|
||||
|
||||
# Set custom properties
|
||||
if len(cprops.keys()) > 0:
|
||||
self.props.custom_props = cprops
|
||||
|
||||
if changed:
|
||||
self._update_validity()
|
||||
|
||||
def _split_properties(self, properties):
|
||||
"""Extracts reserved properties.
|
||||
|
||||
properties - Dictionary object containing properties keyed by
|
||||
property names
|
||||
|
||||
returns a tuple of 2 dictionaries, reserved properties and custom
|
||||
properties
|
||||
"""
|
||||
rprops = {}
|
||||
cprops = {}
|
||||
for (key, value) in properties.items():
|
||||
if key in self._RESERVED_PROPNAMES:
|
||||
rprops[key] = value
|
||||
else:
|
||||
cprops[key] = value
|
||||
return (rprops, cprops)
|
@ -1,638 +0,0 @@
|
||||
"""An "actor" on the network, whether remote or local"""
|
||||
# Copyright (C) 2007, Red Hat, Inc.
|
||||
# Copyright (C) 2007, Collabora Ltd.
|
||||
#
|
||||
# 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 os
|
||||
import gobject
|
||||
import dbus
|
||||
import dbus.service
|
||||
from dbus.gobject_service import ExportedGObject
|
||||
import psutils
|
||||
|
||||
from sugar import env, profile
|
||||
import logging
|
||||
|
||||
_BUDDY_PATH = "/org/laptop/Sugar/Presence/Buddies/"
|
||||
_BUDDY_INTERFACE = "org.laptop.Sugar.Presence.Buddy"
|
||||
_OWNER_INTERFACE = "org.laptop.Sugar.Presence.Buddy.Owner"
|
||||
|
||||
_PROP_NICK = "nick"
|
||||
_PROP_KEY = "key"
|
||||
_PROP_ICON = "icon"
|
||||
_PROP_CURACT = "current-activity"
|
||||
_PROP_COLOR = "color"
|
||||
_PROP_OWNER = "owner"
|
||||
_PROP_VALID = "valid"
|
||||
_PROP_OBJID = 'objid'
|
||||
|
||||
# Will go away soon
|
||||
_PROP_IP4_ADDRESS = "ip4-address"
|
||||
|
||||
_logger = logging.getLogger('s-p-s.buddy')
|
||||
|
||||
|
||||
class Buddy(ExportedGObject):
|
||||
"""Person on the network (tracks properties and shared activites)
|
||||
|
||||
The Buddy is a collection of metadata describing a particular
|
||||
actor/person on the network. The Buddy object tracks a set of
|
||||
activities which the actor has shared with the presence service.
|
||||
|
||||
Buddies have a "valid" property which is used to flag Buddies
|
||||
which are no longer reachable. That is, a Buddy may represent
|
||||
a no-longer reachable target on the network.
|
||||
|
||||
The Buddy emits GObject events that the PresenceService uses
|
||||
to track changes in its status.
|
||||
|
||||
Attributes:
|
||||
|
||||
_activities -- dictionary mapping activity ID to
|
||||
activity.Activity objects
|
||||
handles -- dictionary mapping Telepathy client plugin to
|
||||
contact handle (an integer representing the JID or unique ID);
|
||||
channel-specific handles do not appear here
|
||||
"""
|
||||
|
||||
__gsignals__ = {
|
||||
'validity-changed':
|
||||
# The buddy's validity changed.
|
||||
# Validity starts off False, and becomes True when the buddy
|
||||
# has a color, a nick and a key.
|
||||
# * the new validity: bool
|
||||
(gobject.SIGNAL_RUN_FIRST, None, [bool]),
|
||||
'property-changed':
|
||||
# One of the buddy's properties has changed.
|
||||
# * those properties that have changed:
|
||||
# dict { str => object }
|
||||
(gobject.SIGNAL_RUN_FIRST, None, [object]),
|
||||
'icon-changed':
|
||||
# The buddy's icon changed.
|
||||
# * the bytes of the icon: str
|
||||
(gobject.SIGNAL_RUN_FIRST, None, [object]),
|
||||
'disappeared':
|
||||
# The buddy is offline (has no Telepathy handles and is not the
|
||||
# Owner)
|
||||
(gobject.SIGNAL_RUN_FIRST, None, []),
|
||||
}
|
||||
|
||||
__gproperties__ = {
|
||||
_PROP_KEY : (str, None, None, None, gobject.PARAM_READWRITE),
|
||||
_PROP_ICON : (object, None, None, gobject.PARAM_READWRITE),
|
||||
_PROP_NICK : (str, None, None, None, gobject.PARAM_READWRITE),
|
||||
_PROP_COLOR : (str, None, None, None, gobject.PARAM_READWRITE),
|
||||
_PROP_CURACT : (str, None, None, None, gobject.PARAM_READWRITE),
|
||||
_PROP_VALID : (bool, None, None, False, gobject.PARAM_READABLE),
|
||||
_PROP_OWNER : (bool, None, None, False, gobject.PARAM_READABLE),
|
||||
_PROP_OBJID : (str, None, None, None, gobject.PARAM_READABLE),
|
||||
_PROP_IP4_ADDRESS : (str, None, None, None, gobject.PARAM_READWRITE)
|
||||
}
|
||||
|
||||
def __init__(self, bus, object_id, **kwargs):
|
||||
"""Initialize the Buddy object
|
||||
|
||||
bus -- connection to the D-Bus session bus
|
||||
object_id -- the buddy's unique identifier, either based on their
|
||||
key-ID or JID
|
||||
kwargs -- used to initialize the object's properties
|
||||
|
||||
constructs a DBUS "object path" from the _BUDDY_PATH
|
||||
and object_id
|
||||
"""
|
||||
|
||||
self._object_id = object_id
|
||||
self._object_path = dbus.ObjectPath(_BUDDY_PATH + object_id)
|
||||
|
||||
self._activities = {} # Activity ID -> Activity
|
||||
self._activity_sigids = {}
|
||||
self.handles = {} # tp client -> handle
|
||||
|
||||
self._valid = False
|
||||
self._owner = False
|
||||
self._key = None
|
||||
self._icon = ''
|
||||
self._current_activity = None
|
||||
self._nick = None
|
||||
self._color = None
|
||||
self._ip4_address = None
|
||||
|
||||
_ALLOWED_INIT_PROPS = [_PROP_NICK, _PROP_KEY, _PROP_ICON,
|
||||
_PROP_CURACT, _PROP_COLOR, _PROP_IP4_ADDRESS]
|
||||
for (key, value) in kwargs.items():
|
||||
if key not in _ALLOWED_INIT_PROPS:
|
||||
_logger.debug("Invalid init property '%s'; ignoring..." % key)
|
||||
del kwargs[key]
|
||||
|
||||
# Set icon after superclass init, because it sends DBus and GObject
|
||||
# signals when set
|
||||
icon_data = None
|
||||
if kwargs.has_key(_PROP_ICON):
|
||||
icon_data = kwargs[_PROP_ICON]
|
||||
del kwargs[_PROP_ICON]
|
||||
|
||||
ExportedGObject.__init__(self, bus, self._object_path,
|
||||
gobject_properties=kwargs)
|
||||
|
||||
if icon_data:
|
||||
self.props.icon = icon_data
|
||||
|
||||
def do_get_property(self, pspec):
|
||||
"""Retrieve current value for the given property specifier
|
||||
|
||||
pspec -- property specifier with a "name" attribute
|
||||
"""
|
||||
if pspec.name == _PROP_OBJID:
|
||||
return self._object_id
|
||||
elif pspec.name == _PROP_KEY:
|
||||
return self._key
|
||||
elif pspec.name == _PROP_ICON:
|
||||
return self._icon
|
||||
elif pspec.name == _PROP_NICK:
|
||||
return self._nick
|
||||
elif pspec.name == _PROP_COLOR:
|
||||
return self._color
|
||||
elif pspec.name == _PROP_CURACT:
|
||||
if not self._current_activity:
|
||||
return None
|
||||
if not self._activities.has_key(self._current_activity):
|
||||
return None
|
||||
return self._current_activity
|
||||
elif pspec.name == _PROP_VALID:
|
||||
return self._valid
|
||||
elif pspec.name == _PROP_OWNER:
|
||||
return self._owner
|
||||
elif pspec.name == _PROP_IP4_ADDRESS:
|
||||
return self._ip4_address
|
||||
|
||||
def do_set_property(self, pspec, value):
|
||||
"""Set given property
|
||||
|
||||
pspec -- property specifier with a "name" attribute
|
||||
value -- value to set
|
||||
|
||||
emits 'icon-changed' signal on icon setting
|
||||
calls _update_validity on all calls
|
||||
"""
|
||||
if pspec.name == _PROP_ICON:
|
||||
if str(value) != self._icon:
|
||||
self._icon = str(value)
|
||||
self.IconChanged(self._icon)
|
||||
self.emit('icon-changed', self._icon)
|
||||
elif pspec.name == _PROP_NICK:
|
||||
self._nick = value
|
||||
elif pspec.name == _PROP_COLOR:
|
||||
self._color = value
|
||||
elif pspec.name == _PROP_CURACT:
|
||||
self._current_activity = value
|
||||
elif pspec.name == _PROP_KEY:
|
||||
if self._key:
|
||||
raise RuntimeError("Key already set.")
|
||||
self._key = value
|
||||
elif pspec.name == _PROP_IP4_ADDRESS:
|
||||
self._ip4_address = value
|
||||
|
||||
self._update_validity()
|
||||
|
||||
# dbus signals
|
||||
@dbus.service.signal(_BUDDY_INTERFACE,
|
||||
signature="ay")
|
||||
def IconChanged(self, icon_data):
|
||||
"""Generates DBUS signal with icon_data"""
|
||||
|
||||
@dbus.service.signal(_BUDDY_INTERFACE,
|
||||
signature="o")
|
||||
def JoinedActivity(self, activity_path):
|
||||
"""Generates DBUS signal when buddy joins activity
|
||||
|
||||
activity_path -- DBUS path to the activity object
|
||||
"""
|
||||
|
||||
@dbus.service.signal(_BUDDY_INTERFACE,
|
||||
signature="o")
|
||||
def LeftActivity(self, activity_path):
|
||||
"""Generates DBUS signal when buddy leaves activity
|
||||
|
||||
activity_path -- DBUS path to the activity object
|
||||
"""
|
||||
|
||||
@dbus.service.signal(_BUDDY_INTERFACE,
|
||||
signature="a{sv}")
|
||||
def PropertyChanged(self, updated):
|
||||
"""Generates DBUS signal when buddy's property changes
|
||||
|
||||
updated -- updated property-set (dictionary) with the
|
||||
Buddy's property (changed) values. Note: not the
|
||||
full set of properties, just the changes.
|
||||
"""
|
||||
|
||||
def add_telepathy_handle(self, tp_client, handle):
|
||||
"""Add a Telepathy handle."""
|
||||
conn = tp_client.get_connection()
|
||||
self.TelepathyHandleAdded(conn.service_name, conn.object_path, handle)
|
||||
self.handles[tp_client] = handle
|
||||
|
||||
@dbus.service.signal(_BUDDY_INTERFACE, signature='sou')
|
||||
def TelepathyHandleAdded(self, tp_conn_name, tp_conn_path, handle):
|
||||
"""Another Telepathy handle has become associated with the buddy.
|
||||
|
||||
This must only be emitted for non-channel-specific handles.
|
||||
|
||||
tp_conn_name -- The bus name at which the Telepathy connection may be
|
||||
found
|
||||
tp_conn_path -- The object path at which the Telepathy connection may
|
||||
be found
|
||||
handle -- The handle of type CONTACT, which is not channel-specific,
|
||||
newly associated with the buddy
|
||||
"""
|
||||
|
||||
def remove_telepathy_handle(self, tp_client, handle):
|
||||
"""Remove a Telepathy handle."""
|
||||
conn = tp_client.get_connection()
|
||||
my_handle = self.handles.get(tp_client, 0)
|
||||
if my_handle == handle:
|
||||
del self.handles[tp_client]
|
||||
self.TelepathyHandleRemoved(conn.service_name, conn.object_path,
|
||||
handle)
|
||||
# the Owner can't disappear - that would be silly
|
||||
if not self.handles and not self._owner:
|
||||
self.emit('disappeared')
|
||||
else:
|
||||
_logger.debug('Telepathy handle %u supposedly removed, but '
|
||||
'my handle on that connection is %u - ignoring',
|
||||
handle, my_handle)
|
||||
|
||||
@dbus.service.signal(_BUDDY_INTERFACE, signature='sou')
|
||||
def TelepathyHandleRemoved(self, tp_conn_name, tp_conn_path, handle):
|
||||
"""A Telepathy handle has ceased to be associated with the buddy,
|
||||
probably because that contact went offline.
|
||||
|
||||
The parameters are the same as for TelepathyHandleAdded.
|
||||
"""
|
||||
|
||||
# dbus methods
|
||||
@dbus.service.method(_BUDDY_INTERFACE,
|
||||
in_signature="", out_signature="ay")
|
||||
def GetIcon(self):
|
||||
"""Retrieve Buddy's icon data
|
||||
|
||||
returns empty string or dbus.ByteArray
|
||||
"""
|
||||
if not self.props.icon:
|
||||
return ""
|
||||
return dbus.ByteArray(self.props.icon)
|
||||
|
||||
@dbus.service.method(_BUDDY_INTERFACE,
|
||||
in_signature="", out_signature="ao")
|
||||
def GetJoinedActivities(self):
|
||||
"""Retrieve set of Buddy's joined activities (paths)
|
||||
|
||||
returns list of dbus service paths for the Buddy's joined
|
||||
activities
|
||||
"""
|
||||
acts = []
|
||||
for act in self.get_joined_activities():
|
||||
if act.props.valid:
|
||||
acts.append(act.object_path())
|
||||
return acts
|
||||
|
||||
@dbus.service.method(_BUDDY_INTERFACE,
|
||||
in_signature="", out_signature="a{sv}")
|
||||
def GetProperties(self):
|
||||
"""Retrieve set of Buddy's properties
|
||||
|
||||
returns dictionary of
|
||||
nick : str(nickname)
|
||||
owner : bool( whether this Buddy is an owner??? )
|
||||
XXX what is the owner flag for?
|
||||
key : str(public-key)
|
||||
color: Buddy's icon colour
|
||||
XXX what type?
|
||||
current-activity: Buddy's current activity_id, or
|
||||
"" if no current activity
|
||||
"""
|
||||
props = {}
|
||||
props[_PROP_NICK] = self.props.nick
|
||||
props[_PROP_OWNER] = self.props.owner
|
||||
props[_PROP_KEY] = self.props.key
|
||||
props[_PROP_COLOR] = self.props.color
|
||||
|
||||
if self.props.ip4_address:
|
||||
props[_PROP_IP4_ADDRESS] = self.props.ip4_address
|
||||
else:
|
||||
props[_PROP_IP4_ADDRESS] = ""
|
||||
|
||||
if self.props.current_activity:
|
||||
props[_PROP_CURACT] = self.props.current_activity
|
||||
else:
|
||||
props[_PROP_CURACT] = ""
|
||||
return props
|
||||
|
||||
@dbus.service.method(_BUDDY_INTERFACE,
|
||||
in_signature='', out_signature='a(sou)')
|
||||
def GetTelepathyHandles(self):
|
||||
"""Return a list of non-channel-specific Telepathy contact handles
|
||||
associated with this Buddy.
|
||||
|
||||
:Returns:
|
||||
An array of triples (connection well-known bus name, connection
|
||||
object path, handle).
|
||||
"""
|
||||
ret = []
|
||||
for plugin in self.handles:
|
||||
conn = plugin.get_connection()
|
||||
ret.append((str(conn.service_name), conn.object_path,
|
||||
self.handles[plugin]))
|
||||
|
||||
# methods
|
||||
def object_path(self):
|
||||
"""Retrieve our dbus.ObjectPath object"""
|
||||
return dbus.ObjectPath(self._object_path)
|
||||
|
||||
def _activity_validity_changed_cb(self, activity, valid):
|
||||
"""Join or leave the activity when its validity changes"""
|
||||
if valid:
|
||||
self.JoinedActivity(activity.object_path())
|
||||
else:
|
||||
self.LeftActivity(activity.object_path())
|
||||
|
||||
def add_activity(self, activity):
|
||||
"""Add an activity to the Buddy's set of activities
|
||||
|
||||
activity -- activity.Activity instance
|
||||
|
||||
calls JoinedActivity
|
||||
"""
|
||||
actid = activity.props.id
|
||||
if self._activities.has_key(actid):
|
||||
return
|
||||
self._activities[actid] = activity
|
||||
# join/leave activity when it's validity changes
|
||||
sigid = activity.connect("validity-changed",
|
||||
self._activity_validity_changed_cb)
|
||||
self._activity_sigids[actid] = sigid
|
||||
if activity.props.valid:
|
||||
self.JoinedActivity(activity.object_path())
|
||||
|
||||
def remove_activity(self, activity):
|
||||
"""Remove the activity from the Buddy's set of activities
|
||||
|
||||
activity -- activity.Activity instance
|
||||
|
||||
calls LeftActivity
|
||||
"""
|
||||
actid = activity.props.id
|
||||
if not self._activities.has_key(actid):
|
||||
return
|
||||
activity.disconnect(self._activity_sigids[actid])
|
||||
del self._activity_sigids[actid]
|
||||
del self._activities[actid]
|
||||
if activity.props.valid:
|
||||
self.LeftActivity(activity.object_path())
|
||||
|
||||
def get_joined_activities(self):
|
||||
"""Retrieves list of still-valid activity objects"""
|
||||
acts = []
|
||||
for act in self._activities.values():
|
||||
acts.append(act)
|
||||
return acts
|
||||
|
||||
def set_properties(self, properties):
|
||||
"""Set the given set of properties on the object
|
||||
|
||||
properties -- set of property values to set
|
||||
|
||||
if no change, no events generated
|
||||
if change, generates property-changed and
|
||||
calls _update_validity
|
||||
"""
|
||||
changed = False
|
||||
changed_props = {}
|
||||
if _PROP_NICK in properties:
|
||||
nick = properties[_PROP_NICK]
|
||||
if nick != self._nick:
|
||||
self._nick = nick
|
||||
changed_props[_PROP_NICK] = nick
|
||||
changed = True
|
||||
if _PROP_COLOR in properties:
|
||||
color = properties[_PROP_COLOR]
|
||||
if color != self._color:
|
||||
self._color = color
|
||||
changed_props[_PROP_COLOR] = color
|
||||
changed = True
|
||||
if _PROP_CURACT in properties:
|
||||
curact = properties[_PROP_CURACT]
|
||||
if curact != self._current_activity:
|
||||
self._current_activity = curact
|
||||
changed_props[_PROP_CURACT] = curact
|
||||
changed = True
|
||||
if _PROP_IP4_ADDRESS in properties:
|
||||
ip4addr = properties[_PROP_IP4_ADDRESS]
|
||||
if ip4addr != self._ip4_address:
|
||||
self._ip4_address = ip4addr
|
||||
changed_props[_PROP_IP4_ADDRESS] = ip4addr
|
||||
changed = True
|
||||
if _PROP_KEY in properties:
|
||||
# don't allow key to be set more than once
|
||||
if self._key is None:
|
||||
key = properties[_PROP_KEY]
|
||||
if key is not None:
|
||||
self._key = key
|
||||
changed_props[_PROP_KEY] = key
|
||||
changed = True
|
||||
|
||||
if not changed or not changed_props:
|
||||
return
|
||||
|
||||
# Try emitting PropertyChanged before updating validity
|
||||
# to avoid leaking a PropertyChanged signal before the buddy is
|
||||
# actually valid the first time after creation
|
||||
if self._valid:
|
||||
dbus_changed = {}
|
||||
for key, value in changed_props.items():
|
||||
if value:
|
||||
dbus_changed[key] = value
|
||||
else:
|
||||
dbus_changed[key] = ""
|
||||
self.PropertyChanged(dbus_changed)
|
||||
|
||||
self.emit('property-changed', changed_props)
|
||||
|
||||
self._update_validity()
|
||||
|
||||
def _update_validity(self):
|
||||
"""Check whether we are now valid
|
||||
|
||||
validity is True if color, nick and key are non-null
|
||||
|
||||
emits validity-changed if we have changed validity
|
||||
"""
|
||||
try:
|
||||
old_valid = self._valid
|
||||
if self._color and self._nick and self._key:
|
||||
self._valid = True
|
||||
else:
|
||||
self._valid = False
|
||||
|
||||
if old_valid != self._valid:
|
||||
self.emit("validity-changed", self._valid)
|
||||
except AttributeError:
|
||||
self._valid = False
|
||||
|
||||
|
||||
class GenericOwner(Buddy):
|
||||
"""Common functionality for Local User-like objects
|
||||
|
||||
The TestOwner wants to produce something *like* a
|
||||
ShellOwner, but with randomised changes and the like.
|
||||
This class provides the common features for a real
|
||||
local owner and a testing one.
|
||||
"""
|
||||
__gtype_name__ = "GenericOwner"
|
||||
|
||||
def __init__(self, ps, bus, object_id, **kwargs):
|
||||
"""Initialize the GenericOwner instance
|
||||
|
||||
ps -- presenceservice.PresenceService object
|
||||
bus -- a connection to the D-Bus session bus
|
||||
object_id -- the activity's unique identifier
|
||||
kwargs -- used to initialize the object's properties
|
||||
|
||||
calls Buddy.__init__
|
||||
"""
|
||||
self._ps = ps
|
||||
self._server = kwargs.pop("server", "olpc.collabora.co.uk")
|
||||
self._key_hash = kwargs.pop("key_hash", None)
|
||||
self._registered = kwargs.pop("registered", False)
|
||||
|
||||
self._ip4_addr_monitor = psutils.IP4AddressMonitor.get_instance()
|
||||
self._ip4_addr_monitor.connect("address-changed",
|
||||
self._ip4_address_changed_cb)
|
||||
if self._ip4_addr_monitor.props.address:
|
||||
kwargs["ip4-address"] = self._ip4_addr_monitor.props.address
|
||||
|
||||
Buddy.__init__(self, bus, object_id, **kwargs)
|
||||
self._owner = True
|
||||
|
||||
self._bus = dbus.SessionBus()
|
||||
|
||||
def _ip4_address_changed_cb(self, monitor, address):
|
||||
"""Handle IPv4 address change, set property to generate event"""
|
||||
props = {_PROP_IP4_ADDRESS: address}
|
||||
self.set_properties(props)
|
||||
|
||||
def get_registered(self):
|
||||
"""Retrieve whether owner has registered with presence server"""
|
||||
return self._registered
|
||||
|
||||
def get_server(self):
|
||||
"""Retrieve XMPP server hostname (used by the server plugin)"""
|
||||
return self._server
|
||||
|
||||
def get_key_hash(self):
|
||||
"""Retrieve the user's private-key hash (used by the server plugin
|
||||
as a password)
|
||||
"""
|
||||
return self._key_hash
|
||||
|
||||
def set_registered(self, registered):
|
||||
"""Customisation point: handle the registration of the owner"""
|
||||
raise RuntimeError("Subclasses must implement")
|
||||
|
||||
|
||||
class ShellOwner(GenericOwner):
|
||||
"""Representation of the local-machine owner using Sugar's Shell
|
||||
|
||||
The ShellOwner uses the Sugar Shell's dbus services to
|
||||
register for updates about the user's profile description.
|
||||
"""
|
||||
__gtype_name__ = "ShellOwner"
|
||||
|
||||
_SHELL_SERVICE = "org.laptop.Shell"
|
||||
_SHELL_OWNER_INTERFACE = "org.laptop.Shell.Owner"
|
||||
_SHELL_PATH = "/org/laptop/Shell"
|
||||
|
||||
def __init__(self, ps, bus):
|
||||
"""Initialize the ShellOwner instance
|
||||
|
||||
ps -- presenceservice.PresenceService object
|
||||
bus -- a connection to the D-Bus session bus
|
||||
|
||||
Retrieves initial property values from the profile
|
||||
module. Loads the buddy icon from file as well.
|
||||
XXX note: no error handling on that
|
||||
|
||||
calls GenericOwner.__init__
|
||||
"""
|
||||
server = profile.get_server()
|
||||
key_hash = profile.get_private_key_hash()
|
||||
registered = profile.get_server_registered()
|
||||
key = profile.get_pubkey()
|
||||
nick = profile.get_nick_name()
|
||||
color = profile.get_color().to_string()
|
||||
|
||||
icon_file = os.path.join(env.get_profile_path(), "buddy-icon.jpg")
|
||||
f = open(icon_file, "r")
|
||||
icon = f.read()
|
||||
f.close()
|
||||
|
||||
GenericOwner.__init__(self, ps, bus,
|
||||
'keyid/' + psutils.pubkey_to_keyid(key),
|
||||
key=key, nick=nick, color=color, icon=icon, server=server,
|
||||
key_hash=key_hash, registered=registered)
|
||||
|
||||
# Ask to get notifications on Owner object property changes in the
|
||||
# shell. If it's not currently running, no problem - we'll get the
|
||||
# signals when it does run
|
||||
for (signal, cb) in (('IconChanged', self._icon_changed_cb),
|
||||
('ColorChanged', self._color_changed_cb),
|
||||
('NickChanged', self._nick_changed_cb)):
|
||||
self._bus.add_signal_receiver(cb, signal_name=signal,
|
||||
dbus_interface=self._SHELL_OWNER_INTERFACE,
|
||||
bus_name=self._SHELL_SERVICE,
|
||||
path=self._SHELL_PATH)
|
||||
|
||||
def set_registered(self, value):
|
||||
"""Handle notification that we have been registered"""
|
||||
if value:
|
||||
profile.set_server_registered()
|
||||
|
||||
def _icon_changed_cb(self, icon):
|
||||
"""Handle icon change, set property to generate event"""
|
||||
self.props.icon = icon
|
||||
|
||||
def _color_changed_cb(self, color):
|
||||
"""Handle color change, set property to generate event"""
|
||||
props = {_PROP_COLOR: color}
|
||||
self.set_properties(props)
|
||||
|
||||
def _nick_changed_cb(self, nick):
|
||||
"""Handle nickname change, set property to generate event"""
|
||||
props = {_PROP_NICK: nick}
|
||||
self.set_properties(props)
|
||||
|
||||
def _cur_activity_changed_cb(self, activity_id):
|
||||
"""Handle current-activity change, set property to generate event
|
||||
|
||||
Filters out local activities (those not in self.activites)
|
||||
because the network users can't join those activities, so
|
||||
the activity_id shared will be None in those cases...
|
||||
"""
|
||||
if not self._activities.has_key(activity_id):
|
||||
# This activity is local-only
|
||||
activity_id = None
|
||||
props = {_PROP_CURACT: activity_id}
|
||||
self.set_properties(props)
|
@ -1,105 +0,0 @@
|
||||
# Copyright (C) 2007, Red Hat, Inc.
|
||||
# Copyright (C) 2007, Collabora Ltd.
|
||||
#
|
||||
# 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
|
||||
|
||||
from sugar import env
|
||||
|
||||
import os.path
|
||||
import cPickle
|
||||
|
||||
class BuddyIconCache(object):
|
||||
"""Caches icons on disk and finds them based on the jid of their owners."""
|
||||
def __init__(self):
|
||||
ppath = env.get_profile_path()
|
||||
self._cachepath = os.path.join(ppath, "cache", "buddy-icons", "cache")
|
||||
|
||||
# Ensure cache directory exists
|
||||
if not os.path.exists(os.path.dirname(self._cachepath)):
|
||||
os.makedirs(os.path.dirname(self._cachepath))
|
||||
|
||||
if not os.path.exists(self._cachepath):
|
||||
self._cache = {}
|
||||
# md5 and server token of the last avatar uploaded
|
||||
self._md5 = ''
|
||||
self._token = ''
|
||||
else:
|
||||
self._load_cache()
|
||||
|
||||
def _load_cache(self):
|
||||
try:
|
||||
self._cache, self._md5, self._token = cPickle.load(open(self._cachepath, "r"))
|
||||
except:
|
||||
self._cache, self._md5, self._token = {}, '', ''
|
||||
|
||||
def _save_cache(self):
|
||||
out = open(self._cachepath, "w")
|
||||
cPickle.dump((self._cache, self._md5, self._token), out, protocol=2)
|
||||
|
||||
def get_icon(self, jid, token):
|
||||
hit = self._cache.get(jid)
|
||||
|
||||
if hit:
|
||||
t, icon = hit[0], hit[1]
|
||||
if t == token:
|
||||
return icon
|
||||
|
||||
return None
|
||||
|
||||
def store_icon(self, jid, token, data):
|
||||
self._cache[jid] = (token, data)
|
||||
self._save_cache()
|
||||
|
||||
def check_avatar(self, md5, token):
|
||||
return self._md5 == md5 and self._token == token
|
||||
|
||||
def set_avatar(self, md5, token):
|
||||
self._md5 = md5
|
||||
self._token = token
|
||||
self._save_cache()
|
||||
|
||||
if __name__ == "__main__":
|
||||
my_cache = BuddyIconCache()
|
||||
|
||||
# look for the icon in the cache
|
||||
icon = my_cache.get_icon("test@olpc.collabora.co.uk", "aaaa")
|
||||
print icon
|
||||
|
||||
my_cache.store_icon("test@olpc.collabora.co.uk", "aaaa", "icon1")
|
||||
|
||||
# now we're sure that the icon is in the cache
|
||||
icon = my_cache.get_icon("test@olpc.collabora.co.uk", "aaaa")
|
||||
print icon
|
||||
|
||||
# new icon
|
||||
my_cache.store_icon("test@olpc.collabora.co.uk", "bbbb", "icon2")
|
||||
|
||||
# the icon in the cache is not valid now
|
||||
icon = my_cache.get_icon("test@olpc.collabora.co.uk", "aaaa")
|
||||
print icon
|
||||
|
||||
|
||||
my_avatar_md5 = "111"
|
||||
my_avatar_token = "222"
|
||||
|
||||
if not my_cache.check_avatar(my_avatar_md5, my_avatar_token):
|
||||
# upload of the new avatar
|
||||
print "upload of the new avatar"
|
||||
my_cache.set_avatar(my_avatar_md5, my_avatar_token)
|
||||
else:
|
||||
print "No need to upload the new avatar"
|
||||
|
||||
if my_cache.check_avatar(my_avatar_md5, my_avatar_token):
|
||||
print "No need to upload the new avatar"
|
@ -1,27 +0,0 @@
|
||||
# Copyright (C) 2007, Red Hat, Inc.
|
||||
# Copyright (C) 2007, Collabora Ltd.
|
||||
#
|
||||
# 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
|
||||
|
||||
class LinkLocalPlugin(gobject.GObject):
|
||||
def __init__(self, registry, owner):
|
||||
gobject.GObject.__init__(self)
|
||||
self._registry = registry
|
||||
self._owner = owner
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
@ -1,4 +0,0 @@
|
||||
[D-BUS Service]
|
||||
Name = org.laptop.Sugar.Presence
|
||||
Exec = @bindir@/sugar-presence-service
|
||||
|
@ -1,517 +0,0 @@
|
||||
# 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()
|
||||
keyid = pubkey_to_keyid(key)
|
||||
buddy = self._buddies.get('keyid/' + keyid)
|
||||
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()
|
@ -1,317 +0,0 @@
|
||||
# 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
|
||||
import os
|
||||
import random
|
||||
from ConfigParser import ConfigParser, NoOptionError
|
||||
|
||||
import gobject
|
||||
|
||||
from sugar import env, util
|
||||
|
||||
from buddy import GenericOwner, _PROP_NICK, _PROP_CURACT, _PROP_COLOR
|
||||
from presenceservice import PresenceService
|
||||
from psutils import pubkey_to_keyid
|
||||
|
||||
|
||||
_logger = logging.getLogger('s-p-s.pstest')
|
||||
|
||||
|
||||
class TestOwner(GenericOwner):
|
||||
"""Class representing the owner of the machine. This test owner
|
||||
changes random attributes periodically."""
|
||||
|
||||
__gtype_name__ = "TestOwner"
|
||||
|
||||
def __init__(self, ps, bus, test_num, randomize):
|
||||
self._cp = ConfigParser()
|
||||
self._section = "Info"
|
||||
self._test_activities = []
|
||||
self._test_cur_act = ""
|
||||
self._change_timeout = 0
|
||||
|
||||
self._cfg_file = os.path.join(env.get_profile_path(), 'test-buddy-%d' % test_num)
|
||||
|
||||
(pubkey, privkey, registered) = self._load_config()
|
||||
if not pubkey or not len(pubkey) or not privkey or not len(privkey):
|
||||
(pubkey, privkey) = _get_new_keypair(test_num)
|
||||
|
||||
if not pubkey or not privkey:
|
||||
raise RuntimeError("Couldn't get or create test buddy keypair")
|
||||
|
||||
self._save_config(pubkey, privkey, registered)
|
||||
privkey_hash = util.printable_hash(util._sha_data(privkey))
|
||||
|
||||
nick = _get_random_name()
|
||||
from sugar.graphics import xocolor
|
||||
color = xocolor.XoColor().to_string()
|
||||
icon = _get_random_image()
|
||||
|
||||
_logger.debug("pubkey is %s" % pubkey)
|
||||
GenericOwner.__init__(self, ps, bus,
|
||||
'keyid/' + pubkey_to_keyid(pubkey),
|
||||
key=pubkey, nick=nick, color=color, icon=icon,
|
||||
registered=registered, key_hash=privkey_hash)
|
||||
|
||||
# Only do the random stuff if randomize is true
|
||||
if randomize:
|
||||
self._ps.connect('connection-status', self._ps_connection_status_cb)
|
||||
|
||||
def _share_reply_cb(self, actid, object_path):
|
||||
activity = self._ps.internal_get_activity(actid)
|
||||
if not activity or not object_path:
|
||||
_logger.debug("Couldn't find activity %s even though it was shared." % actid)
|
||||
return
|
||||
_logger.debug("Shared activity %s (%s)." % (actid, activity.props.name))
|
||||
self._test_activities.append(activity)
|
||||
|
||||
def _share_error_cb(self, actid, err):
|
||||
_logger.debug("Error sharing activity %s: %s" % (actid, str(err)))
|
||||
|
||||
def _ps_connection_status_cb(self, ps, connected):
|
||||
if not connected:
|
||||
return
|
||||
|
||||
if not len(self._test_activities):
|
||||
# Share some activities
|
||||
actid = util.unique_id("Activity 1")
|
||||
callbacks = (lambda *args: self._share_reply_cb(actid, *args),
|
||||
lambda *args: self._share_error_cb(actid, *args))
|
||||
atype = "org.laptop.WebActivity"
|
||||
properties = {"foo": "bar"}
|
||||
self._ps._share_activity(actid, atype, "Wembley Stadium", properties, callbacks)
|
||||
|
||||
actid2 = util.unique_id("Activity 2")
|
||||
callbacks = (lambda *args: self._share_reply_cb(actid2, *args),
|
||||
lambda *args: self._share_error_cb(actid2, *args))
|
||||
atype = "org.laptop.WebActivity"
|
||||
properties = {"baz": "bar"}
|
||||
self._ps._share_activity(actid2, atype, "Maine Road", properties, callbacks)
|
||||
|
||||
# Change a random property ever 10 seconds
|
||||
if self._change_timeout == 0:
|
||||
self._change_timeout = gobject.timeout_add(10000, self._update_something)
|
||||
|
||||
def set_registered(self, value):
|
||||
if value:
|
||||
self._registered = True
|
||||
|
||||
def _load_config(self):
|
||||
if not os.path.exists(self._cfg_file):
|
||||
return (None, None, False)
|
||||
if not self._cp.read([self._cfg_file]):
|
||||
return (None, None, False)
|
||||
if not self._cp.has_section(self._section):
|
||||
return (None, None, False)
|
||||
|
||||
try:
|
||||
pubkey = self._cp.get(self._section, "pubkey")
|
||||
privkey = self._cp.get(self._section, "privkey")
|
||||
registered = self._cp.get(self._section, "registered")
|
||||
return (pubkey, privkey, registered)
|
||||
except NoOptionError:
|
||||
pass
|
||||
|
||||
return (None, None, False)
|
||||
|
||||
def _save_config(self, pubkey, privkey, registered):
|
||||
# Save config again
|
||||
if not self._cp.has_section(self._section):
|
||||
self._cp.add_section(self._section)
|
||||
self._cp.set(self._section, "pubkey", pubkey)
|
||||
self._cp.set(self._section, "privkey", privkey)
|
||||
self._cp.set(self._section, "registered", registered)
|
||||
f = open(self._cfg_file, 'w')
|
||||
self._cp.write(f)
|
||||
f.close()
|
||||
|
||||
def _update_something(self):
|
||||
it = random.randint(0, 10000) % 4
|
||||
if it == 0:
|
||||
self.props.icon = _get_random_image()
|
||||
elif it == 1:
|
||||
from sugar.graphics import xocolor
|
||||
props = {_PROP_COLOR: xocolor.XoColor().to_string()}
|
||||
self.set_properties(props)
|
||||
elif it == 2:
|
||||
props = {_PROP_NICK: _get_random_name()}
|
||||
self.set_properties(props)
|
||||
elif it == 3:
|
||||
actid = ""
|
||||
idx = random.randint(0, len(self._test_activities))
|
||||
# if idx == len(self._test_activites), it means no current
|
||||
# activity
|
||||
if idx < len(self._test_activities):
|
||||
activity = self._test_activities[idx]
|
||||
actid = activity.props.id
|
||||
props = {_PROP_CURACT: actid}
|
||||
self.set_properties(props)
|
||||
return True
|
||||
|
||||
|
||||
class TestPresenceService(PresenceService):
|
||||
|
||||
def __init__(self, test_num=0, randomize=False):
|
||||
self.__test_num = test_num
|
||||
self.__randomize = randomize
|
||||
PresenceService.__init__(self)
|
||||
|
||||
def _create_owner(self):
|
||||
return TestOwner(self, self._session_bus,
|
||||
self.__test_num, self.__randomize)
|
||||
|
||||
def internal_get_activity(self, actid):
|
||||
return self._activities.get(actid, None)
|
||||
|
||||
|
||||
def _extract_public_key(keyfile):
|
||||
try:
|
||||
f = open(keyfile, "r")
|
||||
lines = f.readlines()
|
||||
f.close()
|
||||
except IOError, e:
|
||||
_logger.error("Error reading public key: %s" % e)
|
||||
return None
|
||||
|
||||
# Extract the public key
|
||||
magic = "ssh-dss "
|
||||
key = ""
|
||||
for l in lines:
|
||||
l = l.strip()
|
||||
if not l.startswith(magic):
|
||||
continue
|
||||
key = l[len(magic):]
|
||||
break
|
||||
if not len(key):
|
||||
_logger.error("Error parsing public key.")
|
||||
return None
|
||||
return key
|
||||
|
||||
def _extract_private_key(keyfile):
|
||||
"""Get a private key from a private key file"""
|
||||
# Extract the private key
|
||||
try:
|
||||
f = open(keyfile, "r")
|
||||
lines = f.readlines()
|
||||
f.close()
|
||||
except IOError, e:
|
||||
_logger.error("Error reading private key: %s" % e)
|
||||
return None
|
||||
|
||||
key = ""
|
||||
for l in lines:
|
||||
l = l.strip()
|
||||
if l.startswith("-----BEGIN DSA PRIVATE KEY-----"):
|
||||
continue
|
||||
if l.startswith("-----END DSA PRIVATE KEY-----"):
|
||||
continue
|
||||
key += l
|
||||
if not len(key):
|
||||
_logger.error("Error parsing private key.")
|
||||
return None
|
||||
return key
|
||||
|
||||
def _get_new_keypair(num):
|
||||
"""Retrieve a public/private key pair for testing"""
|
||||
# Generate keypair
|
||||
privkeyfile = os.path.join("/tmp", "test%d.key" % num)
|
||||
pubkeyfile = os.path.join("/tmp", 'test%d.key.pub' % num)
|
||||
|
||||
# force-remove key files if they exist to ssh-keygen doesn't
|
||||
# start asking questions
|
||||
try:
|
||||
os.remove(pubkeyfile)
|
||||
os.remove(privkeyfile)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % privkeyfile
|
||||
import commands
|
||||
print "Generating new keypair..."
|
||||
(s, o) = commands.getstatusoutput(cmd)
|
||||
print "Done."
|
||||
pubkey = privkey = None
|
||||
if s != 0:
|
||||
_logger.error("Could not generate key pair: %d (%s)" % (s, o))
|
||||
else:
|
||||
pubkey = _extract_public_key(pubkeyfile)
|
||||
privkey = _extract_private_key(privkeyfile)
|
||||
|
||||
try:
|
||||
os.remove(pubkeyfile)
|
||||
os.remove(privkeyfile)
|
||||
except OSError:
|
||||
pass
|
||||
return (pubkey, privkey)
|
||||
|
||||
def _get_random_name():
|
||||
"""Produce random names for testing"""
|
||||
names = ["Liam", "Noel", "Guigsy", "Whitey", "Bonehead"]
|
||||
return names[random.randint(0, len(names) - 1)]
|
||||
|
||||
def _get_random_image():
|
||||
"""Produce a random image for display"""
|
||||
import cairo, math, gtk
|
||||
|
||||
def rand():
|
||||
return random.random()
|
||||
|
||||
SIZE = 200
|
||||
|
||||
s = cairo.ImageSurface(cairo.FORMAT_ARGB32, SIZE, SIZE)
|
||||
cr = cairo.Context(s)
|
||||
|
||||
# background gradient
|
||||
cr.save()
|
||||
g = cairo.LinearGradient(0, 0, 1, 1)
|
||||
g.add_color_stop_rgba(1, rand(), rand(), rand(), rand())
|
||||
g.add_color_stop_rgba(0, rand(), rand(), rand(), rand())
|
||||
cr.set_source(g)
|
||||
cr.rectangle(0, 0, SIZE, SIZE);
|
||||
cr.fill()
|
||||
cr.restore()
|
||||
|
||||
# random path
|
||||
cr.set_line_width(10 * rand() + 5)
|
||||
cr.move_to(SIZE * rand(), SIZE * rand())
|
||||
cr.line_to(SIZE * rand(), SIZE * rand())
|
||||
cr.rel_line_to(SIZE * rand() * -1, 0)
|
||||
cr.close_path()
|
||||
cr.stroke()
|
||||
|
||||
# a circle
|
||||
cr.set_source_rgba(rand(), rand(), rand(), rand())
|
||||
cr.arc(SIZE * rand(), SIZE * rand(), 100 * rand() + 30, 0, 2 * math.pi)
|
||||
cr.fill()
|
||||
|
||||
# another circle
|
||||
cr.set_source_rgba(rand(), rand(), rand(), rand())
|
||||
cr.arc(SIZE * rand(), SIZE * rand(), 100 * rand() + 30, 0, 2 * math.pi)
|
||||
cr.fill()
|
||||
|
||||
def img_convert_func(buf, data):
|
||||
data[0] += buf
|
||||
return True
|
||||
|
||||
data = [""]
|
||||
pixbuf = gtk.gdk.pixbuf_new_from_data(s.get_data(), gtk.gdk.COLORSPACE_RGB,
|
||||
True, 8, s.get_width(), s.get_height(), s.get_stride())
|
||||
pixbuf.save_to_callback(img_convert_func, "jpeg", {"quality": "90"}, data)
|
||||
del pixbuf
|
||||
|
||||
return str(data[0])
|
@ -1,259 +0,0 @@
|
||||
# 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 string import ascii_letters, digits
|
||||
try:
|
||||
from hashlib import sha1
|
||||
except ImportError:
|
||||
# Python < 2.5
|
||||
from sha import new as sha1
|
||||
|
||||
import dbus
|
||||
import gobject
|
||||
|
||||
|
||||
_logger = logging.getLogger('s-p-s.psutils')
|
||||
|
||||
_ASCII_ALNUM = ascii_letters + digits
|
||||
|
||||
|
||||
def pubkey_to_keyid(key):
|
||||
"""Return the key ID for the given public key. This is currently its SHA-1
|
||||
in hex.
|
||||
|
||||
:Parameters:
|
||||
`key` : str
|
||||
The public key as a Base64 string
|
||||
:Returns:
|
||||
The key ID as a string of hex digits
|
||||
"""
|
||||
return sha1(key).hexdigest()
|
||||
|
||||
|
||||
def escape_identifier(identifier):
|
||||
"""Escape the given string to be a valid D-Bus object path or service
|
||||
name component, using a reversible encoding to ensure uniqueness.
|
||||
|
||||
The reversible encoding is as follows:
|
||||
|
||||
* The empty string becomes '_'
|
||||
* Otherwise, each non-alphanumeric character is replaced by '_' plus
|
||||
two lower-case hex digits; the same replacement is carried out on
|
||||
the first character, if it's a digit
|
||||
"""
|
||||
# '' -> '_'
|
||||
if not identifier:
|
||||
return '_'
|
||||
|
||||
# A bit of a fast path for strings which are already OK.
|
||||
# We deliberately omit '_' because, for reversibility, that must also
|
||||
# be escaped.
|
||||
if (identifier.strip(_ASCII_ALNUM) == '' and
|
||||
identifier[0] in ascii_letters):
|
||||
return identifier
|
||||
|
||||
# The first character may not be a digit
|
||||
if identifier[0] not in ascii_letters:
|
||||
ret = ['_%02x' % ord(identifier[0])]
|
||||
else:
|
||||
ret = [identifier[0]]
|
||||
|
||||
# Subsequent characters may be digits or ASCII letters
|
||||
for c in identifier[1:]:
|
||||
if c in _ASCII_ALNUM:
|
||||
ret.append(c)
|
||||
else:
|
||||
ret.append('_%02x' % ord(c))
|
||||
|
||||
return ''.join(ret)
|
||||
|
||||
|
||||
NM_SERVICE = 'org.freedesktop.NetworkManager'
|
||||
NM_IFACE = 'org.freedesktop.NetworkManager'
|
||||
NM_IFACE_DEVICES = 'org.freedesktop.NetworkManager.Devices'
|
||||
NM_PATH = '/org/freedesktop/NetworkManager'
|
||||
|
||||
_ip4am = None
|
||||
|
||||
class IP4AddressMonitor(gobject.GObject):
|
||||
"""This class, and direct buddy IPv4 address access, will go away quite soon"""
|
||||
|
||||
__gsignals__ = {
|
||||
'address-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
||||
([gobject.TYPE_PYOBJECT]))
|
||||
}
|
||||
|
||||
__gproperties__ = {
|
||||
'address' : (str, None, None, None, gobject.PARAM_READABLE)
|
||||
}
|
||||
|
||||
def get_instance():
|
||||
"""Retrieve (or create) the IP4Address monitor singleton instance"""
|
||||
global _ip4am
|
||||
if not _ip4am:
|
||||
_ip4am = IP4AddressMonitor()
|
||||
return _ip4am
|
||||
get_instance = staticmethod(get_instance)
|
||||
|
||||
def __init__(self):
|
||||
gobject.GObject.__init__(self)
|
||||
self._nm_present = False
|
||||
self._nm_has_been_present = False
|
||||
self._matches = []
|
||||
self._addr = None
|
||||
self._nm_obj = None
|
||||
|
||||
sys_bus = dbus.SystemBus()
|
||||
self._watch = sys_bus.watch_name_owner(NM_SERVICE, self._nm_owner_cb)
|
||||
if not sys_bus.name_has_owner(NM_SERVICE):
|
||||
addr = self._get_address_fallback()
|
||||
self._update_address(addr)
|
||||
|
||||
def do_get_property(self, pspec):
|
||||
if pspec.name == "address":
|
||||
return self._addr
|
||||
|
||||
def _update_address(self, new_addr):
|
||||
if new_addr == "0.0.0.0":
|
||||
new_addr = None
|
||||
if new_addr == self._addr:
|
||||
return
|
||||
|
||||
self._addr = new_addr
|
||||
_logger.debug("IP4 address now '%s'" % new_addr)
|
||||
self.emit('address-changed', new_addr)
|
||||
|
||||
def _connect_to_nm(self):
|
||||
"""Connect to NM device state signals to tell when the IPv4 address changes"""
|
||||
try:
|
||||
sys_bus = dbus.SystemBus()
|
||||
proxy = sys_bus.get_object(NM_SERVICE, NM_PATH)
|
||||
self._nm_obj = dbus.Interface(proxy, NM_IFACE)
|
||||
except dbus.DBusException, err:
|
||||
_logger.debug("Error finding NetworkManager: %s" % err)
|
||||
self._nm_present = False
|
||||
return
|
||||
|
||||
sys_bus = dbus.SystemBus()
|
||||
match = sys_bus.add_signal_receiver(self._nm_device_active_cb,
|
||||
signal_name="DeviceNowActive",
|
||||
dbus_interface=NM_IFACE)
|
||||
self._matches.append(match)
|
||||
|
||||
match = sys_bus.add_signal_receiver(self._nm_device_no_longer_active_cb,
|
||||
signal_name="DeviceNoLongerActive",
|
||||
dbus_interface=NM_IFACE,
|
||||
bus_name=NM_SERVICE)
|
||||
self._matches.append(match)
|
||||
|
||||
match = sys_bus.add_signal_receiver(self._nm_state_change_cb,
|
||||
signal_name="StateChange",
|
||||
dbus_interface=NM_IFACE,
|
||||
bus_name=NM_SERVICE)
|
||||
self._matches.append(match)
|
||||
|
||||
state = self._nm_obj.state()
|
||||
if state == 3: # NM_STATE_CONNECTED
|
||||
self._query_devices()
|
||||
|
||||
def _device_properties_cb(self, *props):
|
||||
active = props[4]
|
||||
if not active:
|
||||
return
|
||||
act_stage = props[5]
|
||||
# HACK: OLPC NM has an extra stage, so activated == 8 on OLPC
|
||||
# but 7 everywhere else
|
||||
if act_stage != 8 and act_stage != 7:
|
||||
# not activated
|
||||
return
|
||||
self._update_address(props[6])
|
||||
|
||||
def _device_properties_error_cb(self, err):
|
||||
_logger.debug("Error querying device properties: %s" % err)
|
||||
|
||||
def _query_device_properties(self, device):
|
||||
sys_bus = dbus.SystemBus()
|
||||
proxy = sys_bus.get_object(NM_SERVICE, device)
|
||||
dev = dbus.Interface(proxy, NM_IFACE_DEVICES)
|
||||
dev.getProperties(reply_handler=self._device_properties_cb,
|
||||
error_handler=self._device_properties_error_cb)
|
||||
|
||||
def _get_devices_cb(self, ops):
|
||||
"""Query each device's properties"""
|
||||
for op in ops:
|
||||
self._query_device_properties(op)
|
||||
|
||||
def _get_devices_error_cb(self, err):
|
||||
_logger.debug("Error getting NetworkManager devices: %s" % err)
|
||||
|
||||
def _query_devices(self):
|
||||
"""Query NM for a list of network devices"""
|
||||
self._nm_obj.getDevices(reply_handler=self._get_devices_cb,
|
||||
error_handler=self._get_devices_error_cb)
|
||||
|
||||
def _nm_device_active_cb(self, device, ssid=None):
|
||||
self._query_device_properties(device)
|
||||
|
||||
def _nm_device_no_longer_active_cb(self, device):
|
||||
self._update_address(None)
|
||||
|
||||
def _nm_state_change_cb(self, new_state):
|
||||
if new_state == 4: # NM_STATE_DISCONNECTED
|
||||
self._update_address(None)
|
||||
|
||||
def _nm_owner_cb(self, unique_name):
|
||||
"""Clear state when NM goes away"""
|
||||
if unique_name == '':
|
||||
# NM went away, or isn't there at all
|
||||
self._nm_present = False
|
||||
for match in self._matches:
|
||||
match.remove()
|
||||
self._matches = []
|
||||
if self._nm_has_been_present:
|
||||
self._update_address(None)
|
||||
else:
|
||||
addr = self._get_address_fallback()
|
||||
self._update_address(addr)
|
||||
elif not self._nm_present:
|
||||
# NM started up
|
||||
self._nm_present = True
|
||||
self._nm_has_been_present = True
|
||||
self._connect_to_nm()
|
||||
|
||||
def _get_iface_address(self, iface):
|
||||
import socket
|
||||
import fcntl
|
||||
import struct
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
fd = s.fileno()
|
||||
SIOCGIFADDR = 0x8915
|
||||
addr = fcntl.ioctl(fd, SIOCGIFADDR, struct.pack('256s', iface[:15]))[20:24]
|
||||
s.close()
|
||||
return socket.inet_ntoa(addr)
|
||||
|
||||
def _get_address_fallback(self):
|
||||
import commands
|
||||
(s, o) = commands.getstatusoutput("/sbin/route -n")
|
||||
if s != 0:
|
||||
return
|
||||
for line in o.split('\n'):
|
||||
fields = line.split(" ")
|
||||
if fields[0] == "0.0.0.0":
|
||||
iface = fields[len(fields) - 1]
|
||||
return self._get_iface_address(iface)
|
||||
return None
|
File diff suppressed because it is too large
Load Diff
@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vi: ts=4 ai noet
|
||||
#
|
||||
# Copyright (C) 2006, Red Hat, Inc.
|
||||
#
|
||||
# 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
|
||||
import sys
|
||||
import os
|
||||
|
||||
from sugar import logger
|
||||
from sugar import env
|
||||
|
||||
|
||||
_logger = logging.getLogger('s-p-s')
|
||||
|
||||
|
||||
def usage():
|
||||
_logger.debug("Usage: sugar-presence-service [<test buddy number (1 - 10)>] [randomize]")
|
||||
|
||||
sys.path.append(env.get_service_path('presence'))
|
||||
|
||||
test_num = 0
|
||||
randomize = False
|
||||
if len(sys.argv) in [2, 3]:
|
||||
try:
|
||||
test_num = int(sys.argv[1])
|
||||
except ValueError:
|
||||
_logger.debug("Bad test user number.")
|
||||
if test_num < 1 or test_num > 10:
|
||||
_logger.debug("Bad test user number.")
|
||||
|
||||
if len(sys.argv) == 3 and sys.argv[2] == "randomize":
|
||||
randomize = True
|
||||
elif len(sys.argv) == 1:
|
||||
pass
|
||||
else:
|
||||
usage()
|
||||
os._exit(1)
|
||||
|
||||
if test_num > 0:
|
||||
logger.start('test-%d-presenceservice' % test_num)
|
||||
else:
|
||||
logger.start('presenceservice')
|
||||
|
||||
import presenceservice
|
||||
|
||||
_logger.info('Starting presence service...')
|
||||
|
||||
presenceservice.main(test_num, randomize)
|
@ -1,12 +0,0 @@
|
||||
print "Running test_psutils..."
|
||||
|
||||
from psutils import escape_identifier, pubkey_to_keyid
|
||||
|
||||
assert pubkey_to_keyid('abc') == 'a9993e364706816aba3e25717850c26c9cd0d89d'
|
||||
|
||||
assert escape_identifier('') == '_'
|
||||
assert escape_identifier('_') == '_5f'
|
||||
assert escape_identifier('1') == '_31'
|
||||
assert escape_identifier('a1') == 'a1'
|
||||
assert escape_identifier('1a') == '_31a'
|
||||
assert escape_identifier("0123abc_xyz\x01\xff") == '_30123abc_5fxyz_01_ff'
|
Loading…
Reference in New Issue
Block a user