summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG1
-rw-r--r--data/default_config.cfg6
-rw-r--r--doc/source/configuration.rst13
-rw-r--r--doc/source/dev/xep.rst2
-rw-r--r--src/connection.py7
-rw-r--r--src/core/core.py18
-rw-r--r--src/core/handlers.py18
-rw-r--r--src/tabs/basetabs.py9
-rw-r--r--src/tabs/conversationtab.py4
-rw-r--r--src/text_buffer.py97
-rw-r--r--src/theming.py3
-rw-r--r--src/windows.py39
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 <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``
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