core: Implement echo-message, disabled by default
authorJanne Koschinski <janne@kuschku.de>
Mon, 20 Jul 2020 23:45:42 +0000 (19:45 -0400)
committerManuel Nickschas <sputnick@quassel-irc.org>
Sat, 28 Nov 2020 12:42:31 +0000 (13:42 +0100)
Implement the IRCv3 "echo-message" capability, allowing Quassel to
show how the network displayed sent messages, including any added
latency.

As this introduces additional latency, don't request this by default.
It can be manually activated with "/CAP REQ echo-message", e.g. in the
list of commands to perform on connect.  There may be other issues as
well, such as with CTCP handling.  Here be danger!

This will serve as the building block for communicating when a message
is pending being sent, vs. being delivered.  This requires support for
IRCv3 "batch" and "labeled-response", alongside protocol changes to
allow updating the status of existing Message objects.

Also add missing sources of setting the "::Self" flag on messages,
ensuring that echoed messages will be properly handled, e.g. with CTCP
events.

[Original commit by justJanne, made opt-in by digitalcircuit]

See https://ircv3.net/specs/extensions/echo-message-3.2.html
And https://ircv3.net/specs/extensions/labeled-response

[Addendum]
If "echo-message" was to be made enabled by default as-is, the
Features tab in the Network settings page can offer a way to opt out.

Here's how it might look:
Checkbox with widget name of "enableCapEchoMessage":
[x] Show outgoing messages after delivery

Tooltip:
<p>Wait until the IRC network has received the message to show it in Quassel, and show any changes the network has made to the message (e.g. removing formatting).</p>
<p>It may take longer for outgoing messages to appear.</p>
<p><i>Toggles the IRCv3 'echo-message' capability.<i></p>

"echo-message" should not be enabled by default yet, I just spent a
fair bit of time thinking on how to phrase this and wanted to preserve
it in the unlikely case it is useful in the future.

And here's the reasoning to go with this now-theoretical commit:
Reasoning:
This will unavoidably add latency to showing messages you've sent, and
if a network incorrectly echoes messages back, this could result in
losing message history.  Some may prefer the lower-latency of having
Quassel locally show messages, and we're a fair distance away from
implementing message labels in order to provide a "sending"/"sent"
message feedback (or any form of message editing at all) - messages
are assumed to be immutable once received.

src/common/irccap.h
src/core/coreuserinputhandler.cpp
src/core/ctcpparser.cpp
src/core/eventstringifier.cpp

index 9fb40e1..3d1e793 100644 (file)
@@ -79,6 +79,13 @@ namespace IrcCap {
      */
     const QString CHGHOST = "chghost";
 
+    /**
+     * Server sending own messages back.
+     *
+     * https://ircv3.net/specs/extensions/echo-message-3.2.html
+     */
+    const QString ECHO_MESSAGE = "echo-message";
+
     /**
      * Extended join information.
      *
@@ -173,6 +180,7 @@ namespace IrcCap {
                                               AWAY_NOTIFY,
                                               CAP_NOTIFY,
                                               CHGHOST,
+                                              //ECHO_MESSAGE, // Postponed for message pending UI with batch + labeled-response
                                               EXTENDED_JOIN,
                                               INVITE_NOTIFY,
                                               MESSAGE_TAGS,
index 8ab8dce..b64eb73 100644 (file)
@@ -183,14 +183,16 @@ void CoreUserInputHandler::handleCtcp(const BufferInfo& bufferInfo, const QStrin
 
     // FIXME make this a proper event
     coreNetwork()->coreSession()->ctcpParser()->query(coreNetwork(), nick, ctcpTag, message);
-    emit displayMsg(NetworkInternalMessage(
-        Message::Action,
-        BufferInfo::StatusBuffer,
-        "",
-        verboseMessage,
-        network()->myNick(),
-        Message::Flag::Self
-    ));
+    if (!network()->capEnabled(IrcCap::ECHO_MESSAGE)) {
+        emit displayMsg(NetworkInternalMessage(
+            Message::Action,
+            BufferInfo::StatusBuffer,
+            "",
+            verboseMessage,
+            network()->myNick(),
+            Message::Flag::Self
+        ));
+    }
 }
 
 void CoreUserInputHandler::handleDelkey(const BufferInfo& bufferInfo, const QString& msg)
@@ -510,14 +512,16 @@ void CoreUserInputHandler::handleMe(const BufferInfo& bufferInfo, const QString&
     for (const auto& message : messages) {
         // Handle each separated message independently
         coreNetwork()->coreSession()->ctcpParser()->query(coreNetwork(), bufferInfo.bufferName(), "ACTION", message);
-        emit displayMsg(NetworkInternalMessage(
-            Message::Action,
-            bufferInfo.type(),
-            bufferInfo.bufferName(),
-            message,
-            network()->myNick(),
-            Message::Self
-        ));
+        if (!network()->capEnabled(IrcCap::ECHO_MESSAGE)) {
+            emit displayMsg(NetworkInternalMessage(
+                Message::Action,
+                bufferInfo.type(),
+                bufferInfo.bufferName(),
+                message,
+                network()->myNick(),
+                Message::Self
+            ));
+        }
     }
 }
 
@@ -590,14 +594,16 @@ void CoreUserInputHandler::handleNotice(const BufferInfo& bufferInfo, const QStr
         params.clear();
         params << serverEncode(bufferName) << channelEncode(bufferInfo.bufferName(), message);
         emit putCmd("NOTICE", params);
-        emit displayMsg(NetworkInternalMessage(
-            Message::Notice,
-            typeByTarget(bufferName),
-            bufferName,
-            message,
-            network()->myNick(),
-            Message::Self
-        ));
+        if (!network()->capEnabled(IrcCap::ECHO_MESSAGE)) {
+            emit displayMsg(NetworkInternalMessage(
+                Message::Notice,
+                typeByTarget(bufferName),
+                bufferName,
+                message,
+                network()->myNick(),
+                Message::Self
+            ));
+        }
     }
 }
 
@@ -680,14 +686,16 @@ void CoreUserInputHandler::handleQuery(const BufferInfo& bufferInfo, const QStri
             // handleMsg is a no-op if message is empty
         }
         else {
-            emit displayMsg(NetworkInternalMessage(
-                Message::Plain,
-                BufferInfo::QueryBuffer,
-                target,
-                message,
-                network()->myNick(),
-                Message::Self
-            ));
+            if (!network()->capEnabled(IrcCap::ECHO_MESSAGE)) {
+                emit displayMsg(NetworkInternalMessage(
+                    Message::Plain,
+                    BufferInfo::QueryBuffer,
+                    target,
+                    message,
+                    network()->myNick(),
+                    Message::Self
+                ));
+            }
             // handleMsg needs the target specified at the beginning of the message
             handleMsg(bufferInfo, target + " " + message);
         }
@@ -731,14 +739,16 @@ void CoreUserInputHandler::handleSay(const BufferInfo& bufferInfo, const QString
 #else
         putPrivmsg(bufferInfo.bufferName(), message, encodeFunc);
 #endif
-        emit displayMsg(NetworkInternalMessage(
-            Message::Plain,
-            bufferInfo.type(),
-            bufferInfo.bufferName(),
-            message,
-            network()->myNick(),
-            Message::Self
-        ));
+        if (!network()->capEnabled(IrcCap::ECHO_MESSAGE)) {
+            emit displayMsg(NetworkInternalMessage(
+                Message::Plain,
+                bufferInfo.type(),
+                bufferInfo.bufferName(),
+                message,
+                network()->myNick(),
+                Message::Self
+            ));
+        }
     }
 }
 
index b35f12b..9b93b84 100644 (file)
@@ -66,6 +66,9 @@ void CtcpParser::displayMsg(NetworkEvent* event,
         return;
 
     MessageEvent* msgEvent = new MessageEvent(msgType, event->network(), std::move(msg), std::move(sender), std::move(target), msgFlags, event->timestamp());
+    if (event->testFlag(EventManager::Self)) {
+        msgEvent->setFlag(EventManager::Self);
+    }
     emit newEvent(msgEvent);
 }
 
@@ -236,6 +239,9 @@ void CtcpParser::parseSimple(IrcEventRawMessage* e,
                                              ctcpparam,
                                              e->timestamp(),
                                              uuid);
+            if (e->testFlag(EventManager::Self)) {
+                event->setFlag(EventManager::Self);
+            }
             emit newEvent(event);
             CtcpEvent* flushEvent = new CtcpEvent(EventManager::CtcpEventFlush,
                                                   e->network(),
@@ -313,6 +319,9 @@ void CtcpParser::parseStandard(IrcEventRawMessage* e,
                                              ctcpparam,
                                              e->timestamp(),
                                              uuid);
+            if (e->testFlag(EventManager::Self)) {
+                event->setFlag(EventManager::Self);
+            }
             ctcpEvents << event;
         }
     }
index 740310d..a852be1 100644 (file)
@@ -801,7 +801,12 @@ void EventStringifier::processCtcpEvent(CtcpEvent* e)
     if (e->type() != EventManager::CtcpEvent)
         return;
 
-    if (e->testFlag(EventManager::Self)) {
+    // Only stringify outgoing CTCP messages this way
+    if (e->testFlag(EventManager::Self) &&
+        // Only stringify CTCP queries this way
+        e->ctcpType() == CtcpEvent::CtcpType::Query &&
+        // Always handle ACTIONs as if they were sent from network, to properly handle echo-message
+        e->ctcpCmd() != "ACTION") {
         displayMsg(e,
                    Message::Action,
                    tr("sending CTCP-%1 request to %2").arg(e->ctcpCmd(), e->target()),
@@ -823,14 +828,24 @@ void EventStringifier::defaultHandler(const QString& ctcpCmd, CtcpEvent* e)
             //: Optional "unknown" in "Received unknown CTCP-FOO request by bar"
             unknown = tr("unknown") + ' ';
         displayMsg(e, Message::Server, tr("Received %1CTCP-%2 request by %3").arg(unknown, e->ctcpCmd(), e->prefix()));
-        return;
+    } else {
+        // Ignore echo messages for our own answers
+        if (!e->testFlag(EventManager::Self)) {
+            displayMsg(e, Message::Server,
+                       tr("Received CTCP-%1 answer from %2: %3").arg(e->ctcpCmd(), nickFromMask(e->prefix()),e->param()));
+        }
     }
-    displayMsg(e, Message::Server, tr("Received CTCP-%1 answer from %2: %3").arg(e->ctcpCmd(), nickFromMask(e->prefix()), e->param()));
 }
 
 void EventStringifier::handleCtcpAction(CtcpEvent* e)
 {
-    displayMsg(e, Message::Action, e->param(), e->prefix(), e->target());
+    Message::Flag msgFlags = Message::Flag::None;
+    if (e->testFlag(EventManager::Self)) {
+        // Mark the message as Self
+        msgFlags = Message::Self;
+    }
+
+    displayMsg(e, Message::Action, e->param(), e->prefix(), e->target(), msgFlags);
 }
 
 void EventStringifier::handleCtcpPing(CtcpEvent* e)