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