From f7874d29c714247ab66e1c786256b90b7487ee2e Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Fri, 23 May 2008 23:27:08 +0000 Subject: [PATCH] reuse chat controls after session termination fixes #3950 --- src/chat_control.py | 8 +- src/common/connection.py | 2 + src/common/connection_handlers.py | 35 ++++---- src/common/stanza_session.py | 3 + src/gajim.py | 22 +---- src/message_control.py | 23 +++-- src/message_window.py | 135 ++++++++++++++++++++++++------ src/roster_window.py | 6 +- src/session.py | 28 ++++++- 9 files changed, 186 insertions(+), 76 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 5984298d1..cddf9cd35 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1206,6 +1206,9 @@ class ChatControl(ChatControlBase): ChatControlBase.update_ui(self) def get_otr_status(self): + if not self.session: + return 0 + ctx = gajim.otr_module.otrl_context_find( self.session.conn.otr_userstates, self.contact.get_full_jid().encode(), @@ -1613,7 +1616,7 @@ class ChatControl(ChatControlBase): def print_esession_details(self): '''print esession settings to textview''' - e2e_is_active = self.session and self.session.enable_encryption + e2e_is_active = bool(self.session) and self.session.enable_encryption if e2e_is_active: msg = _('E2E encryption enabled') ChatControlBase.print_conversation_line(self, msg, 'status', '', None) @@ -2022,7 +2025,8 @@ class ChatControl(ChatControlBase): self.contact.our_chatstate = None # disconnect self from session - self.session.control = None + if self.session: + self.session.control = None # Disconnect timer callbacks gobject.source_remove(self.possible_paused_timeout_id) diff --git a/src/common/connection.py b/src/common/connection.py index 54984ab00..e9ffba607 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -980,6 +980,8 @@ class Connection(ConnectionHandlers): elif show == 'offline': self.connected = 0 if self.connection: + self.terminate_sessions() + self.on_purpose = True p = common.xmpp.Presence(typ = 'unavailable') p = self.add_sha(p, False) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index f0b671160..e6b7bdc84 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1282,13 +1282,13 @@ class ConnectionHandlersBase: self.sessions = {} def delete_session(self, jid, thread_id): - try: - del self.sessions[jid][thread_id] + if not jid in self.sessions: + jid = gajim.get_jid_without_resource(jid) - if not self.sessions[jid]: - del self.sessions[jid] - except KeyError: - pass + del self.sessions[jid][thread_id] + + if not self.sessions[jid]: + del self.sessions[jid] def find_null_session(self, jid): '''finds all of the sessions between us and a remote jid in which we @@ -1644,17 +1644,17 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, mtype = 'normal' msgtxt = msg.getBody() - subject = msg.getSubject() # if not there, it's None jid = helpers.get_jid_from_iq(msg) encrypted = False + xep_200_encrypted = msg.getTag('c', namespace=common.xmpp.NS_STANZA_CRYPTO) # I don't trust libotr, that's why I only pass the # message to it if it either contains the magic # ?OTR string or a plaintext tagged message. - if gajim.otr_module and \ - isinstance(msgtxt, unicode) and \ + if gajim.otr_module and not xep_200_encrypted \ + and isinstance(msgtxt, unicode) and \ ('\x20\x09\x20\x20\x09\x09\x09\x09\x20\x09\x20\x09\x20\x09\x20\x20' \ in msgtxt or '?OTR' in msgtxt): # If it doesn't include ?OTR, it wasn't an @@ -1764,11 +1764,12 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, tim = helpers.datetime_tuple(tim) tim = localtime(timegm(tim)) - if msg.getTag('c', namespace=common.xmpp.NS_STANZA_CRYPTO): + if xep_200_encrypted: encrypted = True try: msg = session.decrypt_stanza(msg) + msgtxt = msg.getBody() except: self.dispatch('FAILED_DECRYPT', (frm, tim, session)) @@ -1791,26 +1792,28 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, msgtxt = decmsg.replace('\x00', '') encrypted = True if mtype == 'error': - self.dispatch_error_message(msg, msgtxt, session, frm, tim, subject) + self.dispatch_error_message(msg, msgtxt, session, frm, tim) elif mtype == 'groupchat': - self.dispatch_gc_message(msg, subject, frm, msgtxt, jid, tim) + self.dispatch_gc_message(msg, frm, msgtxt, jid, tim) elif invite is not None: self.dispatch_invite_message(invite, frm) else: if isinstance(session, ChatControlSession): - session.received(frm, msgtxt, tim, encrypted, subject, msg) + session.received(frm, msgtxt, tim, encrypted, msg) else: session.received(msg) # END messageCB # process and dispatch an error message - def dispatch_error_message(self, msg, msgtxt, session, frm, tim, subject): + def dispatch_error_message(self, msg, msgtxt, session, frm, tim): error_msg = msg.getErrorMsg() if not error_msg: error_msg = msgtxt msgtxt = None + subject = msg.getSubject() + if session.is_loggable(): try: gajim.logger.write('error', frm, error_msg, tim=tim, @@ -1821,9 +1824,11 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, tim, session)) # process and dispatch a groupchat message - def dispatch_gc_message(self, msg, subject, frm, msgtxt, jid, tim): + def dispatch_gc_message(self, msg, frm, msgtxt, jid, tim): has_timestamp = bool(msg.timestamp) + subject = msg.getSubject() + if subject is not None: self.dispatch('GC_SUBJECT', (frm, subject, msgtxt, has_timestamp)) return diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index cd734a595..f611a1f93 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -243,6 +243,9 @@ class EncryptedStanzaSession(StanzaSession): return stanza + def is_xep_200_encrypted(self, msg): + msg.getTag('c', namespace=common.xmpp.NS_STANZA_CRYPTO) + def hmac(self, key, content): return HMAC.new(key, content, self.hash_alg).digest() diff --git a/src/gajim.py b/src/gajim.py index 615becc46..98bc71f3a 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -2038,30 +2038,12 @@ class Interface: if form.getField('terminate') and\ form.getField('terminate').getValue() in ('1', 'true'): - was_encrypted = session.enable_encryption - ctrl = session.control jid = str(jid) session.acknowledge_termination() - gajim.connections[account].delete_session(jid, session.thread_id) - if ctrl: - # replace the old session in this control with a new one - new_sess = gajim.connections[account].make_new_session(jid) - win = ctrl.parent_win - - ctrl.set_session(new_sess) - gajim.connections[account].delete_session(jid, - session.thread_id) - - if not jid in win._controls[account]: - jid = gajim.get_jid_without_resource(jid) - - del win._controls[account][jid][session.thread_id] - win._controls[account][jid][new_sess.thread_id] = ctrl - - if was_encrypted: - ctrl.print_esession_details() + conn = gajim.connections[account] + conn.delete_session(jid, session.thread_id) return diff --git a/src/message_control.py b/src/message_control.py index 032d8c0c5..ebff23368 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -128,16 +128,18 @@ class MessageControl: self.session = session + new_key = None if session: session.control = self + new_key = session.thread_id - if oldsession: - self.parent_win.change_thread_key( - self.contact.jid, self.account, - oldsession.thread_id, session.thread_id) + if oldsession: + self.parent_win.change_thread_key( + self.contact.jid, self.account, + oldsession.thread_id, new_key) - if oldsession.enable_encryption: - self.print_esession_details() + if oldsession.enable_encryption: + self.print_esession_details() def send_message(self, message, keyID = '', type = 'chat', chatstate = None, msg_id = None, composing_xep = None, resource = None, @@ -147,7 +149,14 @@ class MessageControl: jid = self.contact.jid original_message = message - if gajim.otr_module and jid not in gajim.otr_dont_append_tag: + if not self.session: + sess = gajim.connections[self.account].make_new_session(jid) + self.set_session(sess) + self.parent_win.move_from_sessionless(self) + + xep_200 = bool(self.session) and self.session.enable_encryption + + if gajim.otr_module and not xep_200 and (jid not in gajim.otr_dont_append_tag): if type == 'chat' and isinstance(message, unicode): d = {'kwargs': {'keyID': keyID, 'type': type, 'chatstate': chatstate, diff --git a/src/message_window.py b/src/message_window.py index 1114bffc2..24aa59896 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -54,8 +54,15 @@ class MessageWindow(object): ) = range(5) def __init__(self, acct, type, parent_window=None, parent_paned=None): - # A dictionary of dictionaries where _contacts[account][jid] == A MessageControl + # A dictionary of dictionaries of dictionaries + # where _contacts[account][jid][thread_id] == A MessageControl self._controls = {} + + # a dictionary of dictionaries where + # sessionless_ctrls[account][jid] = a list of MessageControls that don't have + # sessions attached + self.sessionless_ctrls = {} + # If None, the window is not tied to any specific account self.account = acct # If None, the window is not tied to any specific type @@ -146,6 +153,11 @@ class MessageWindow(object): if self._controls.has_key(old_name): self._controls[new_name] = self._controls[old_name] del self._controls[old_name] + + if self.sessionless_ctrls.has_key(old_name): + self.sessionless_ctrls[new_name] = self.sessionless_ctrls[old_name] + del self.sessionless_ctrls[old_name] + for ctrl in self.controls(): if ctrl.account == old_name: ctrl.account = new_name @@ -157,6 +169,11 @@ class MessageWindow(object): for jid_dict in self._controls.values(): for dict in jid_dict.values(): n += len(dict) + + for jid_dict in self.sessionless_ctrls.values(): + for ctrls in jid_dict.values(): + n += len(ctrls) + return n def resize(self, width, height): @@ -197,6 +214,7 @@ class MessageWindow(object): for ctrl in self.controls(): ctrl.shutdown() self._controls.clear() + self.sessionless_ctrls.clear() # Clean up handlers connected to the parent window, this is important since # self.window may be the RosterWindow for i in self.handlers.keys(): @@ -206,14 +224,24 @@ class MessageWindow(object): del self.handlers def new_tab(self, control): - if not self._controls.has_key(control.account): - self._controls[control.account] = {} fjid = control.get_full_jid() - if not self._controls[control.account].has_key(fjid): - self._controls[control.account][fjid] = {} + if control.session: + if not self._controls.has_key(control.account): + self._controls[control.account] = {} - self._controls[control.account][fjid][control.session.thread_id] = control + if not self._controls[control.account].has_key(fjid): + self._controls[control.account][fjid] = {} + + self._controls[control.account][fjid][control.session.thread_id] = control + else: + if not self.sessionless_ctrls.has_key(control.account): + self.sessionless_ctrls[control.account] = {} + + if not self.sessionless_ctrls[control.account].has_key(fjid): + self.sessionless_ctrls[control.account][fjid] = [] + + self.sessionless_ctrls[control.account][fjid].append(control) if self.get_num_controls() == 2: # is first conversation_textview scrolled down ? @@ -443,7 +471,6 @@ class MessageWindow(object): fjid = ctrl.get_full_jid() jid = gajim.get_jid_without_resource(fjid) - thread_id = ctrl.session.thread_id fctrls = self.get_controls(fjid, ctrl.account) bctrls = self.get_controls(jid, ctrl.account) @@ -458,13 +485,20 @@ class MessageWindow(object): self.notebook.remove_page(self.notebook.page_num(ctrl.widget)) - del self._controls[ctrl.account][fjid][thread_id] + if ctrl.session: + dict = self._controls + idx = ctrl.session.thread_id + else: + dict = self.sessionless_ctrls + idx = dict[ctrl.account][fjid].index(ctrl) - if len(self._controls[ctrl.account][fjid]) == 0: - del self._controls[ctrl.account][fjid] + del dict[ctrl.account][fjid][idx] - if len(self._controls[ctrl.account]) == 0: - del self._controls[ctrl.account] + if len(dict[ctrl.account][fjid]) == 0: + del dict[ctrl.account][fjid] + + if len(dict[ctrl.account]) == 0: + del dict[ctrl.account] self.check_tabs() self.show_title() @@ -591,7 +625,16 @@ class MessageWindow(object): def get_controls(self, jid, acct): try: - return self._controls[acct][jid].values() + sessioned = self._controls[acct][jid].values() + except KeyError: + sessioned = [] + + sessionless = self.sessionless_controls(acct, jid) + return sessioned + sessionless + + def sessionless_controls(self, acct, jid): + try: + return self.sessionless_ctrls[acct][jid] except KeyError: return [] @@ -604,27 +647,70 @@ class MessageWindow(object): return self._controls[acct][new_jid] = ctrls del self._controls[acct][old_jid] + + try: + ctrls = self.sessionless_ctrls[acct][old_jid] + except KeyError: + return + + self.sessionless_ctrls[acct][new_jid] = ctrls + del self.sessionless_ctrls[acct][new_jid] + if old_jid in gajim.last_message_time[acct]: gajim.last_message_time[acct][new_jid] = \ gajim.last_message_time[acct][old_jid] del gajim.last_message_time[acct][old_jid] - def change_thread_key(self, jid, acct, old_thread_id, new_thread_id): - '''Change the thread_id key of a control''' - try: - # Check if control exists - ctrl = self._controls[acct][jid][old_thread_id] - except KeyError: - return + def change_thread_key(self, jid, acct, old_thread_id, new_thread_id): + '''Change the thread_id key of a control''' + + if jid in self._controls[acct]: + ctrl = self._controls[acct][jid][old_thread_id] + else: + jid = gajim.get_jid_without_resource(jid) + ctrl = self._controls[acct][jid][old_thread_id] - self._controls[acct][jid][new_thread_id] = ctrl del self._controls[acct][jid][old_thread_id] + if new_thread_id: + self._controls[acct][jid][new_thread_id] = ctrl + else: + if acct not in self.sessionless_ctrls: + self.sessionless_ctrls[acct] = {} + + if jid not in self.sessionless_ctrls[acct]: + self.sessionless_ctrls[acct][jid] = [] + + self.sessionless_ctrls[acct][jid].append(ctrl) + + def move_from_sessionless(self, ctrl): + '''a control just got a session, move it to the proper holding cell''' + acct = ctrl.account + jid = ctrl.get_full_jid() + + idx = self.sessionless_ctrls[acct][jid].index(ctrl) + + del self.sessionless_ctrls[acct][jid][idx] + + if not self._controls.has_key(acct): + self._controls[acct] = {} + + if not self.sessionless_ctrls[acct].has_key(jid): + self._controls[acct][jid] = {} + + thread_id = ctrl.session.thread_id + + self._controls[acct][jid][thread_id] = ctrl + def controls(self): for jid_dict in self._controls.values(): for ctrl_dict in jid_dict.values(): for ctrl in ctrl_dict.values(): yield ctrl + for jid_dict in self.sessionless_ctrls.values(): + for ctrl_dict in jid_dict.values(): + for ctrl in ctrl_dict: + yield ctrl def move_to_next_unread_tab(self, forward): ind = self.notebook.get_current_page() @@ -834,11 +920,10 @@ class MessageWindowMgr(gobject.GObject): def get_window(self, jid, acct): for win in self.windows(): - try: - if win._controls[acct][jid]: + if (acct in win._controls and jid in win._controls[acct]) or \ + (acct in win.sessionless_ctrls and jid in win.sessionless_ctrls[acct]): return win - except KeyError: - pass + return None def get_gc_control(self, jid, acct): diff --git a/src/roster_window.py b/src/roster_window.py index c645c1ca4..70026cd99 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1830,7 +1830,7 @@ class RosterWindow: if was_invisible and status != 'offline': # We come back from invisible, join bookmarks gajim.interface.auto_join_bookmarks(account) - + def chg_contact_status(self, contact, show, status, account): '''When a contact changes his or her status''' contact_instances = gajim.contacts.get_contacts(account, contact.jid) @@ -2100,8 +2100,6 @@ class RosterWindow: self.quit_on_next_offline = 0 for acct in accounts: - gajim.connections[acct].terminate_sessions() - if gajim.connections[acct].connected: self.quit_on_next_offline += 1 self.send_status(acct, 'offline', message) @@ -3214,7 +3212,7 @@ class RosterWindow: import tictactoe sess = gajim.connections[account].make_new_session(jid, - klass=tictactoe.TicTacToeSession) + cls=tictactoe.TicTacToeSession) sess.begin() def on_execute_command(self, widget, contact, account, resource=None): diff --git a/src/session.py b/src/session.py index 9049a5902..c9cdde654 100644 --- a/src/session.py +++ b/src/session.py @@ -20,11 +20,19 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): self.control = None def acknowledge_termination(self): - # the other party terminated the session. we'll keep the control around, though. stanza_session.EncryptedStanzaSession.acknowledge_termination(self) if self.control: - self.control.session = None + if self.enable_encryption: + self.control.print_esession_details() + + self.control.set_session(None) + + def terminate(self): + stanza_session.EncryptedStanzaSession.terminate(self) + + if self.control: + self.control.set_session(None) # extracts chatstate from a stanza def get_chatstate(self, msg, msgtxt): @@ -53,8 +61,9 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): return (composing_xep, chatstate) # dispatch a received stanza - def received(self, full_jid_with_resource, msgtxt, tim, encrypted, subject, msg): + def received(self, full_jid_with_resource, msgtxt, tim, encrypted, msg): msg_type = msg.getType() + subject = msg.getSubject() if not msg_type: msg_type = 'normal' @@ -246,6 +255,19 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): contact = gajim.interface.roster.add_to_not_in_the_roster( self.conn.name, jid, user_nick) + if not self.control: + # look for an existing chat control without a session + mw = gajim.interface.msg_win_mgr.get_window(jid, self.conn.name) + + if mw: + ctrls = mw.sessionless_controls(self.conn.name, jid) + + if len(ctrls): + ctrl = ctrls[0] + self.control = ctrl + ctrl.set_session(self) + ctrl.parent_win.move_from_sessionless(ctrl) + if not self.control: # if no control exists and message comes from highest prio, the new # control shouldn't have a resource