From dd2a6d1d6552476db671ad77d55d549122947954 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sat, 26 Jan 2013 05:22:12 +0100 Subject: Implement XEP 296 for locking resource in conversations. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a few specific behaviours: When manually opening a conversation with a bare jid, we open a normal conversation that follows the XEP (locked and unlocked accordingly). If the user manually opens a conversation with a fulljid (by selecting a specific resource in the roster, or by specifying a fulljid to the /message command), we open a special tab that doesn’t follow the XEP (it is always locked to the same resource, and cannot be unlocked). When a message is received, unless a special tab has been manually opened by the other with that specific resource, we always send the messages to a uniq normal tab, unlocking or locking it according to the XEP. This means that only one tab can be opened with a given contact, unless the user specifically chooses to open a special tab for a specific resource. fixes #2159 --- src/core.py | 34 ++++++++++------- src/tabs.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++++--------- src/theming.py | 1 + src/windows.py | 19 ++++++++-- 4 files changed, 134 insertions(+), 33 deletions(-) diff --git a/src/core.py b/src/core.py index 7b536faa..1bb3d071 100644 --- a/src/core.py +++ b/src/core.py @@ -654,15 +654,17 @@ class Core(object): and return it. Otherwise, we return None """ jid = safeJID(jid) - # We first check if we have a conversation opened with this precise resource - conversation = self.get_tab_by_name(jid.full, tabs.ConversationTab) + # We first check if we have a static conversation opened with this precise resource + conversation = self.get_tab_by_name(jid.full, tabs.StaticConversationTab) if not conversation: # If not, we search for a conversation with the bare jid - conversation = self.get_tab_by_name(jid.bare, tabs.ConversationTab) + conversation = self.get_tab_by_name(jid.bare, tabs.DynamicConversationTab) if not conversation: if create: - # We create the conversation with the full Jid if nothing was found - conversation = self.open_conversation_window(jid.full, False) + # We create a dynamic conversation with the bare Jid if + # nothing was found (and we lock it to the resource + # later) + conversation = self.open_conversation_window(jid.bare, False) else: conversation = None return conversation @@ -869,15 +871,14 @@ class Core(object): def open_conversation_window(self, jid, focus=True): """ - Open a new conversation tab and focus it if needed + Open a new conversation tab and focus it if needed. If a resource is + provided, we open a StaticConversationTab, else a + DynamicConversationTab """ - for tab in self.tabs: # if the room exists, focus it and return - if isinstance(tab, tabs.ConversationTab): - if tab.get_name() == jid: - self.command_win('%s' % tab.nb) - return tab - new_tab = tabs.ConversationTab(jid) - # insert it in the rooms + if safeJID(jid).resource: + new_tab = tabs.StaticConversationTab(jid) + else: + new_tab = tabs.DynamicConversationTab(jid) if not focus: new_tab.state = "private" self.add_tab(new_tab, focus) @@ -2523,6 +2524,8 @@ class Core(object): body = xhtml.get_body_from_message_stanza(message) if not body: return + if isinstance(conversation, tabs.DynamicConversationTab): + conversation.lock(jid.resource) if jid.bare in roster: remote_nick = roster[jid.bare].name or jid.user else: @@ -2692,6 +2695,8 @@ class Core(object): return False self.events.trigger('normal_chatstate', message, tab) tab.chatstate = state + if state == 'gone' and isinstance(tab, tabs.DynamicConversationTab): + tab.unlock() if tab == self.current_tab(): tab.refresh_info_header() self.doupdate() @@ -2802,6 +2807,9 @@ class Core(object): return jid = presence['from'] contact = roster[jid.bare] + tab = self.get_conversation_by_jid(jid, create=False) + if isinstance(tab, tabs.DynamicConversationTab): + tab.unlock() if contact is None: return self.events.trigger('normal_presence', presence, contact[jid.full]) diff --git a/src/tabs.py b/src/tabs.py index 2e39988d..fb47591b 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -525,7 +525,7 @@ class ChatTab(Tab): self.core.information('Could not send custom xhtml', 'Error') return - msg = self.core.xmpp.make_message(self.get_name()) + msg = self.core.xmpp.make_message(self.get_dest_jid()) msg['body'] = body msg.enable('html') msg['html']['body'] = arg @@ -536,6 +536,9 @@ class ChatTab(Tab): self.refresh() msg.send() + def get_dest_jid(self): + return self.get_name() + @refresh_wrapper.always def command_clear(self, args): """ @@ -551,7 +554,7 @@ class ChatTab(Tab): if not isinstance(self, MucTab) or self.joined: if state in ('active', 'inactive', 'gone') and self.inactive and not always_send: return - msg = self.core.xmpp.make_message(self.get_name()) + msg = self.core.xmpp.make_message(self.get_dest_jid()) msg['type'] = self.message_type msg['chat_state'] = state self.chat_state = state @@ -2883,6 +2886,7 @@ class RosterInfoTab(Tab): class ConversationTab(ChatTab): """ The tab containg a normal conversation (not from a MUC) + Must not be instantiated, use Static or Dynamic version only. """ plugin_commands = {} plugin_keys = {} @@ -2895,7 +2899,6 @@ class ConversationTab(ChatTab): self.text_win = windows.TextWin() self._text_buffer.add_window(self.text_win) self.upper_bar = windows.ConversationStatusMessageWin() - self.info_header = windows.ConversationInfoWin() self.input = windows.MessageInput() self.check_attention() # keys @@ -2938,7 +2941,7 @@ class ConversationTab(ChatTab): self.complete_commands(self.input) def command_say(self, line, attention=False, correct=False): - msg = self.core.xmpp.make_message(self.get_name()) + msg = self.core.xmpp.make_message(self.get_dest_jid()) msg['type'] = 'chat' msg['body'] = line # trigger the event BEFORE looking for colors. @@ -2966,7 +2969,7 @@ class ConversationTab(ChatTab): self.core.events.trigger('conversation_say_after', msg, self) self.last_sent_message = msg msg.send() - logger.log_message(safeJID(self.get_name()).bare, self.core.own_nick, line) + logger.log_message(safeJID(self.get_dest_jid()).bare, self.core.own_nick, line) self.cancel_paused_delay() self.text_win.refresh() self.input.refresh() @@ -3007,8 +3010,8 @@ class ConversationTab(ChatTab): @refresh_wrapper.conditional def command_info(self, arg): - contact = roster[self.get_name()] - jid = safeJID(self.get_name()) + contact = roster[self.get_dest_jid()] + jid = safeJID(self.get_dest_jid()) if jid.resource: resource = contact[jid.full] else: @@ -3021,13 +3024,13 @@ class ConversationTab(ChatTab): if message is not '': self.command_say(message, attention=True) else: - msg = self.core.xmpp.make_message(self.get_name()) + msg = self.core.xmpp.make_message(self.get_dest_jid()) msg['type'] = 'chat' msg['attention'] = True msg.send() def check_attention(self): - self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_name(), block=False, timeout=5, callback=self.on_attention_checked) + self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_dest_jid(), block=False, timeout=5, callback=self.on_attention_checked) def on_attention_checked(self, iq): if 'urn:xmpp:attention:0' in iq['disco_info'].get_features(): @@ -3076,14 +3079,14 @@ class ConversationTab(ChatTab): self.resize() log.debug(' TAB Refresh: %s',self.__class__.__name__) self.text_win.refresh() - self.upper_bar.refresh(self.get_name(), roster[self.get_name()]) - self.info_header.refresh(self.get_name(), roster[self.get_name()], self.text_win, self.chatstate, ConversationTab.additional_informations) + self.upper_bar.refresh(self.get_dest_jid(), roster[self.get_dest_jid()]) + self.info_header.refresh(self.get_dest_jid(), roster[self.get_dest_jid()], self.text_win, self.chatstate, ConversationTab.additional_informations) self.info_win.refresh() self.refresh_tab_win() self.input.refresh() def refresh_info_header(self): - self.info_header.refresh(self.get_name(), roster[self.get_name()], + self.info_header.refresh(self.get_dest_jid(), roster[self.get_dest_jid()], self.text_win, self.chatstate, ConversationTab.additional_informations) self.input.refresh() @@ -3108,8 +3111,8 @@ class ConversationTab(ChatTab): return False def on_lose_focus(self): - contact = roster[self.get_name()] - jid = safeJID(self.get_name()) + contact = roster[self.get_dest_jid()] + jid = safeJID(self.get_dest_jid()) if contact: if jid.resource: resource = contact[jid.full] @@ -3125,8 +3128,8 @@ class ConversationTab(ChatTab): self.send_chat_state('inactive') def on_gain_focus(self): - contact = roster[self.get_name()] - jid = safeJID(self.get_name()) + contact = roster[self.get_dest_jid()] + jid = safeJID(self.get_dest_jid()) if contact: if jid.resource: resource = contact[jid.full] @@ -3178,6 +3181,83 @@ class ConversationTab(ChatTab): res.append(contact.name) return res +class DynamicConversationTab(ConversationTab): + """ + A conversation tab associated with one bare JID that can be “locked” to + a full jid, and unlocked, as described in the XEP-0296. + Only one DynamicConversationTab can be opened for a given jid. + """ + def __init__(self, jid, resource=None): + self.locked_resource = None + self.name = safeJID(jid).bare + if resource: + self.lock(resource) + self.info_header = windows.DynamicConversationInfoWin() + ConversationTab.__init__(self, jid) + + def lock(self, resource): + """ + Lock the tab to the resource. + """ + assert(resource) + self.locked_resource = resource + + def unlock(self): + """ + Unlock the tab from a resource. It is now “associated” with the bare + jid. + """ + self.locked_resource = None + + def get_dest_jid(self): + """ + Returns the full jid (using the locked resource), or the bare jid if + the conversation is not locked. + """ + if self.locked_resource: + return "%s/%s" % (self.get_name(), self.locked_resource) + return self.get_name() + + def refresh(self): + """ + Different from the parent class only for the info_header object. + """ + if self.need_resize: + self.resize() + log.debug(' TAB Refresh: %s',self.__class__.__name__) + self.text_win.refresh() + self.upper_bar.refresh(self.get_name(), roster[self.get_name()]) + if self.locked_resource: + displayed_jid = "%s/%s" % (self.get_name(), self.locked_resource) + else: + displayed_jid = self.get_name() + self.info_header.refresh(displayed_jid, roster[self.get_name()], self.text_win, self.chatstate, ConversationTab.additional_informations) + self.info_win.refresh() + self.refresh_tab_win() + self.input.refresh() + + def refresh_info_header(self): + """ + Different from the parent class only for the info_header object. + """ + if self.locked_resource: + displayed_jid = "%s/%s" % (self.get_name(), self.locked_resource) + else: + displayed_jid = self.get_name() + self.info_header.refresh(displayed_jid, roster[self.get_name()], + self.text_win, self.chatstate, ConversationTab.additional_informations) + self.input.refresh() + +class StaticConversationTab(ConversationTab): + """ + A conversation tab associated with one Full JID. It cannot be locked to + an different resource or unlocked. + """ + def __init__(self, jid): + assert(safeJID(jid).resource) + self.info_header = windows.ConversationInfoWin() + ConversationTab.__init__(self, jid) + class MucListTab(Tab): """ A tab listing rooms from a specific server, displaying various information, @@ -3528,7 +3608,6 @@ class XMLTab(Tab): self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0) self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0) - class SimpleTextTab(Tab): """ A very simple tab, with just a text displaying some diff --git a/src/theming.py b/src/theming.py index 07a37c6e..dd933fc7 100644 --- a/src/theming.py +++ b/src/theming.py @@ -184,6 +184,7 @@ class Theme(object): COLOR_SELECTED_ROW = (-1, 33) COLOR_PRIVATE_NAME = (-1, 4) COLOR_CONVERSATION_NAME = (2, 4) + COLOR_CONVERSATION_RESOURCE = (121, 4) COLOR_GROUPCHAT_NAME = (7, 4) COLOR_COLUMN_HEADER = (36, 4) COLOR_COLUMN_HEADER_SEL = (4, 36) diff --git a/src/windows.py b/src/windows.py index f0911086..e507be60 100644 --- a/src/windows.py +++ b/src/windows.py @@ -503,7 +503,7 @@ class ConversationInfoWin(InfoWin): else: resource = None # if contact is None, then resource is None too: user is not in the roster - # so we don't know almost anything about it + # so we know almost nothing about it # If contact is a Contact, then # resource can now be a Resource: user is in the roster and online # or resource is None: user is in the roster but offline @@ -546,8 +546,9 @@ class ConversationInfoWin(InfoWin): if not contact: self.addstr("(contact not in roster)", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) return - display_name = contact.name or contact.bare_jid - self.addstr('%s '%(display_name), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + display_name = contact.name + if display_name: + self.addstr('%s '%(display_name), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) def write_contact_jid(self, jid): """ @@ -561,6 +562,18 @@ class ConversationInfoWin(InfoWin): if state: self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) +class DynamicConversationInfoWin(ConversationInfoWin): + def write_contact_jid(self, jid): + """ + Just displays the resource in an other color + """ + log.debug("write_contact_jid DynamicConversationInfoWin, jid: %s" % jid.resource) + self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + self.addstr(jid.bare, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME)) + if jid.resource: + self.addstr("/%s" % (jid.resource,), to_curses_attr(get_theme().COLOR_CONVERSATION_RESOURCE)) + self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) + class ConversationStatusMessageWin(InfoWin): """ The upper bar displaying the status message of the contact -- cgit v1.2.3