From 459c73f9619c4b83cd6837d77906ebea1b0505d8 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer <liori@exroot.org> Date: Mon, 6 Aug 2007 23:19:57 +0000 Subject: [PATCH] Jingle: UI entry point and lots of small changes. --- data/glade/chat_control_popup_menu.glade | 9 ++ src/chat_control.py | 8 ++ src/common/connection_handlers.py | 8 +- src/common/jingle.py | 151 ++++++++++++++++------- src/common/meta.py | 36 ++++++ src/common/xmpp/protocol.py | 4 + 6 files changed, 171 insertions(+), 45 deletions(-) create mode 100644 src/common/meta.py diff --git a/data/glade/chat_control_popup_menu.glade b/data/glade/chat_control_popup_menu.glade index c3ebb1e245..61b2674165 100644 --- a/data/glade/chat_control_popup_menu.glade +++ b/data/glade/chat_control_popup_menu.glade @@ -65,6 +65,15 @@ </widget> </child> + <child> + <widget class="GtkMenuItem" id="start_voip_menuitem"> + <property name="visible">True</property> + <property name="label" translatable="yes">Start _Voice chat</property> + <property name="use_underline">True</property> + <signal name="activate" handler="_on_start_voip_menuitem_activate" last_modification_time="Tue, 03 Jan 2006 04:26:46 GMT"/> + </widget> + </child> + <child> <widget class="GtkImageMenuItem" id="add_to_roster_menuitem"> <property name="visible">True</property> diff --git a/src/chat_control.py b/src/chat_control.py index 9bf90ced91..91ca40c133 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1191,6 +1191,10 @@ class ChatControl(ChatControlBase): gajim.config.set_per('contacts', self.contact.jid, 'gpg_enabled', widget.get_active()) + def _on_start_voip_menuitem_activate(self, *things): + print 'Start VoiP' + gajim.connections[self.account].startVoiP(self.contact.jid) + def _update_gpg(self): tb = self.xml.get_widget('gpg_togglebutton') # we can do gpg @@ -1533,6 +1537,7 @@ class ChatControl(ChatControlBase): history_menuitem = xml.get_widget('history_menuitem') toggle_gpg_menuitem = xml.get_widget('toggle_gpg_menuitem') + start_voip_menuitem = xml.get_widget('start_voip_menuitem') add_to_roster_menuitem = xml.get_widget('add_to_roster_menuitem') send_file_menuitem = xml.get_widget('send_file_menuitem') information_menuitem = xml.get_widget('information_menuitem') @@ -1583,6 +1588,9 @@ class ChatControl(ChatControlBase): id = toggle_gpg_menuitem.connect('activate', self._on_toggle_gpg_menuitem_activate) self.handlers[id] = toggle_gpg_menuitem + id = start_voip_menuitem.connect('activate', + self._on_start_voip_menuitem_activate) + self.handlers[id] = start_voip_menuitem id = information_menuitem.connect('activate', self._on_contact_information_menuitem_activate) self.handlers[id] = information_menuitem diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index bf47b95a9d..2bd258ae90 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -38,6 +38,7 @@ from common import exceptions from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub from common.caps import ConnectionCaps +from common.jingle import ConnectionJingle STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] @@ -1174,12 +1175,13 @@ class ConnectionVcard: #('VCARD', {entry1: data, entry2: {entry21: data, ...}, ...}) self.dispatch('VCARD', vcard) -class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps): +class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps, ConnectionJingle): def __init__(self): ConnectionVcard.__init__(self) ConnectionBytestream.__init__(self) ConnectionCommands.__init__(self) ConnectionPubSub.__init__(self) + ConnectionJingle.__init__(self) self.gmail_url=None # List of IDs we are waiting answers for {id: (type_of_request, data), } self.awaiting_answers = {} @@ -2099,6 +2101,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, con.RegisterHandler('iq', self._search_fields_received, 'result', common.xmpp.NS_SEARCH) 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 --git a/src/common/jingle.py b/src/common/jingle.py index 49b869a5ff..dac64e4c17 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -12,19 +12,24 @@ ## ''' Handles the jingle signalling protocol. ''' +import gajim import xmpp +import meta + class JingleStates(object): ''' States in which jingle session may exist. ''' ended=0 pending=1 active=2 -class WrongState(exception): pass -class NoCommonCodec(exception): pass +class Exception(object): pass +class WrongState(Exception): pass +class NoCommonCodec(Exception): pass class JingleSession(object): ''' This represents one jingle session. ''' + __metaclass__=meta.VerboseClassType def __init__(self, con, weinitiate, jid): ''' con -- connection object, weinitiate -- boolean, are we the initiator? @@ -32,48 +37,59 @@ class JingleSession(object): self.contents={} # negotiated contents self.connection=con # connection to use # our full jid - self.ourjid=gajim.get_full_jid_from_account(self.connection.name) - self.jid=jid # jid we connect to + 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.jid + self.initiator=weinitiate and self.ourjid or self.peerjid # jid we use as the responder - self.responder=weinitiate and self.jid or self.ourjid + 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 - self.sid=con.getAnID() # sessionid + self.sid=con.connection.getAnID() # sessionid # callbacks to call on proper contents # use .prepend() to add new callbacks self.callbacks=dict((key, [self.__defaultCB]) for key in - ('content-accept', 'content-add', 'content-modify', + ('content-add', 'content-modify', 'content-remove', 'session-accept', 'session-info', 'session-initiate', 'session-terminate', 'transport-info')) self.callbacks['iq-result']=[] self.callbacks['iq-error']=[] + self.callbacks['content-accept']=[self.__contentAcceptCB, self.__defaultCB] + ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' - def addContent(self, name, description, transport, profile=None): + def addContent(self, name, content, initiator='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.''' + The protocol prohibits changing that when pending. + Initiator must be one of ('we', 'peer', 'initiator', 'responder')''' if self.state==JingleStates.pending: raise WrongState - content={'creator': 'initiator', - 'name': name, - 'description': description, - 'transport': transport} - if profile is not None: - content['profile']=profile - self.contents[('initiator', name)]=content + if (initiator=='we' and self.weinitiate) or (initiator=='peer' and not self.weinitiate): + initiator='initiator' + elif (initiator=='peer' and self.weinitiate) or (initiator=='we' and not self.weinitiate): + initiator='responder' + content.creator = initiator + content.name = name + self.contents[(initiator,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 + ''' Middle-level function to do stanza exchange. ''' def startSession(self): ''' Start session. ''' @@ -92,7 +108,7 @@ class JingleSession(object): if error: # it's an iq-error stanza callables = 'iq-error' - else if jingle: + elif jingle: # it's a jingle action action = jingle.getAttr('action') callables = action @@ -115,11 +131,20 @@ class JingleSession(object): self.connection.send(response) raise xmpp.NodeProcessed + def __contentAcceptCB(self, stanza, jingle, error): + ''' Called when we get content-accept stanza or equivalent one + (like session-accept).''' + # check which contents are accepted, call their callbacks + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name'] + + ''' Methods that make/send proper pieces of XML. They check if the session is in appropriate state. ''' - def makeJingle(self, action): + def __makeJingle(self, action): stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.jid)) - jingle = stanza.addChild('jingle', attrs= + jingle = stanza.addChild('jingle', attrs={ 'xmlns': 'http://www.xmpp.org/extensions/xep-0166.html#ns', 'action': action, 'initiator': self.initiator, @@ -127,21 +152,18 @@ class JingleSession(object): 'sid': self.sid}) return stanza, jingle - def appendContent(self, jingle, content, full=True): + def __appendContent(self, jingle, content, full=True): ''' Append <content/> element to <jingle/> element, with (full=True) or without (full=False) <content/> children. ''' - c=jingle.addChild('content', attrs={ - 'creator': content['creator'], - 'name': content['name']}) - if 'profile' in content: - c['profile']=content['profile'] if full: - c.addChild(node=content['description']) - c.addChild(node=content['transport']) + jingle.addChild(node=content.toXML()) + else: + jingle.addChild('content', + attrs={'name': content.name, 'creator': content.creator}) return c - def appendContents(self, jingle, full=True): + def __appendContents(self, jingle, full=True): ''' Append all <content/> elements to <jingle/>.''' # TODO: integrate with __appendContent? # TODO: parameters 'name', 'content'? @@ -150,6 +172,9 @@ class JingleSession(object): def __sessionInitiate(self): assert self.state==JingleStates.ended + stanza, jingle = self.__makeJingle('session-initiate') + self.__appendContents(jingle) + self.connection.send(jingle) def __sessionAccept(self): assert self.state==JingleStates.pending @@ -197,7 +222,7 @@ class JingleSession(object): self.sid = jingle['sid'] for element in jingle.iterTags('content'): content={'creator': 'initiator', - 'name': element['name'] + 'name': element['name'], 'description': element.getTag('description'), 'transport': element.getTag('transport')} if element.has_attr('profile'): @@ -207,6 +232,7 @@ class JingleSession(object): def sessionTerminateCB(self, stanza): pass class JingleAudioSession(object): + __metaclass__=meta.VerboseClassType class Codec(object): ''' This class keeps description of a single codec. ''' def __init__(self, name, id=None, **params): @@ -232,15 +258,15 @@ class JingleAudioSession(object): attrs=self.attrs, payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) - def __init__(self, con, weinitiate, jid): - JingleSession.__init__(self, con, weinitiate, jid) - if weinitiate: - pass #add voice content - self.callbacks['session-initiate'].prepend( + def __init__(self, content): + self.content = content self.initiator_codecs=[] self.responder_codecs=[] + def sessionInitiateCB(self, stanza, ourcontent): + pass + ''' "Negotiation" of codecs... simply presenting what *we* can do, nothing more... ''' def getOurCodecs(self, other=None): ''' Get a list of codecs we support. Try to get them in the same @@ -282,16 +308,46 @@ class JingleAudioSession(object): xmlns=xmpp.NS_JINGLE_AUDIO, payload=(codec.toXML() for codec in codecs)) + def toXML(self): + if not self.initiator_codecs: + # we are the initiator, so just send our codecs + self.initiator_codecs = self.getOurCodecs() + return self.__codecsList(self.initiator_codecs) + else: + # we are the responder, we SHOULD adjust our codec list + self.responder_codecs = self.getOurCodecs(self.initiator_codecs) + return self.__codecsList(self.responder_codecs) + class JingleICEUDPSession(object): - def __init__(self, con, weinitiate, jid): + __metaclass__=meta.VerboseClassType + def __init__(self, content): + self.content = content + + def _sessionInitiateCB(self): + ''' Called when we initiate the session. ''' pass -class JingleVoiP(JingleSession): + def toXML(self): + ''' ICE-UDP doesn't send much in its transport stanza... ''' + return xmpp.Node('transport', xmlns=xmpp.JINGLE_ICE_UDP) + +class JingleVoiP(object): ''' Jingle VoiP sessions consist of audio content transported over an ICE UDP protocol. ''' - def __init__(*data): - JingleAudioSession.__init__(*data) - JingleICEUDPSession.__init__(*data) + __metaclass__=meta.VerboseClassType + def __init__(self): + self.audio = JingleAudioSession(self) + self.transport = JingleICEUDPSession(self) + + def toXML(self): + ''' Return proper XML for <content/> element. ''' + return xmpp.Node('content', + attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, + childs=[self.audio.toXML(), self.transport.toXML()]) + + def _sessionInitiateCB(self): + ''' Called when we initiate the session. ''' + self.transport._sessionInitiateCB() class ConnectionJingle(object): ''' This object depends on that it is a part of Connection class. ''' @@ -307,13 +363,13 @@ class ConnectionJingle(object): ''' Add a jingle session to a jingle stanza dispatcher jingle - a JingleSession object. ''' - self.__sessions[(jingle.jid, jingle.sid)]=jingle + self.__sessions[(jingle.peerjid, jingle.sid)]=jingle def deleteJingle(self, jingle): ''' Remove a jingle session from a jingle stanza dispatcher ''' - del self.__session[(jingle.jid, jingle.sid)] + del self.__session[(jingle.peerjid, jingle.sid)] - def _jingleCB(self, con, stanza): + def _JingleCB(self, con, stanza): ''' The jingle stanza dispatcher. Route jingle stanza to proper JingleSession object, or create one if it is a new session. @@ -330,6 +386,7 @@ class ConnectionJingle(object): raise xmpp.NodeProcessed jingle = stanza.getTag('jingle') + if not jingle: return sid = jingle.getAttr('sid') # do we need to create a new jingle object @@ -341,5 +398,11 @@ class ConnectionJingle(object): # we already have such session in dispatcher... return self.__sessions[(jid, sid)].stanzaCB(stanza) - def addJingleIqCallback(jid, id, jingle): + def addJingleIqCallback(self, jid, id, jingle): self.__iq_responses[(jid, id)]=jingle + + def startVoiP(self, jid): + jingle = JingleSession(self, weinitiate=True, jid=jid) + self.addJingle(jingle) + jingle.addContent('voice', JingleVoiP()) + jingle.startSession() diff --git a/src/common/meta.py b/src/common/meta.py new file mode 100644 index 0000000000..153c2e9522 --- /dev/null +++ b/src/common/meta.py @@ -0,0 +1,36 @@ +#!/usr/bin/python + +import types + +class VerboseClassType(type): + indent = '' + + def __init__(cls, name, bases, dict): + super(VerboseClassType, cls).__init__(cls, name, bases, dict) + new = {} + print 'Initializing new class %s:' % cls + for fname, fun in dict.iteritems(): + wrap = hasattr(fun, '__call__') + print '%s%s is %s, we %s wrap it.' % \ + (cls.__class__.indent, fname, fun, wrap and 'will' or "won't") + if not wrap: continue + setattr(cls, fname, cls.wrap(name, fname, fun)) + + def wrap(cls, name, fname, fun): + def verbose(*a, **b): + args = ', '.join(map(repr, a)+map(lambda x:'%s=%r'%x, b.iteritems())) + print '%s%s.%s(%s):' % (cls.__class__.indent, name, fname, args) + cls.__class__.indent += '| ' + r = fun(*a, **b) + cls.__class__.indent = cls.__class__.indent[:-4] + print '%s+=%r' % (cls.__class__.indent, r) + return r + verbose.__name__ = fname + return verbose + +def nested_property(f): + ret = f() + p = {} + for v in ('fget', 'fset', 'fdel', 'doc'): + if v in ret: p[v]=ret[v] + return property(**p) diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 79883c4617..0cce76377d 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -55,6 +55,10 @@ NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-01 NS_IBB ='http://jabber.org/protocol/ibb' NS_INVISIBLE ='presence-invisible' # Jabberd2 NS_IQ ='iq' # Jabberd2 +NS_JINGLE ='http://www.xmpp.org/extensions/xep-0166.html#ns' # XEP-0166 +NS_JINGLE_AUDIO ='http://www.xmpp.org/extensions/xep-0167.html#ns' # XEP-0167 +NS_JINGLE_RAW_UDP='http://www.xmpp.org/extensions/xep-0177.html#ns' # XEP-0177 +NS_JINGLE_ICE_UDP='http://www.xmpp.org/extensions/xep-0176.html#ns-udp' # XEP-0176 NS_LAST ='jabber:iq:last' NS_MESSAGE ='message' # Jabberd2 NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107 -- GitLab