Commit 5999b71c authored by mathieui's avatar mathieui

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
parent 60224bb7
......@@ -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
......
......@@ -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
......
......@@ -364,6 +364,19 @@ to understand what is :ref:`carbons <carbons-details>` 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``
......
......@@ -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 | |
+----------+-------------------------+---------------------+
......
......@@ -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()
......
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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():
......
......@@ -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)
......
......@@ -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)
......
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment