From 5999b71c416f02dc11803bf52a406b9109ddc3c1 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sun, 27 Apr 2014 16:32:03 +0200 Subject: Fix #2106 (implement message delivery receipts) - two options request/ack_message_receipts - two new theme parameters : CHAR_ACK_RECEIVED and COLOR_CHAR_ACK - if a message has a receipt, the character is displayed between the timestamp and the nick, using the color --- CHANGELOG | 1 + data/default_config.cfg | 6 +++ doc/source/configuration.rst | 13 ++++++ doc/source/dev/xep.rst | 2 + src/connection.py | 7 ++++ src/core/core.py | 18 ++++++++ src/core/handlers.py | 18 ++++++++ src/tabs/basetabs.py | 9 ++++ src/tabs/conversationtab.py | 4 +- src/text_buffer.py | 97 ++++++++++++++++++++++++++++---------------- src/theming.py | 3 ++ src/windows.py | 39 +++++++++++++++--- 12 files changed, 176 insertions(+), 41 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 339346fd..9bcab2f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ For more detailed changelog, see the roadmap: http://dev.louiz.org/projects/poezio/roadmap * Poezio 0.8.3 - dev +- Implement XEP-0184 (message delivery receipts) - better setup scripts (use setuptools) - Better timezone handling - Better alias plugin, with permanent alias storage diff --git a/data/default_config.cfg b/data/default_config.cfg index f2e45e46..ef863ee4 100644 --- a/data/default_config.cfg +++ b/data/default_config.cfg @@ -370,6 +370,12 @@ display_tune_notifications = false # other resources with carbons enabled. enable_carbons = false +# Acknowledge message delivery receipts (XEP-0184) +ack_message_receipts = true + +# Ask for message delivery receipts (XEP-0184) +request_message_receipts = true + # Receive the tune notifications or not (in order to display informations # in the roster). # If this is set to false, then the display_tune_notifications diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 50b9fc71..cb2fbddb 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -364,6 +364,19 @@ to understand what is :ref:`carbons ` or XHTML and CSS formating. We can use this to make colored text for example. Set to ``true`` if you want to see colored (and otherwise formatted) messages. + request_message_receipts + + **Default value:** ``true`` + + Request message receipts when sending messages (except in groupchats). + + ack_message_receipts + + **Default value:** ``true`` + + Acknowledge message receipts requested by the other party. + + send_chat_states **Default value:** ``true`` diff --git a/doc/source/dev/xep.rst b/doc/source/dev/xep.rst index 7d303a03..268ee932 100644 --- a/doc/source/dev/xep.rst +++ b/doc/source/dev/xep.rst @@ -37,6 +37,8 @@ Table of all XEPs implemented in poezio. +----------+-------------------------+---------------------+ |0172 |User Nickname |100% | +----------+-------------------------+---------------------+ +|0184 |Message Delivery Receipts|100% | ++----------+-------------------------+---------------------+ |0191 |Simple Communication |95% | | |Blocking | | +----------+-------------------------+---------------------+ diff --git a/src/connection.py b/src/connection.py index da26f25b..0af2b228 100644 --- a/src/connection.py +++ b/src/connection.py @@ -84,6 +84,13 @@ class Connection(sleekxmpp.ClientXMPP): self.plugin['xep_0077'].create_account = False self.register_plugin('xep_0085') self.register_plugin('xep_0115') + + self.register_plugin('xep_0184') + self.plugin['xep_0184'].auto_ack = config.get('ack_message_receipts', + True) + self.plugin['xep_0184'].auto_request = config.get( + 'request_message_receipts', True) + self.register_plugin('xep_0191') self.register_plugin('xep_0199') self.set_keepalive_values() diff --git a/src/core/core.py b/src/core/core.py index 4bb0725b..95adc067 100644 --- a/src/core/core.py +++ b/src/core/core.py @@ -208,6 +208,7 @@ class Core(object): self.xmpp.add_event_handler("groupchat_subject", self.on_groupchat_subject) self.xmpp.add_event_handler("message", self.on_message) + self.xmpp.add_event_handler("receipt_received", self.on_receipt) self.xmpp.add_event_handler("got_online", self.on_got_online) self.xmpp.add_event_handler("got_offline", self.on_got_offline) self.xmpp.add_event_handler("roster_update", self.on_roster_update) @@ -277,6 +278,10 @@ class Core(object): self.configuration_change_handlers = {"": []} self.add_configuration_handler("create_gaps", self.on_gaps_config_change) + self.add_configuration_handler("request_message_receipts", + self.on_request_receipts_config_change) + self.add_configuration_handler("ack_message_receipts", + self.on_ack_receipts_config_change) self.add_configuration_handler("plugins_dir", self.on_plugins_dir_config_change) self.add_configuration_handler("plugins_conf_dir", @@ -331,6 +336,18 @@ class Core(object): if value.lower() == "false": self.tabs = list(tab for tab in self.tabs if tab) + def on_request_receipts_config_change(self, option, value): + """ + Called when the request_message_receipts option changes + """ + self.xmpp.plugin['xep_0184'].auto_request = config.get(option, True) + + def on_ack_receipts_config_change(self, option, value): + """ + Called when the ack_message_receipts option changes + """ + self.xmpp.plugin['xep_0184'].auto_ack = config.get(option, True) + def on_plugins_dir_config_change(self, option, value): """ Called when the plugins_dir option is changed @@ -1850,6 +1867,7 @@ class Core(object): on_status_codes = handlers.on_status_codes on_groupchat_subject = handlers.on_groupchat_subject on_data_form = handlers.on_data_form + on_receipt = handlers.on_receipt on_attention = handlers.on_attention room_error = handlers.room_error outgoing_stanza = handlers.outgoing_stanza diff --git a/src/core/handlers.py b/src/core/handlers.py index 7ce14c65..58217e8f 100644 --- a/src/core/handlers.py +++ b/src/core/handlers.py @@ -59,6 +59,8 @@ def on_carbon_received(self, message): else: return recv['to'] = self.xmpp.boundjid.full + if recv['receipt']: + return self.on_receipt(recv) self.on_normal_message(recv) def on_carbon_sent(self, message): @@ -955,6 +957,22 @@ def on_groupchat_subject(self, message): if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab(): self.refresh_window() +def on_receipt(self, message): + """ + When a delivery receipt is received (XEP-0184) + """ + jid = message['from'] + msg_id = message['receipt'] + if not msg_id: + return + + conversation = self.get_tab_by_name(jid) + conversation = conversation or self.get_tab_by_name(jid.bare) + if not conversation: + return + + conversation.ack_message(msg_id) + def on_data_form(self, message): """ When a data form is received diff --git a/src/tabs/basetabs.py b/src/tabs/basetabs.py index 1e7564dc..2811ba66 100644 --- a/src/tabs/basetabs.py +++ b/src/tabs/basetabs.py @@ -496,6 +496,15 @@ class ChatTab(Tab): identifier=identifier, jid=jid) + def ack_message(self, msg_id): + """ + Ack a message + """ + new_msg = self._text_buffer.ack_message(msg_id) + if new_msg: + self.text_win.modify_message(msg_id, new_msg) + self.core.refresh_window() + def modify_message(self, txt, old_id, new_id, user=None, jid=None, nickname=None): self.log_message(txt, nickname, typ=1) message = self._text_buffer.modify_message(txt, old_id, new_id, time=time, user=user, jid=jid) diff --git a/src/tabs/conversationtab.py b/src/tabs/conversationtab.py index ce60689c..51262db0 100644 --- a/src/tabs/conversationtab.py +++ b/src/tabs/conversationtab.py @@ -219,7 +219,9 @@ class ConversationTab(ChatTab): msg.send() def check_attention(self): - self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_dest_jid(), 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(): diff --git a/src/text_buffer.py b/src/text_buffer.py index ed213d2d..4a41fd97 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -18,7 +18,7 @@ from config import config from theming import get_theme, dump_tuple message_fields = ('txt nick_color time str_time nickname user identifier' - ' highlight me old_message revisions jid') + ' highlight me old_message revisions jid ack') Message = collections.namedtuple('Message', message_fields) class CorrectionError(Exception): @@ -84,7 +84,7 @@ class TextBuffer(object): @staticmethod def make_message(txt, time, nickname, nick_color, history, user, identifier, str_time=None, highlight=False, - old_message=None, revisions=0, jid=None): + old_message=None, revisions=0, jid=None, ack=None): """ Create a new Message object with parameters, check for /me messages, and delayed messages @@ -118,19 +118,20 @@ class TextBuffer(object): me=me, old_message=old_message, revisions=revisions, - jid=jid) + jid=jid, + ack=ack) log.debug('Set message %s with %s.', identifier, msg) return msg def add_message(self, txt, time=None, nickname=None, nick_color=None, history=None, user=None, highlight=False, - identifier=None, str_time=None, jid=None): + identifier=None, str_time=None, jid=None, ack=None): """ Create a message and add it to the text buffer """ msg = self.make_message(txt, time, nickname, nick_color, history, user, identifier, str_time=str_time, - highlight=highlight, jid=jid) + highlight=highlight, jid=jid, ack=ack) self.messages.append(msg) while len(self.messages) > self.messages_nb_limit: @@ -150,42 +151,68 @@ class TextBuffer(object): return ret_val or 1 + def _find_message(self, old_id): + """ + Find a message in the text buffer from its message id + """ + for i in range(len(self.messages) -1, -1, -1): + msg = self.messages[i] + if msg.identifier == old_id: + return i + return -1 + + def ack_message(self, old_id): + """ + Ack a message + """ + i = self._find_message(old_id) + if i == -1: + return + msg = self.messages[i] + new_msg = list(msg) + new_msg[12] = True + new_msg = Message(*new_msg) + self.messages[i] = new_msg + return new_msg + def modify_message(self, txt, old_id, new_id, highlight=False, time=None, user=None, jid=None): """ Correct a message in a text buffer. """ - for i in range(len(self.messages) -1, -1, -1): - msg = self.messages[i] - - if msg.identifier == old_id: - if msg.user and msg.user is not user: - raise CorrectionError("Different users") - elif len(msg.str_time) > 8: # ugly - raise CorrectionError("Delayed message") - elif not msg.user and (msg.jid is None or jid is None): - raise CorrectionError('Could not check the ' - 'identity of the sender') - elif not msg.user and msg.jid != jid: - raise CorrectionError('Messages %s and %s have not been ' - 'sent by the same fullJID' % - (old_id, new_id)) - - if not time: - time = msg.time - message = self.make_message(txt, time, msg.nickname, - msg.nick_color, None, msg.user, - new_id, highlight=highlight, - old_message=msg, - revisions=msg.revisions + 1, - jid=jid) - self.messages[i] = message - log.debug('Replacing message %s with %s.', old_id, new_id) - return message - log.debug('Message %s not found in text_buffer, abort replacement.', - old_id) - raise CorrectionError("nothing to replace") + i = self._find_message(old_id) + + if i == -1: + log.debug('Message %s not found in text_buffer, abort replacement.', + old_id) + raise CorrectionError("nothing to replace") + + msg = self.messages[i] + + if msg.user and msg.user is not user: + raise CorrectionError("Different users") + elif len(msg.str_time) > 8: # ugly + raise CorrectionError("Delayed message") + elif not msg.user and (msg.jid is None or jid is None): + raise CorrectionError('Could not check the ' + 'identity of the sender') + elif not msg.user and msg.jid != jid: + raise CorrectionError('Messages %s and %s have not been ' + 'sent by the same fullJID' % + (old_id, new_id)) + + if not time: + time = msg.time + message = self.make_message(txt, time, msg.nickname, + msg.nick_color, None, msg.user, + new_id, highlight=highlight, + old_message=msg, + revisions=msg.revisions + 1, + jid=jid) + self.messages[i] = message + log.debug('Replacing message %s with %s.', old_id, new_id) + return message def del_window(self, win): self.windows.remove(win) diff --git a/src/theming.py b/src/theming.py index 71ed760c..093fa553 100644 --- a/src/theming.py +++ b/src/theming.py @@ -301,6 +301,7 @@ class Theme(object): CHAR_QUIT = '<---' CHAR_KICK = '-!-' CHAR_NEW_TEXT_SEPARATOR = '- ' + CHAR_ACK_RECEIVED = '✔' CHAR_COLUMN_ASC = ' ▲' CHAR_COLUMN_DESC = ' ▼' CHAR_ROSTER_ERROR = '✖' @@ -314,6 +315,8 @@ class Theme(object): CHAR_ROSTER_TO = '→' CHAR_ROSTER_NONE = '⇹' + COLOR_CHAR_ACK = (2, -1) + COLOR_ROSTER_GAMING = (6, -1) COLOR_ROSTER_MOOD = (2, -1) COLOR_ROSTER_ACTIVITY = (3, -1) diff --git a/src/windows.py b/src/windows.py index 4dd0c242..fb901f19 100644 --- a/src/windows.py +++ b/src/windows.py @@ -914,6 +914,8 @@ class TextWin(Win): ret = [] nick = truncate_nick(message.nickname) offset = 0 + if message.ack: + offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1 if nick: offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length if message.revisions > 0: @@ -967,6 +969,8 @@ class TextWin(Win): color = None if with_timestamps: self.write_time(msg.str_time) + if msg.ack: + self.write_ack() if msg.me: self._win.attron(to_curses_attr(get_theme().COLOR_ME_MESSAGE)) self.addstr('* ') @@ -990,11 +994,29 @@ class TextWin(Win): if not line: self.write_line_separator(y) else: - self.write_text(y, - # Offset for the timestamp (if any) plus a space after it - (0 if not with_timestamps else (len(line.msg.str_time) + (1 if line.msg.str_time else 0) )) + - # Offset for the nickname (if any) plus a space and a > after it - (0 if not line.msg.nickname else (poopt.wcswidth(truncate_nick(line.msg.nickname)) + (3 if line.msg.me else 2) + ceil(log10(line.msg.revisions + 1)))), + offset = 0 + # Offset for the timestamp (if any) plus a space after it + if with_timestamps: + offset += len(line.msg.str_time) + if offset: + offset += 1 + + # Offset for the nickname (if any) + # plus a space and a > after it + if line.msg.nickname: + offset += poopt.wcswidth( + truncate_nick(line.msg.nickname)) + if line.msg.me: + offset += 3 + else: + offset += 2 + offset += ceil(log10(line.msg.revisions + 1)) + + if line.msg.ack: + offset += 1 + poopt.wcswidth( + get_theme().CHAR_ACK_RECEIVED) + + self.write_text(y, offset, line.prepend+line.msg.txt[line.start_pos:line.end_pos]) if y != self.height-1: self.addstr('\n') @@ -1014,6 +1036,13 @@ class TextWin(Win): """ self.addstr_colored(txt, y, x) + def write_ack(self): + color = get_theme().COLOR_CHAR_ACK + self._win.attron(to_curses_attr(color)) + self.addstr(get_theme().CHAR_ACK_RECEIVED) + self._win.attroff(to_curses_attr(color)) + self.addstr(' ') + def write_nickname(self, nickname, color, highlight=False): """ Write the nickname, using the user's color -- cgit v1.2.3