Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/telepathy_plugin.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/telepathy_plugin.py')
-rw-r--r--src/telepathy_plugin.py338
1 files changed, 338 insertions, 0 deletions
diff --git a/src/telepathy_plugin.py b/src/telepathy_plugin.py
new file mode 100644
index 0000000..42133f4
--- /dev/null
+++ b/src/telepathy_plugin.py
@@ -0,0 +1,338 @@
+"""Base class for Telepathy plugins."""
+
+# 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 logging
+from itertools import izip
+
+import gobject
+
+from telepathy.client import Channel
+from telepathy.constants import (CONNECTION_STATUS_DISCONNECTED,
+ CONNECTION_STATUS_CONNECTING, CONNECTION_STATUS_CONNECTED,
+ CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED,
+ HANDLE_TYPE_CONTACT, HANDLE_TYPE_ROOM)
+from telepathy.interfaces import (CONN_INTERFACE, CHANNEL_TYPE_TEXT,
+ CHANNEL_TYPE_STREAMED_MEDIA, CHANNEL_INTERFACE_GROUP)
+
+
+_logger = logging.getLogger('s-p-s.server_plugin')
+
+
+class TelepathyPlugin(gobject.GObject):
+ __gsignals__ = {
+ 'contacts-online':
+ # Contacts in the subscribe list have come online.
+ # args:
+ # contact identification (based on key ID or JID): list of str
+ # contact handle: list of int or long
+ # contact identifier (JID): list of str or unicode
+ (gobject.SIGNAL_RUN_FIRST, None, [object, object, object]),
+ 'contacts-offline':
+ # Contacts in the subscribe list have gone offline.
+ # args: iterable over contact handles
+ (gobject.SIGNAL_RUN_FIRST, None, [object]),
+ 'status':
+ # Connection status changed.
+ # args: status, reason as for Telepathy StatusChanged
+ (gobject.SIGNAL_RUN_FIRST, None, [int, int]),
+ 'activity-invitation':
+ # We were invited to join an activity
+ # args:
+ # activity room handle: int or long
+ (gobject.SIGNAL_RUN_FIRST, None, [object]),
+ 'private-invitation':
+ # We were invited to join a chat or a media call
+ # args:
+ # channel object path
+ (gobject.SIGNAL_RUN_FIRST, None, [object]),
+ }
+
+ _RECONNECT_TIMEOUT = 5000
+
+ def __init__(self, registry, owner):
+ """Initialize the ServerPlugin instance
+
+ :Parameters:
+ `registry` : telepathy.client.ManagerRegistry
+ From the PresenceService, used to find the connection
+ manager details
+ `owner` : buddy.GenericOwner
+ The Buddy representing the owner of this XO (normally a
+ buddy.ShellOwner instance)
+ """
+ gobject.GObject.__init__(self)
+
+ #: The connection, a `telepathy.client.Connection`
+ self._conn = None
+
+ #: List of dbus-python SignalMatch objects representing signal match
+ #: rules associated with the connection, so we don't leak the match
+ #: rules when disconnected.
+ self._matches = []
+
+ #: The manager registry, a `telepathy.client.ManagerRegistry`
+ self._registry = registry
+
+ #: set of contact handles: those for whom we've emitted contacts-online
+ self._online_contacts = set()
+
+ #: The owner, a `buddy.GenericOwner`
+ self._owner = owner
+ #: The owner's handle on this connection
+ self.self_handle = None
+ #: The owner's identifier (e.g. JID) on this connection
+ self.self_identifier = None
+
+ #: The connection's status
+ self._conn_status = CONNECTION_STATUS_DISCONNECTED
+
+ #: GLib signal ID for reconnections
+ self._reconnect_id = 0
+
+ #: Parameters for the connection manager
+ self._account = self._get_account_info()
+
+ #: The ``subscribe`` channel: a `telepathy.client.Channel` or None
+ self._subscribe_channel = None
+ #: The members of the ``subscribe`` channel
+ self._subscribe_members = set()
+ #: The local-pending members of the ``subscribe`` channel
+ self._subscribe_local_pending = set()
+ #: The remote-pending members of the ``subscribe`` channel
+ self._subscribe_remote_pending = set()
+
+ #: The ``publish`` channel: a `telepathy.client.Channel` or None
+ self._publish_channel = None
+
+ @property
+ def status(self):
+ """Return the Telepathy connection status."""
+ return self._conn_status
+
+ def get_connection(self):
+ """Retrieve our telepathy.client.Connection object"""
+ return self._conn
+
+ def _get_account_info(self):
+ """Retrieve connection manager parameters for this account
+ """
+ raise NotImplementedError
+
+ def start(self):
+ raise NotImplementedError
+
+ def suggest_room_for_activity(self, activity_id):
+ """Suggest a room to use to share the given activity.
+ """
+ return activity_id
+
+ def identify_contacts(self, tp_chan, handles, identifiers=None):
+ raise NotImplementedError
+
+ def _reconnect_cb(self):
+ """Attempt to reconnect to the server"""
+ self.start()
+ return False
+
+ def _init_connection(self):
+ raise NotImplementedError
+
+ def _handle_connection_status_change(self, status, reason):
+ if status == self._conn_status:
+ return
+
+ if status == CONNECTION_STATUS_CONNECTING:
+ self._conn_status = status
+ _logger.debug("status: connecting...")
+ elif status == CONNECTION_STATUS_CONNECTED:
+ if self._connected_cb():
+ _logger.debug("status: connected")
+ self._conn_status = status
+ else:
+ self.cleanup()
+ _logger.debug("status: was connected, but an error occurred")
+ elif status == CONNECTION_STATUS_DISCONNECTED:
+ self.cleanup()
+ _logger.debug("status: disconnected (reason %r)", reason)
+ if reason == CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED:
+ # FIXME: handle connection failure; retry later?
+ pass
+ else:
+ # If disconnected, but still have a network connection, retry
+ # If disconnected and no network connection, do nothing here
+ # and let the IP4AddressMonitor address-changed signal handle
+ # reconnection
+ if self._should_reconnect() and not self._reconnect_id:
+ self._reconnect_id = gobject.timeout_add(self._RECONNECT_TIMEOUT,
+ self._reconnect_cb)
+
+ self.emit('status', self._conn_status, int(reason))
+
+ def _connected_cb(self):
+ raise NotImplementedError
+
+ def _should_reconnect(self):
+ return True
+
+ def cleanup(self):
+ """If we still have a connection, disconnect it"""
+
+ matches = self._matches
+ self._matches = []
+ for match in matches:
+ match.remove()
+
+ if self._conn:
+ try:
+ self._conn[CONN_INTERFACE].Disconnect()
+ except:
+ pass
+ self._conn = None
+ self._conn_status = CONNECTION_STATUS_DISCONNECTED
+
+ if self._online_contacts:
+ self._contacts_offline(self._online_contacts)
+
+ if self._reconnect_id > 0:
+ gobject.source_remove(self._reconnect_id)
+ self._reconnect_id = 0
+
+ def _contacts_offline(self, handles):
+ """Handle contacts going offline (send message, update set)"""
+ self._online_contacts -= handles
+ _logger.debug('Contacts now offline: %r', handles)
+ self.emit("contacts-offline", handles)
+
+ def _contacts_online(self, handles):
+ """Handle contacts coming online"""
+ relevant = []
+
+ for handle in handles:
+ if handle == self.self_handle:
+ # ignore network events for Owner property changes since those
+ # are handled locally
+ pass
+ elif (handle in self._subscribe_members or
+ handle in self._subscribe_local_pending or
+ handle in self._subscribe_remote_pending):
+ relevant.append(handle)
+ # else it's probably a channel-specific handle - can't create a
+ # Buddy object for those yet
+
+ if not relevant:
+ return
+
+ jids = self._conn[CONN_INTERFACE].InspectHandles(
+ HANDLE_TYPE_CONTACT, relevant)
+
+ handle_to_objid = self.identify_contacts(None, relevant, jids)
+ objids = []
+ for handle in relevant:
+ objids.append(handle_to_objid[handle])
+
+ self._online_contacts |= frozenset(relevant)
+ _logger.debug('Contacts now online:')
+ for handle, objid in izip(relevant, objids):
+ _logger.debug(' %u .../%s', handle, objid)
+ self.emit('contacts-online', objids, relevant, jids)
+
+ def _subscribe_members_changed_cb(self, message, added, removed,
+ local_pending, remote_pending,
+ actor, reason):
+
+ added = set(added)
+ removed = set(removed)
+ local_pending = set(local_pending)
+ remote_pending = set(remote_pending)
+
+ affected = added|removed
+ affected |= local_pending
+ affected |= remote_pending
+
+ self._subscribe_members -= affected
+ self._subscribe_members |= added
+ self._subscribe_local_pending -= affected
+ self._subscribe_local_pending |= local_pending
+ self._subscribe_remote_pending -= affected
+ self._subscribe_remote_pending |= remote_pending
+
+ def _publish_members_changed_cb(self, added, removed, local_pending,
+ remote_pending, actor, reason):
+
+ if local_pending:
+ # accept all requested subscriptions
+ self._publish_channel[CHANNEL_INTERFACE_GROUP].AddMembers(
+ local_pending, '')
+
+ # subscribe to people who've subscribed to us, if necessary
+ if self._subscribe_channel is not None:
+ added = list(set(added) - self._subscribe_members
+ - self._subscribe_remote_pending)
+ if added:
+ self._subscribe_channel[CHANNEL_INTERFACE_GROUP].AddMembers(
+ added, '')
+
+ def _presence_update_cb(self, presence):
+ """Send update for online/offline status of presence"""
+
+ now_online = set()
+ now_offline = set(presence.iterkeys())
+
+ for handle in presence:
+ timestamp, statuses = presence[handle]
+ for status, params in statuses.items():
+ # FIXME: use correct logic involving the GetStatuses method
+ if status in ["available", "away", "brb", "busy", "dnd", "xa"]:
+ now_online.add(handle)
+ now_offline.discard(handle)
+
+ now_online -= self._online_contacts
+ now_offline &= self._online_contacts
+
+ if now_online:
+ self._contacts_online(now_online)
+ if now_offline:
+ self._contacts_offline(now_offline)
+
+ def _new_channel_cb(self, object_path, channel_type, handle_type, handle,
+ suppress_handler):
+ """Handle creation of a new channel
+ """
+ if (handle_type == HANDLE_TYPE_ROOM and
+ channel_type == CHANNEL_TYPE_TEXT):
+ def ready(channel):
+ def got_all_members(current, local_pending, remote_pending):
+ if local_pending:
+ self.emit('activity-invitation', handle)
+ def got_all_members_err(e):
+ _logger.debug('Unable to get channel members for %s:',
+ object_path, exc_info=1)
+
+ group = channel[CHANNEL_INTERFACE_GROUP]
+ group.GetAllMembers(reply_handler=got_all_members,
+ error_handler=got_all_members_err)
+
+ # we throw away the channel as soon as ready() finishes
+ Channel(self._conn.service_name, object_path,
+ ready_handler=ready)
+
+ elif (handle_type == HANDLE_TYPE_CONTACT and
+ channel_type in (CHANNEL_TYPE_TEXT,
+ CHANNEL_TYPE_STREAMED_MEDIA)):
+ self.emit("private-invitation", object_path)