From e0123f0c24bd1f9c526ff37013a09bae81b5cf5c Mon Sep 17 00:00:00 2001
From: Yann Leboulanger <asterix@lagaule.org>
Date: Fri, 6 Feb 2009 19:01:36 +0000
Subject: [PATCH] encrypt and decrypt GPG messages in a thread, and call a
 callback when it's finished (sending a message is now asyncronous). Fixes
 #4445

---
 src/chat_control.py                        | 30 ++++---
 src/common/connection.py                   | 96 ++++++++++++++--------
 src/common/connection_handlers.py          | 21 ++++-
 src/common/gajim.py                        |  1 +
 src/common/xmpp/dispatcher_nb.py           |  5 +-
 src/common/zeroconf/client_zeroconf.py     | 44 ++++++----
 src/common/zeroconf/connection_zeroconf.py | 13 +--
 src/gajim.py                               | 18 ++++
 src/message_control.py                     | 17 ++--
 9 files changed, 157 insertions(+), 88 deletions(-)

diff --git a/src/chat_control.py b/src/chat_control.py
index 6613f6b8af..d93adc6d45 100644
--- a/src/chat_control.py
+++ b/src/chat_control.py
@@ -312,6 +312,7 @@ class ChatControlBase(MessageControl):
 				spell.set_language(lang)
 		except (gobject.GError, RuntimeError):
 			dialogs.AspellDictError(lang)
+
 	def on_msg_textview_populate_popup(self, textview, menu):
 		'''we override the default context menu and we prepend an option to switch languages'''
 		def _on_select_dictionary(widget, lang):
@@ -594,21 +595,19 @@ class ChatControlBase(MessageControl):
 			return True
 		return False
 
-	def send_message(self, message, keyID = '', type_ = 'chat', chatstate = None,
-	msg_id = None, composing_xep = None, resource = None,
-	process_command = True, xhtml = None):
+	def send_message(self, message, keyID='', type_='chat', chatstate=None,
+	msg_id=None, composing_xep=None, resource=None, process_command=True,
+	xhtml=None, callback=None, callback_args=[]):
 		'''Send the given message to the active tab. Doesn't return None if error
 		'''
 		if not message or message == '\n':
 			return None
 
-		ret = None
-
 		if not process_command or not self._process_command(message):
-			ret = MessageControl.send_message(self, message, keyID, type_ = type_,
-				chatstate = chatstate, msg_id = msg_id,
-				composing_xep = composing_xep, resource = resource,
-				user_nick = self.user_nick, xhtml = xhtml)
+			MessageControl.send_message(self, message, keyID, type_=type_,
+				chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep,
+				resource=resource, user_nick=self.user_nick, xhtml=xhtml,
+				callback=callback, callback_args=callback_args)
 
 			# Record message history
 			self.save_sent_message(message)
@@ -620,8 +619,6 @@ class ChatControlBase(MessageControl):
 		message_buffer = self.msg_textview.get_buffer()
 		message_buffer.set_text('') # clear message buffer (and tv of course)
 
-		return ret
-
 	def save_sent_message(self, message):
 		# save the message, so user can scroll though the list with key up/down
 		size = len(self.sent_history)
@@ -1802,11 +1799,7 @@ class ChatControl(ChatControlBase):
 				gobject.source_remove(self.possible_inactive_timeout_id)
 				self._schedule_activity_timers()
 
-		id_ = ChatControlBase.send_message(self, message, keyID,
-			type_ = 'chat', chatstate = chatstate_to_send,
-			composing_xep = composing_xep,
-			process_command = process_command, xhtml = xhtml)
-		if id_:
+		def _on_sent(id_, contact, message, encrypted, xhtml):
 			# XXX: Once we have fallback to disco, remove notexistant check
 			if gajim.capscache.is_supported(contact, NS_RECEIPTS) \
 			and not gajim.capscache.is_supported(contact,
@@ -1820,6 +1813,11 @@ class ChatControl(ChatControlBase):
 				encrypted = encrypted, xep0184_id = xep0184_id,
 				xhtml = xhtml)
 
+		ChatControlBase.send_message(self, message, keyID, type_='chat',
+			chatstate=chatstate_to_send, composing_xep=composing_xep,
+			process_command=process_command, xhtml=xhtml, callback=_on_sent,
+			callback_args=[contact, message, encrypted, xhtml])
+
 	def check_for_possible_paused_chatstate(self, arg):
 		''' did we move mouse of that window or write something in message
 		textview in the last 5 seconds?
diff --git a/src/common/connection.py b/src/common/connection.py
index cd7ed005c7..6c2ed9ecac 100644
--- a/src/common/connection.py
+++ b/src/common/connection.py
@@ -1135,7 +1135,7 @@ class Connection(ConnectionHandlers):
 	def send_message(self, jid, msg, keyID, type_='chat', subject='',
 	chatstate=None, msg_id=None, composing_xep=None, resource=None,
 	user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None,
-	original_message=None, delayed=None):
+	original_message=None, delayed=None, callback=None, callback_args=[]):
 		if not self.connection or self.connected < 2:
 			return 1
 		try:
@@ -1151,7 +1151,7 @@ class Connection(ConnectionHandlers):
 			from common.rst_xhtml_generator import create_xhtml
 			xhtml = create_xhtml(msg)
 		if not msg and chatstate is None and form_node is None:
-			return 2
+			return
 		fjid = jid
 		if resource:
 			fjid += '/' + resource
@@ -1168,30 +1168,61 @@ class Connection(ConnectionHandlers):
 			elif keyID.endswith('MISMATCH'):
 				error = _('The contact\'s key (%s) does not match the key assigned in Gajim.' % keyID[:8])
 			else:
-				#encrypt
-				msgenc, error = self.gpg.encrypt(msg, [keyID])
-			if msgenc and not error:
-				msgtxt = '[This message is *encrypted* (See :XEP:`27`]'
-				lang = os.getenv('LANG')
-				if lang is not None and lang != 'en': # we're not english
-					# one in locale and one en
-					msgtxt = _('[This message is *encrypted* (See :XEP:`27`]') + \
-						' (' + msgtxt + ')'
-			else:
-				# Encryption failed, do not send message
-				tim = localtime()
-				self.dispatch('MSGNOTSENT', (jid, error, msgtxt, tim, session))
-				return 3
+				def encrypt_thread(msg, keyID):
+					# encrypt message. This function returns (msgenc, error)
+					return self.gpg.encrypt(msg, [keyID])
+				gajim.thread_interface(encrypt_thread, [msg, keyID],
+					self._on_message_encrypted, [type_, msg, msgtxt,
+						original_message, fjid, resource, jid, xhtml, subject,
+						chatstate, composing_xep, forward_from, delayed, session,
+						form_node, user_nick, keyID, callback, callback_args])
+				return
+
+			self._on_message_encrypted(self, ('', error), type_, msg, msgtxt,
+				original_message, fjid, resource, jid, xhtml, subject, chatstate,
+				composing_xep, forward_from, delayed, session, form_node, user_nick,
+				keyID, callback, callback_args)
+
+		self._on_continue_message(type_, msg, msgtxt, original_message, fjid,
+			resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep,
+			forward_from, delayed, session, form_node, user_nick, callback,
+			callback_args)
+
+	def _on_message_encrypted(self, output, type_, msg, msgtxt, original_message,
+	fjid, resource, jid, xhtml, subject, chatstate, composing_xep, forward_from,
+	delayed, session, form_node, user_nick, keyID, callback, callback_args):
+		msgenc, error = output
+
+		if msgenc and not error:
+			msgtxt = '[This message is *encrypted* (See :XEP:`27`]'
+			lang = os.getenv('LANG')
+			if lang is not None and lang != 'en': # we're not english
+				# one in locale and one en
+				msgtxt = _('[This message is *encrypted* (See :XEP:`27`]') + \
+					' (' + msgtxt + ')'
+			self._on_continue_message(type_, msg, msgtxt, original_message, fjid,
+				resource, jid, xhtml, subject, msgenc, keyID, chatstate,
+				composing_xep, forward_from, delayed, session, form_node, user_nick,
+				callback, callback_args)
+			return
+		# Encryption failed, do not send message
+		tim = localtime()
+		self.dispatch('MSGNOTSENT', (jid, error, msgtxt, tim, session))
+
+	def _on_continue_message(self, type_, msg, msgtxt, original_message, fjid,
+	resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep,
+	forward_from, delayed, session, form_node, user_nick, callback,
+	callback_args):
 		if type_ == 'chat':
-			msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type_,
-				xhtml = xhtml)
+			msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ=type_,
+				xhtml=xhtml)
 		else:
 			if subject:
-				msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
-					typ = 'normal', subject = subject, xhtml = xhtml)
+				msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal',
+					subject=subject, xhtml=xhtml)
 			else:
-				msg_iq = common.xmpp.Message(to = fjid, body = msgtxt,
-					typ = 'normal', xhtml = xhtml)
+				msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal',
+					xhtml=xhtml)
 		if msgenc:
 			msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc)
 
@@ -1206,11 +1237,9 @@ class Connection(ConnectionHandlers):
 		# TODO: We might want to write a function so we don't need to
 		#	reproduce that ugly if somewhere else.
 		if resource:
-			contact = gajim.contacts.get_contact(self.name, jid,
-				resource)
+			contact = gajim.contacts.get_contact(self.name, jid, resource)
 		else:
-			contact = gajim.contacts. \
-				get_contact_with_highest_priority(self.name,
+			contact = gajim.contacts.get_contact_with_highest_priority(self.name,
 				jid)
 
 		# chatstates - if peer supports xep85 or xep22, send chatstates
@@ -1226,16 +1255,13 @@ class Connection(ConnectionHandlers):
 			not gajim.capscache.is_supported(contact,
 			'notexistant')):
 				# XEP-0085
-				msg_iq.setTag(chatstate,
-					namespace = common.xmpp.NS_CHATSTATES)
+				msg_iq.setTag(chatstate, namespace=common.xmpp.NS_CHATSTATES)
 			if composing_xep in ('XEP-0022', 'asked_once') or \
 			not composing_xep:
 				# XEP-0022
-				chatstate_node = msg_iq.setTag('x',
-					namespace = common.xmpp.NS_EVENT)
+				chatstate_node = msg_iq.setTag('x', namespace=common.xmpp.NS_EVENT)
 				if chatstate is 'composing' or msgtxt:
-					chatstate_node.addChild(
-						name = 'composing')
+					chatstate_node.addChild(name='composing')
 
 		if forward_from:
 			addresses = msg_iq.addChild('addresses',
@@ -1255,8 +1281,7 @@ class Connection(ConnectionHandlers):
 		if msgtxt and gajim.config.get_per('accounts', self.name,
 		'request_receipt') and gajim.capscache.is_supported(contact,
 		common.xmpp.NS_RECEIPTS):
-			msg_iq.setTag('request',
-				namespace=common.xmpp.NS_RECEIPTS)
+			msg_iq.setTag('request', namespace=common.xmpp.NS_RECEIPTS)
 
 		if session:
 			# XEP-0201
@@ -1294,7 +1319,8 @@ class Connection(ConnectionHandlers):
 							common.logger.LOG_DB_PATH
 		self.dispatch('MSGSENT', (jid, msg, keyID))
 
-		return msg_id
+		if callback:
+			callback(msg_id, *callback_args)
 
 	def send_stanza(self, stanza):
 		''' send a stanza untouched '''
diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py
index a04ac525e1..6648831f5f 100644
--- a/src/common/connection_handlers.py
+++ b/src/common/connection_handlers.py
@@ -1772,6 +1772,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
 		encrypted = False
 		xep_200_encrypted = msg.getTag('c', namespace=common.xmpp.NS_STANZA_CRYPTO)
 
+		session = None
 		if mtype != 'groupchat':
 			session = self.get_or_create_session(frm, thread_id)
 
@@ -1853,10 +1854,22 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
 
 			keyID = gajim.config.get_per('accounts', self.name, 'keyid')
 			if keyID:
-				decmsg = self.gpg.decrypt(encmsg, keyID)
-				# \x00 chars are not allowed in C (so in GTK)
-				msgtxt = decmsg.replace('\x00', '')
-				encrypted = 'xep27'
+				def decrypt_thread(encmsg, keyID):
+					decmsg = self.gpg.decrypt(encmsg, keyID)
+					# \x00 chars are not allowed in C (so in GTK)
+					msgtxt = decmsg.replace('\x00', '')
+					encrypted = 'xep27'
+					return (msgtxt, encrypted)
+				gajim.thread_interface(decrypt_thread, [encmsg, keyID],
+					self._on_message_decrypted, [mtype, msg, session, frm, jid,
+					invite, tim])
+				return
+		self._on_message_decrypted((msgtxt, encrypted), mtype, msg, session, frm,
+			jid, invite, tim)
+
+	def _on_message_decrypted(self, output, mtype, msg, session, frm, jid,
+	invite, tim):
+		msgtxt, encrypted = output
 		if mtype == 'error':
 			self.dispatch_error_message(msg, msgtxt, session, frm, tim)
 		elif mtype == 'groupchat':
diff --git a/src/common/gajim.py b/src/common/gajim.py
index 89a72be325..3eac844951 100644
--- a/src/common/gajim.py
+++ b/src/common/gajim.py
@@ -63,6 +63,7 @@ If you start gajim from svn:
 	sys.exit(1)
 
 interface = None # The actual interface (the gtk one for the moment)
+thread_interface = None # Interface to run a thread and then a callback
 config = config.Config()
 version = config.get('version')
 connections = {} # 'account name': 'account (connection.Connection) instance'
diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py
index b683d38882..fe126d4c22 100644
--- a/src/common/xmpp/dispatcher_nb.py
+++ b/src/common/xmpp/dispatcher_nb.py
@@ -134,7 +134,10 @@ class XMPPDispatcher(PlugIn):
 		self._owner.lastErrNode = None
 		self._owner.lastErr = None
 		self._owner.lastErrCode = None
-		self.StreamInit()
+		if hasattr(self._owner, 'StreamInit'):
+			self._owner.StreamInit()
+		else:
+			self.StreamInit()
 
 	def plugout(self):
 		''' Prepares instance to be destructed. '''
diff --git a/src/common/zeroconf/client_zeroconf.py b/src/common/zeroconf/client_zeroconf.py
index 77f72e75e8..a643454fac 100644
--- a/src/common/zeroconf/client_zeroconf.py
+++ b/src/common/zeroconf/client_zeroconf.py
@@ -155,11 +155,16 @@ class P2PClient(IdleObject):
 					if on_not_ok:
 						on_not_ok('Connection to host could not be established.')
 					return
-				id = stanza.getThread()
+				thread_id = stanza.getThread()
+				id_ = stanza.getID()
+				if not id_:
+					id_ = self.Dispatcher.getAnID()
 				if self.conn_holder.ids_of_awaiting_messages.has_key(self.fd):
-					self.conn_holder.ids_of_awaiting_messages[self.fd].append(id)
+					self.conn_holder.ids_of_awaiting_messages[self.fd].append((id_,
+						thread_id))
 				else:
-					self.conn_holder.ids_of_awaiting_messages[self.fd] = [id]
+					self.conn_holder.ids_of_awaiting_messages[self.fd] = [(id_,
+						thread_id)]
 
 	def add_stanza(self, stanza, is_message=False):
 		if self.Connection:
@@ -170,24 +175,32 @@ class P2PClient(IdleObject):
 			self.stanzaqueue.append((stanza, is_message))
 
 		if is_message:
-			id = stanza.getThread()
+			id_ = stanza.getID()
+			if not id_:
+				id_ = self.Dispatcher.getAnID()
 			if self.conn_holder.ids_of_awaiting_messages.has_key(self.fd):
-				self.conn_holder.ids_of_awaiting_messages[self.fd].append(id)
+				self.conn_holder.ids_of_awaiting_messages[self.fd].append((id_,
+					thread_id))
 			else:
-				self.conn_holder.ids_of_awaiting_messages[self.fd] = [id]
+				self.conn_holder.ids_of_awaiting_messages[self.fd] = [(id_,
+					thread_id)]
 
 		return True
 
 	def on_message_sent(self, connection_id):
-		self.conn_holder.ids_of_awaiting_messages[connection_id].pop(0)
+		id_, thread_id = \
+			self.conn_holder.ids_of_awaiting_messages[connection_id].pop(0)
+		if self.on_ok:
+			self.on_ok(id_)
+			# use on_ok only on first message. For others it's called in
+			# ClientZeroconf
+			self.on_ok = None
 
 	def on_connect(self, conn):
 		self.Connection = conn
 		self.Connection.PlugIn(self)
 		dispatcher_nb.Dispatcher().PlugIn(self)
 		self._register_handlers()
-		if self.on_ok:
-			self.on_ok()
 
 	def StreamInit(self):
 		''' Send an initial stream header. '''
@@ -393,8 +406,9 @@ class P2PConnection(IdleObject, PlugIn):
 	def read_timeout(self):
 		ids = self.client.conn_holder.ids_of_awaiting_messages
 		if self.fd in ids and len(ids[self.fd]) > 0:
-			for id in ids[self.fd]:
-				self._owner.Dispatcher.Event('', DATA_ERROR, (self.client.to, id))
+			for (id_, thread_id) in ids[self.fd]:
+				self._owner.Dispatcher.Event('', DATA_ERROR, (self.client.to,
+					thread_id))
 			ids[self.fd] = []
 		self.pollend()
 
@@ -679,8 +693,7 @@ class ClientZeroconf:
 			stanza.setID(id_)
 			if conn.add_stanza(stanza, is_message):
 				if on_ok:
-					on_ok()
-				return id_
+					on_ok(id_)
 
 		if item['address'] in self.ip_to_hash:
 			hash_ = self.ip_to_hash[item['address']]
@@ -690,14 +703,11 @@ class ClientZeroconf:
 				stanza.setID(id_)
 				if conn.add_stanza(stanza, is_message):
 					if on_ok:
-						on_ok()
-					return id_
+						on_ok(id_)
 
 		# otherwise open new connection
 		stanza.setID('zero')
 		P2PClient(None, item['address'], item['port'], self,
 			[(stanza, is_message)], to, on_ok=on_ok, on_not_ok=on_not_ok)
 
-		return 'zero'
-
 # vim: se ts=3:
diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py
index ceb3226171..b7728c45db 100644
--- a/src/common/zeroconf/connection_zeroconf.py
+++ b/src/common/zeroconf/connection_zeroconf.py
@@ -364,7 +364,7 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf):
 	def send_message(self, jid, msg, keyID, type_='chat', subject='',
 	chatstate=None, msg_id=None, composing_xep=None, resource=None,
 	user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None,
-	original_message=None, delayed=None):
+	original_message=None, delayed=None, callback=None, callback_args=[]):
 		fjid = jid
 
 		if msg and not xhtml and gajim.config.get(
@@ -402,7 +402,7 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf):
 				# Encryption failed, do not send message
 				tim = time.localtime()
 				self.dispatch('MSGNOTSENT', (jid, error, msgtxt, tim, session))
-				return 3
+				return
 
 		if type_ == 'chat':
 			msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ=type_,
@@ -458,7 +458,7 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf):
 			if session.enable_encryption:
 				msg_iq = session.encrypt_stanza(msg_iq)
 
-		def on_send_ok():
+		def on_send_ok(id):
 			no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')
 			ji = gajim.get_jid_without_resource(jid)
 			if session.is_loggable() and self.name not in no_log_for and\
@@ -473,9 +473,12 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf):
 					else:
 						kind = 'single_msg_sent'
 					gajim.logger.write(kind, jid, log_msg)
-			
+
 			self.dispatch('MSGSENT', (jid, msg, keyID))
 
+			if callback:
+				callback(id, *callback_args)
+
 		def on_send_not_ok(reason):
 			reason += ' ' + _('Your message could not be sent.')
 			self.dispatch('MSGERROR', [jid, -1, reason, None, None, session])
@@ -484,8 +487,6 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf):
 		if ret == -1:
 			# Contact Offline
 			self.dispatch('MSGERROR', [jid, -1, _('Contact is offline. Your message could not be sent.'), None, None, session])
-		return ret
-		return ret
 
 
 	def send_stanza(self, stanza):
diff --git a/src/gajim.py b/src/gajim.py
index 740971177f..6ec553dc0e 100644
--- a/src/gajim.py
+++ b/src/gajim.py
@@ -423,6 +423,7 @@ parser = optparser.OptionsParser(config_filename)
 import roster_window
 import profile_window
 import config
+from threading import Thread
 
 
 class PassphraseRequest:
@@ -482,6 +483,22 @@ class PassphraseRequest:
 			cancel_handler=_cancel)
 		self.dialog_created = True
 
+
+class ThreadInterface: 
+		def __init__(self, func, func_args, callback, callback_args): 
+			'''Call a function in a thread 
+			
+			:param func: the function to call in the thread 
+			:param func_args: list or arguments for this function 
+			:param callback: callback to call once function is finished 
+			:param callback_args: list of arguments for this callback 
+			''' 
+			def thread_function(func, func_args, callback, callback_args): 
+				output = func(*func_args) 
+				gobject.idle_add(callback, output, *callback_args) 
+			Thread(target=thread_function, args=(func, func_args, callback, 
+				callback_args)).start()
+
 class Interface:
 
 ################################################################################
@@ -3071,6 +3088,7 @@ class Interface:
 
 	def __init__(self):
 		gajim.interface = self
+		gajim.thread_interface = ThreadInterface
 		# This is the manager and factory of message windows set by the module
 		self.msg_win_mgr = None
 		self.jabber_state_images = {'16': {}, '32': {}, 'opened': {},
diff --git a/src/message_control.py b/src/message_control.py
index b27a1e9a26..ab580a1aeb 100644
--- a/src/message_control.py
+++ b/src/message_control.py
@@ -158,9 +158,9 @@ class MessageControl:
 		if crypto_changed:
 			self.print_esession_details()
 
-	def send_message(self, message, keyID = '', type_ = 'chat',
-	chatstate = None, msg_id = None, composing_xep = None, resource = None,
-	user_nick = None, xhtml = None):
+	def send_message(self, message, keyID='', type_='chat', chatstate=None,
+	msg_id=None, composing_xep=None, resource=None, user_nick=None, xhtml=None,
+	callback=None, callback_args=[]):
 		# Send the given message to the active tab.
 		# Doesn't return None if error
 		jid = self.contact.jid
@@ -182,11 +182,10 @@ class MessageControl:
 			self.set_session(sess)
 
 		# Send and update history
-		return conn.send_message(jid, message, keyID, type_ = type_,
-			chatstate = chatstate, msg_id = msg_id,
-			composing_xep = composing_xep,
-			resource = self.resource, user_nick = user_nick,
-			session = self.session,
-			original_message = original_message, xhtml = xhtml)
+		conn.send_message(jid, message, keyID, type_=type_, chatstate=chatstate,
+			msg_id=msg_id, composing_xep=composing_xep, resource=self.resource,
+			user_nick=user_nick, session=self.session,
+			original_message=original_message, xhtml=xhtml, callback=callback,
+			callback_args=callback_args)
 
 # vim: se ts=3:
-- 
GitLab