changeset 236a04e1fa69 in /home/hg/repos/gajim
details:http://hg.gajim.org/gajim?cmd=changeset;node=236a04e1fa69 description: implement jingle negociation for whiteboard
diffstat:
src/chat_control.py | 8 +- src/common/connection_handlers.py | 10 +- src/common/jingle.py | 699 +++++++++++++++++++++++++++++++++++++++++++ src/common/xmpp/protocol.py | 6 + src/common/xmpp/simplexml.py | 3 + src/dialogs.py | 31 + src/gajim.py | 114 +++++++- src/whiteboardwidget.py | 44 +-- 8 files changed, 868 insertions(+), 47 deletions(-)
diffs (truncated from 1045 to 300 lines):
diff -r 77f805b84006 -r 236a04e1fa69 src/chat_control.py --- a/src/chat_control.py Tue Jul 21 15:58:30 2009 +0200 +++ b/src/chat_control.py Tue Jul 21 16:23:34 2009 +0200 @@ -2749,14 +2749,18 @@ self.print_conversation(')', 'status', simple=True) def _on_whiteboard_button_clicked(self, widget): + gajim.connections[self.account].startWhiteboard( + self.contact.get_full_jid()) + + def draw_whiteboard(self, session): hbox = self.xml.get_widget('chat_child_hbox') if len(hbox.get_children()) == 1: - whiteboard = Whiteboard(self.account, self.contact) + whiteboard = Whiteboard(self.account, self.contact, session) try: + # set minimum size whiteboard.hbox.set_size_request(300, 0) hbox.pack_start(whiteboard.hbox, expand=False, fill=False) whiteboard.hbox.show_all() - except: pass # TODO: Fix problem with groupchat?
diff -r 77f805b84006 -r 236a04e1fa69 src/common/connection_handlers.py --- a/src/common/connection_handlers.py Tue Jul 21 15:58:30 2009 +0200 +++ b/src/common/connection_handlers.py Tue Jul 21 16:23:34 2009 +0200 @@ -51,6 +51,7 @@ from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub from common.caps import ConnectionCaps +from common.jingle import ConnectionJingle
from common import dbus_support if dbus_support.supported: @@ -1413,13 +1414,16 @@
return sess
-class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps, ConnectionHandlersBase): +class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, +ConnectionCommands, ConnectionPubSub, ConnectionCaps, ConnectionHandlersBase, +ConnectionJingle): def __init__(self): ConnectionVcard.__init__(self) ConnectionBytestream.__init__(self) ConnectionCommands.__init__(self) ConnectionPubSub.__init__(self) ConnectionHandlersBase.__init__(self) + ConnectionJingle.__init__(self) self.gmail_url = None
# keep the latest subscribed event for each jid to prevent loop when we @@ -2639,6 +2643,10 @@ con.RegisterHandler('iq', self._PrivacySetCB, 'set', common.xmpp.NS_PRIVACY) con.RegisterHandler('iq', self._PubSubCB, 'result') + con.RegisterHandler('iq', self._JingleCB, 'result') + con.RegisterHandler('iq', self._JingleCB, 'error') + con.RegisterHandler('iq', self._JingleCB, 'set', + common.xmpp.NS_JINGLE) con.RegisterHandler('iq', self._ErrorCB, 'error') con.RegisterHandler('iq', self._IqCB) con.RegisterHandler('iq', self._StanzaArrivedCB) diff -r 77f805b84006 -r 236a04e1fa69 src/common/jingle.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/common/jingle.py Tue Jul 21 16:23:34 2009 +0200 @@ -0,0 +1,699 @@ +## +## Copyright (C) 2006 Gajim Team +## +## 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; version 2 only. +## +## 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. +## +''' Handles the jingle signalling protocol. ''' + +# note: if there will be more types of sessions (possibly file transfer, +# video...), split this file + +import gajim +import gobject +import xmpp +import urllib + +import logging +log = logging.getLogger('gajim.c.jingle') + +def timeout_add_and_call(timeout, callable, *args, **kwargs): + ''' Call a callback once. If it returns True, add a timeout handler to call it more times. + Helper function. ''' + if callable(*args, **kwargs): + return gobject.timeout_add(timeout, callable, *args, **kwargs) + return -1 # gobject.source_remove will not object + +class JingleStates(object): + ''' States in which jingle session may exist. ''' + ended=0 + pending=1 + active=2 + +class Error(Exception): pass +class WrongState(Error): pass +class NoSuchSession(Error): pass + +class JingleSession(object): + ''' This represents one jingle session. ''' + def __init__(self, con, weinitiate, jid, sid=None): + ''' con -- connection object, + weinitiate -- boolean, are we the initiator? + jid - jid of the other entity''' + self.contents={} # negotiated contents + self.connection=con # connection to use + # our full jid + self.ourjid=gajim.get_jid_from_account(self.connection.name)+'/'+con.server_resource + self.peerjid=jid # jid we connect to + # jid we use as the initiator + self.initiator=weinitiate and self.ourjid or self.peerjid + # jid we use as the responder + self.responder=weinitiate and self.peerjid or self.ourjid + # are we an initiator? + self.weinitiate=weinitiate + # what state is session in? (one from JingleStates) + self.state=JingleStates.ended + if not sid: + sid=con.connection.getAnID() + self.sid=sid # sessionid + + self.accepted=True # is this session accepted by user + + # callbacks to call on proper contents + # use .prepend() to add new callbacks, especially when you're going + # to send error instead of ack + self.callbacks={ + 'content-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], + 'content-add': [self.__defaultCB], + 'content-modify': [self.__defaultCB], + 'content-remove': [self.__defaultCB], + 'session-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], + 'session-info': [self.__defaultCB], + 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, self.__defaultCB], + 'session-terminate': [self.__broadcastAllCB, self.__defaultCB], + 'transport-info': [self.__broadcastCB, self.__defaultCB], + 'iq-result': [], + 'iq-error': [], + } + + ''' Interaction with user ''' + def approveSession(self): + ''' Called when user accepts session in UI (when we aren't the initiator).''' + self.accepted=True + self.acceptSession() + + def declineSession(self): + ''' Called when user declines session in UI (when we aren't the initiator, + or when the user wants to stop session completly. ''' + self.__sessionTerminate() + + ''' Middle-level functions to manage contents. Handle local content + cache and send change notifications. ''' + def addContent(self, name, content, creator='we'): + ''' Add new content to session. If the session is active, + this will send proper stanza to update session. + The protocol prohibits changing that when pending. + Creator must be one of ('we', 'peer', 'initiator', 'responder')''' + assert creator in ('we', 'peer', 'initiator', 'responder') + + if self.state==JingleStates.pending: + raise WrongState + + if (creator=='we' and self.weinitiate) or (creator=='peer' and not self.weinitiate): + creator='initiator' + elif (creator=='peer' and self.weinitiate) or (creator=='we' and not self.weinitiate): + creator='responder' + content.creator = creator + content.name = name + self.contents[(creator,name)]=content + + if self.state==JingleStates.active: + pass # TODO: send proper stanza, shouldn't be needed now + + def removeContent(self, creator, name): + ''' We do not need this now ''' + pass + + def modifyContent(self, creator, name, *someother): + ''' We do not need this now ''' + pass + + def acceptSession(self): + ''' Check if all contents and user agreed to start session. ''' + if not self.weinitiate and self.accepted and \ + all(c.negotiated for c in self.contents.itervalues()): + self.__sessionAccept() + ''' Middle-level function to do stanza exchange. ''' + def startSession(self): + ''' Start session. ''' + self.__sessionInitiate() + + def sendSessionInfo(self): pass + + ''' Session callbacks. ''' + def stanzaCB(self, stanza): + ''' A callback for ConnectionJingle. It gets stanza, then + tries to send it to all internally registered callbacks. + First one to raise xmpp.NodeProcessed breaks function.''' + jingle = stanza.getTag('jingle') + error = stanza.getTag('error') + if error: + # it's an iq-error stanza + action = 'iq-error' + elif jingle: + # it's a jingle action + action = jingle.getAttr('action') + else: + # it's an iq-result (ack) stanza + action = 'iq-result' + + callables = self.callbacks[action] + + try: + for callable in callables: + callable(stanza=stanza, jingle=jingle, error=error, action=action) + except xmpp.NodeProcessed: + pass + + def __defaultCB(self, stanza, jingle, error, action): + ''' Default callback for action stanzas -- simple ack + and stop processing. ''' + response = stanza.buildReply('result') + self.connection.connection.send(response) + + def __contentAcceptCB(self, stanza, jingle, error, action): + ''' Called when we get content-accept stanza or equivalent one + (like session-accept).''' + # check which contents are accepted + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name'] + + def __sessionInitiateCB(self, stanza, jingle, error, action): + ''' We got a jingle session request from other entity, + therefore we are the receiver... Unpack the data, + inform the user. ''' + self.initiator = jingle['initiator'] + self.responder = self.ourjid + self.peerjid = self.initiator + self.accepted = False # user did not accept this session yet + + # TODO: If the initiator is unknown to the receiver (e.g., via presence + # subscription) and the receiver has a policy of not communicating via + # Jingle with unknown entities, it SHOULD return a <service-unavailable/> + # error. + + # Lets check what kind of jingle session does the peer want + fail = True + contents = [] + for element in jingle.iterTags('content'): + # checking what kind of session this will be + desc_ns = element.getTag('description').getNamespace() + tran_ns = element.getTag('transport').getNamespace() + if desc_ns==xmpp.NS_JINGLE_RTP and tran_ns==xmpp.NS_JINGLE_ICE_UDP: + # we've got voip content + self.addContent(element['name'], JingleVoiP(self), 'peer') + contents.append(('VOIP',)) + fail = False + if desc_ns==xmpp.NS_JINGLE_XHTML and tran_ns==xmpp.NS_JINGLE_SXE: + # we've got whiteboard content + self.addContent(element['name'], JingleWhiteboard(self), 'peer') + contents.append(('WHITEBOARD',)) + fail = False + + # If there's no content we understand... + if fail: + # TODO: we should send <unsupported-content/> inside too + # TODO: delete this instance + self.connection.connection.send( + xmpp.Error(stanza, xmpp.NS_STANZAS + 'feature-not-implemented')) + self.connection.deleteJingle(self) + raise xmpp.NodeProcessed + + self.state = JingleStates.pending + + # Send event about starting a session + self.connection.dispatch('JINGLE_INCOMING', (self.initiator, self.sid, contents)) + + def __broadcastCB(self, stanza, jingle, error, action): + ''' Broadcast the stanza contents to proper content handlers. ''' + for content in jingle.iterTags('content'): + name = content['name'] + creator = content['creator'] + cn = self.contents[(creator, name)] + cn.stanzaCB(stanza, content, error, action) + + def __broadcastAllCB(self, stanza, jingle, error, action):