9d837710f5
Pass the PS's owner object to each plugin. Make the server plugin listen to property changes and push those changes to the Jabber server when they occur.
450 lines
18 KiB
Python
450 lines
18 KiB
Python
# 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
|
|
from sugar import profile
|
|
from sugar import util
|
|
from sugar import env
|
|
import gtk
|
|
from buddyiconcache import BuddyIconCache
|
|
import logging
|
|
import os
|
|
import hashlib
|
|
|
|
from telepathy.client import ConnectionManager, ManagerRegistry, Connection, Channel
|
|
from telepathy.interfaces import (
|
|
CONN_MGR_INTERFACE, CONN_INTERFACE, CHANNEL_TYPE_CONTACT_LIST, CHANNEL_INTERFACE_GROUP, CONN_INTERFACE_ALIASING,
|
|
CONN_INTERFACE_AVATARS, CONN_INTERFACE_PRESENCE, CHANNEL_TYPE_TEXT, CHANNEL_TYPE_STREAMED_MEDIA)
|
|
from telepathy.constants import (
|
|
CONNECTION_HANDLE_TYPE_NONE, CONNECTION_HANDLE_TYPE_CONTACT,
|
|
CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, CONNECTION_STATUS_CONNECTING,
|
|
CONNECTION_HANDLE_TYPE_LIST, CONNECTION_HANDLE_TYPE_CONTACT, CONNECTION_HANDLE_TYPE_ROOM,
|
|
CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED)
|
|
|
|
CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo'
|
|
|
|
_PROTOCOL = "jabber"
|
|
|
|
class InvalidBuddyError(Exception):
|
|
pass
|
|
|
|
def _buddy_icon_save_cb(buf, data):
|
|
data[0] += buf
|
|
return True
|
|
|
|
def _get_buddy_icon_at_size(icon, maxw, maxh, maxsize):
|
|
loader = gtk.gdk.PixbufLoader()
|
|
loader.write(icon)
|
|
loader.close()
|
|
unscaled_pixbuf = loader.get_pixbuf()
|
|
del loader
|
|
|
|
pixbuf = unscaled_pixbuf.scale_simple(maxw, maxh, gtk.gdk.INTERP_BILINEAR)
|
|
del unscaled_pixbuf
|
|
|
|
data = [""]
|
|
quality = 90
|
|
img_size = maxsize + 1
|
|
while img_size > maxsize:
|
|
del data
|
|
data = [""]
|
|
pixbuf.save_to_callback(_buddy_icon_save_cb, "jpeg", {"quality":"%d" % quality}, data)
|
|
quality -= 10
|
|
img_size = len(data[0])
|
|
del pixbuf
|
|
|
|
if img_size > maxsize:
|
|
del data
|
|
raise RuntimeError("could not size image less than %d bytes" % maxsize)
|
|
|
|
return str(data[0])
|
|
|
|
|
|
class ServerPlugin(gobject.GObject):
|
|
__gsignals__ = {
|
|
'contact-online': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
|
([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])),
|
|
'contact-offline': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
|
([gobject.TYPE_PYOBJECT])),
|
|
'status': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
|
([gobject.TYPE_INT, gobject.TYPE_INT])),
|
|
'avatar-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
|
([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])),
|
|
'properties-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
|
([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])),
|
|
'contact-activities-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
|
([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])),
|
|
'activity-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
|
([gobject.TYPE_PYOBJECT])),
|
|
'private-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
|
|
([gobject.TYPE_PYOBJECT]))
|
|
}
|
|
|
|
def __init__(self, registry, owner):
|
|
gobject.GObject.__init__(self)
|
|
|
|
self._icon_cache = BuddyIconCache()
|
|
|
|
self._gabble_mgr = registry.GetManager('gabble')
|
|
self._online_contacts = {} # handle -> jid
|
|
|
|
self._activities = {} # activity id -> handle
|
|
self._joined_activities = [] # (activity_id, handle of the activity channel)
|
|
|
|
self._owner = owner
|
|
self._owner.connect("property-changed", self._owner_property_changed_cb)
|
|
self._owner.connect("icon-changed", self._owner_icon_changed_cb)
|
|
|
|
self._account = self._get_account_info()
|
|
|
|
self._conn = self._init_connection()
|
|
|
|
self._reconnect_id = 0
|
|
|
|
def _owner_property_changed_cb(self, owner, properties):
|
|
print "Owner properties changed: %s" % properties
|
|
self._set_self_buddy_info()
|
|
|
|
def _owner_icon_changed_cb(self, owner, icon):
|
|
print "Owner icon changed to size %d" % len(str(icon))
|
|
self._upload_avatar()
|
|
|
|
def _get_account_info(self):
|
|
account_info = {}
|
|
|
|
pubkey = self._owner.props.key
|
|
|
|
server = profile.get_server()
|
|
if not server:
|
|
account_info['server'] = 'olpc.collabora.co.uk'
|
|
else:
|
|
account_info['server'] = server
|
|
|
|
registered = profile.get_server_registered()
|
|
account_info['register'] = not registered
|
|
|
|
khash = util.printable_hash(util._sha_data(pubkey))
|
|
account_info['account'] = "%s@%s" % (khash, account_info['server'])
|
|
|
|
account_info['password'] = profile.get_private_key_hash()
|
|
return account_info
|
|
|
|
def _find_existing_connection(self):
|
|
our_name = self._account['account']
|
|
|
|
# Search existing connections, if any, that we might be able to use
|
|
connections = Connection.get_connections()
|
|
conn = None
|
|
for item in connections:
|
|
if not item.object_path.startswith("/org/freedesktop/Telepathy/Connection/gabble/jabber/"):
|
|
continue
|
|
if item[CONN_INTERFACE].GetProtocol() != _PROTOCOL:
|
|
continue
|
|
if item[CONN_INTERFACE].GetStatus() == CONNECTION_STATUS_CONNECTED:
|
|
test_handle = item[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_CONTACT, [our_name])[0]
|
|
if item[CONN_INTERFACE].GetSelfHandle() != test_handle:
|
|
continue
|
|
return item
|
|
return None
|
|
|
|
def get_connection(self):
|
|
return self._conn
|
|
|
|
def _init_connection(self):
|
|
conn = self._find_existing_connection()
|
|
if not conn:
|
|
acct = self._account.copy()
|
|
|
|
# Create a new connection
|
|
name, path = self._gabble_mgr[CONN_MGR_INTERFACE].RequestConnection(_PROTOCOL, acct)
|
|
conn = Connection(name, path)
|
|
del acct
|
|
|
|
conn[CONN_INTERFACE].connect_to_signal('StatusChanged', self._status_changed_cb)
|
|
conn[CONN_INTERFACE].connect_to_signal('NewChannel', self._new_channel_cb)
|
|
|
|
# hack
|
|
conn._valid_interfaces.add(CONN_INTERFACE_PRESENCE)
|
|
conn._valid_interfaces.add(CONN_INTERFACE_BUDDY_INFO)
|
|
conn._valid_interfaces.add(CONN_INTERFACE_AVATARS)
|
|
conn._valid_interfaces.add(CONN_INTERFACE_ALIASING)
|
|
|
|
conn[CONN_INTERFACE_PRESENCE].connect_to_signal('PresenceUpdate',
|
|
self._presence_update_cb)
|
|
|
|
return conn
|
|
|
|
def _request_list_channel(self, name):
|
|
handle = self._conn[CONN_INTERFACE].RequestHandles(
|
|
CONNECTION_HANDLE_TYPE_LIST, [name])[0]
|
|
chan_path = self._conn[CONN_INTERFACE].RequestChannel(
|
|
CHANNEL_TYPE_CONTACT_LIST, CONNECTION_HANDLE_TYPE_LIST,
|
|
handle, True)
|
|
channel = Channel(self._conn._dbus_object._named_service, chan_path)
|
|
# hack
|
|
channel._valid_interfaces.add(CHANNEL_INTERFACE_GROUP)
|
|
return channel
|
|
|
|
def _connected_cb(self):
|
|
if self._account['register']:
|
|
# we successfully register this account
|
|
profile.set_server_registered()
|
|
|
|
# the group of contacts who may receive your presence
|
|
publish = self._request_list_channel('publish')
|
|
publish_handles, local_pending, remote_pending = publish[CHANNEL_INTERFACE_GROUP].GetAllMembers()
|
|
|
|
# the group of contacts for whom you wish to receive presence
|
|
subscribe = self._request_list_channel('subscribe')
|
|
subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers()
|
|
|
|
if local_pending:
|
|
# accept pending subscriptions
|
|
#print 'pending: %r' % local_pending
|
|
publish[CHANNEL_INTERFACE_GROUP].AddMembers(local_pending, '')
|
|
|
|
not_subscribed = list(set(publish_handles) - set(subscribe_handles))
|
|
self_handle = self._conn[CONN_INTERFACE].GetSelfHandle()
|
|
self._online_contacts[self_handle] = self._account['account']
|
|
|
|
for handle in not_subscribed:
|
|
# request subscriptions from people subscribed to us if we're not subscribed to them
|
|
subscribe[CHANNEL_INTERFACE_GROUP].AddMembers([self_handle], '')
|
|
|
|
if CONN_INTERFACE_BUDDY_INFO not in self._conn.get_valid_interfaces():
|
|
print 'OLPC information not available'
|
|
self.cleanup()
|
|
return
|
|
|
|
self._conn[CONN_INTERFACE_BUDDY_INFO].connect_to_signal('PropertiesChanged', self._properties_changed_cb)
|
|
self._conn[CONN_INTERFACE_BUDDY_INFO].connect_to_signal('ActivitiesChanged', self._activities_changed_cb)
|
|
|
|
self._conn[CONN_INTERFACE_AVATARS].connect_to_signal('AvatarUpdated', self._avatar_updated_cb)
|
|
|
|
self._conn[CONN_INTERFACE_ALIASING].connect_to_signal('AliasesChanged', self._alias_changed_cb)
|
|
|
|
try:
|
|
self._set_self_buddy_info()
|
|
except RuntimeError, e:
|
|
print e
|
|
self.cleanup()
|
|
return
|
|
|
|
# Request presence for everyone on the channel
|
|
self._conn[CONN_INTERFACE_PRESENCE].GetPresence(subscribe_handles)
|
|
|
|
def _upload_avatar(self):
|
|
icon_data = self._owner.props.icon
|
|
|
|
md5 = hashlib.md5()
|
|
md5.update(icon_data)
|
|
hash = md5.hexdigest()
|
|
|
|
self_handle = self._conn[CONN_INTERFACE].GetSelfHandle()
|
|
token = self._conn[CONN_INTERFACE_AVATARS].GetAvatarTokens([self_handle])[0]
|
|
|
|
if self._icon_cache.check_avatar(hash, token):
|
|
# avatar is up to date
|
|
return
|
|
|
|
types, minw, minh, maxw, maxh, maxsize = self._conn[CONN_INTERFACE_AVATARS].GetAvatarRequirements()
|
|
if not "image/jpeg" in types:
|
|
print "server does not accept JPEG format avatars."
|
|
return
|
|
|
|
img_data = _get_buddy_icon_at_size(icon_data, min(maxw, 96), min(maxh, 96), maxsize)
|
|
token = self._conn[CONN_INTERFACE_AVATARS].SetAvatar(img_data, "image/jpeg")
|
|
self._icon_cache.set_avatar(hash, token)
|
|
|
|
def join_activity(self, act):
|
|
handle = self._activities.get(act)
|
|
|
|
if not handle:
|
|
handle = self._conn[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_ROOM, [act])[0]
|
|
self._activities[act] = handle
|
|
|
|
if (act, handle) in self._joined_activities:
|
|
print "%s already joined" % act
|
|
return
|
|
|
|
chan_path = self._conn[CONN_INTERFACE].RequestChannel(
|
|
CHANNEL_TYPE_TEXT, CONNECTION_HANDLE_TYPE_ROOM,
|
|
handle, True)
|
|
channel = Channel(self._conn._dbus_object._named_service, chan_path)
|
|
|
|
self._joined_activities.append((act, handle))
|
|
self._conn[CONN_INTERFACE_BUDDY_INFO].SetActivities(self._joined_activities)
|
|
|
|
return channel
|
|
|
|
def _set_self_buddy_info(self):
|
|
# Set our OLPC buddy properties
|
|
props = {}
|
|
props['color'] = self._owner.props.color
|
|
props['key'] = self._owner.props.key
|
|
try:
|
|
self._conn[CONN_INTERFACE_BUDDY_INFO].SetProperties(props)
|
|
except dbus.DBusException, e:
|
|
if str(e).find("Server does not support PEP") >= 0:
|
|
raise RuntimeError("Server does not support PEP")
|
|
|
|
name = self._owner.props.nick
|
|
self_handle = self._conn[CONN_INTERFACE].GetSelfHandle()
|
|
self._conn[CONN_INTERFACE_ALIASING].SetAliases( {self_handle : name} )
|
|
|
|
self._conn[CONN_INTERFACE_BUDDY_INFO].SetActivities(self._joined_activities)
|
|
|
|
self._upload_avatar()
|
|
|
|
def _status_changed_cb(self, state, reason):
|
|
if state == CONNECTION_STATUS_CONNECTING:
|
|
print 'connecting: %r' % reason
|
|
elif state == CONNECTION_STATUS_CONNECTED:
|
|
print 'connected: %r' % reason
|
|
self._connected_cb()
|
|
self.emit('status', state, int(reason))
|
|
elif state == CONNECTION_STATUS_DISCONNECTED:
|
|
print 'disconnected: %r' % reason
|
|
self.emit('status', state, int(reason))
|
|
self._conn = None
|
|
if reason == CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED:
|
|
# FIXME: handle connection failure; retry later?
|
|
pass
|
|
return False
|
|
|
|
def start(self):
|
|
print "Trying to connect..."
|
|
# If the connection is already connected query initial contacts
|
|
conn_status = self._conn[CONN_INTERFACE].GetStatus()
|
|
if conn_status == CONNECTION_STATUS_CONNECTED:
|
|
self._connected_cb()
|
|
subscribe = self._request_list_channel('subscribe')
|
|
subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers()
|
|
self._conn[CONN_INTERFACE_PRESENCE].RequestPresence(subscribe_handles)
|
|
elif conn_status == CONNECTION_STATUS_CONNECTING:
|
|
pass
|
|
else:
|
|
self._conn[CONN_INTERFACE].Connect(reply_handler=self._connect_reply_cb,
|
|
error_handler=self._connect_error_cb)
|
|
|
|
def _connect_reply_cb(self):
|
|
if self._reconnect_id > 0:
|
|
gobject.source_remove(self._reconnect_id)
|
|
|
|
def _reconnect(self):
|
|
self._reconnect_id = 0
|
|
self.start()
|
|
return False
|
|
|
|
def _connect_error_cb(self, exception):
|
|
print "Connect error: %s" % exception
|
|
if not self._reconnect_id:
|
|
self._reconnect_id = gobject.timeout_add(10000, self._reconnect)
|
|
|
|
def cleanup(self):
|
|
if not self._conn:
|
|
return
|
|
self._conn[CONN_INTERFACE].Disconnect()
|
|
|
|
def _contact_offline(self, handle):
|
|
self.emit("contact-offline", handle)
|
|
del self._online_contacts[handle]
|
|
|
|
def _contact_online(self, handle):
|
|
try:
|
|
props = self._conn[CONN_INTERFACE_BUDDY_INFO].GetProperties(handle)
|
|
except dbus.DBusException, e:
|
|
if str(e).startswith("org.freedesktop.DBus.Error.NoReply"):
|
|
raise InvalidBuddyError("couldn't get properties")
|
|
|
|
if not props.has_key('color'):
|
|
raise InvalidBuddyError("no color")
|
|
if not props.has_key('key'):
|
|
raise InvalidBuddyError("no key")
|
|
|
|
jid = self._conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0]
|
|
nick = self._conn[CONN_INTERFACE_ALIASING].RequestAliases([handle])[0]
|
|
if not nick:
|
|
raise InvalidBuddyError("no name")
|
|
props['nick'] = nick
|
|
|
|
self._online_contacts[handle] = jid
|
|
self.emit("contact-online", handle, props)
|
|
|
|
activities = self._conn[CONN_INTERFACE_BUDDY_INFO].GetActivities(handle)
|
|
self._activities_changed_cb(handle, activities)
|
|
|
|
def _presence_update_cb(self, presence):
|
|
for handle in presence:
|
|
timestamp, statuses = presence[handle]
|
|
online = handle in self._online_contacts
|
|
for status, params in statuses.items():
|
|
jid = self._conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0]
|
|
print "Handle %s (%s) was online=%s. new statuse %s" % (handle, jid, online, status)
|
|
if not online and status in ["available", "away", "brb", "busy", "dnd", "xa"]:
|
|
try:
|
|
self._contact_online(handle)
|
|
except InvalidBuddyError, e:
|
|
print "Not adding %s because %s" % (handle, e)
|
|
elif online and status in ["offline", "invisible"]:
|
|
self._contact_offline(handle)
|
|
|
|
def _avatar_updated_cb(self, handle, new_avatar_token):
|
|
jid = self._online_contacts[handle]
|
|
icon = self._icon_cache.get_icon(jid, new_avatar_token)
|
|
if not icon:
|
|
# cache miss
|
|
avatar, mime_type = self._conn[CONN_INTERFACE_AVATARS].RequestAvatar(handle)
|
|
icon = ''.join(map(chr, avatar))
|
|
self._icon_cache.store_icon(jid, new_avatar_token, icon)
|
|
|
|
self.emit("avatar-updated", handle, icon)
|
|
|
|
def _alias_changed_cb(self, aliases):
|
|
for handle, alias in aliases:
|
|
prop = {'nick': alias}
|
|
#print "Buddy %s alias changed to %s" % (handle, alias)
|
|
self._properties_changed_cb(handle, prop)
|
|
|
|
def _properties_changed_cb(self, contact, properties):
|
|
self.emit("properties-changed", contact, properties)
|
|
|
|
def _activities_changed_cb(self, contact, activities):
|
|
for act_id, act_handle in activities:
|
|
self._activities[act_id] = act_handle
|
|
activities_id = map(lambda x: x[0], activities)
|
|
self.emit("contact-activities-changed", contact, activities_id)
|
|
|
|
def _new_channel_cb(self, object_path, channel_type, handle_type, handle, suppress_handler):
|
|
if handle_type == CONNECTION_HANDLE_TYPE_ROOM and channel_type == CHANNEL_TYPE_TEXT:
|
|
channel = Channel(self._conn._dbus_object._named_service, object_path)
|
|
|
|
# hack
|
|
channel._valid_interfaces.add(CHANNEL_INTERFACE_GROUP)
|
|
|
|
current, local_pending, remote_pending = channel[CHANNEL_INTERFACE_GROUP].GetAllMembers()
|
|
|
|
if local_pending:
|
|
for act_id, act_handle in self._activities.items():
|
|
if handle == act_handle:
|
|
self.emit("activity-invitation", act_id)
|
|
|
|
elif handle_type == CONNECTION_HANDLE_TYPE_CONTACT and \
|
|
channel_type in [CHANNEL_TYPE_TEXT, CHANNEL_TYPE_STREAMED_MEDIA]:
|
|
self.emit("private-invitation", object_path)
|