core: Implement account-tag
authorShane Synan <digitalcircuit36939@gmail.com>
Tue, 21 Jul 2020 01:56:39 +0000 (21:56 -0400)
committerManuel Nickschas <sputnick@quassel-irc.org>
Sat, 28 Nov 2020 12:42:31 +0000 (13:42 +0100)
Implement the "account-tag" capability, updating IrcUser account
status with information from the "account" tag when available.
According to the specification, "account" will be sent with all
user-initiated commands when logged in, and will NOT be sent when
logged out.

Quassel takes a conservative approach combining the approaches taken
by other clients.  For PRIVMSG and NOTICE, "account" tag is used to
set whether the IrcUser is logged in if present, AND logged out if NOT
present.  For all other messages, if the "account" tag is present and
the prefix refers to an EXISTING IrcUser, the IrcUser will be set as
logged in, but will NOT be set as logged out if the tag is missing.

Server/client details as of 2020-7-20
* Oragono (server)
Sends account tag with any client-initiated command
https://github.com/oragono/oragono/blob/b5855e61941e4dc33b64f6d61996a23686136166/irc/client.go#L1474

* InspIRCd (server)
Sends account tag with any client-initiated command
https://github.com/inspircd/inspircd/blob/4a6fedd9324d87349a806c9c1d0ae6e7d3c1fd38/src/modules/m_ircv3_accounttag.cpp

* BitBot (client/bot)
Marks as logged in, does not handle logging out
https://github.com/jesopo/bitbot/blob/9dfbe3723c8af97c9fbd35a3391298bf6f641043/src/core_modules/permissions/__init__.py#L117-L121

* PIRC.pl web client (client)
Marks as logged in, does not handle logging out
https://github.com/pirc-pl/pirc-gateway/blob/63d7be177568e99cada61cb53f1176b857bfca72/js/gateway_def.js#L2605-L2607

* KiwiIRC (client)
Marks as logged in AND logged out via PRIVMSG, NOTICE, WALLOPS, ignores other commands
https://github.com/kiwiirc/irc-framework/blob/11e9e8141dbbff68d730d2d840454949d2b2b5f5/src/commands/handlers/messaging.js#L9-L139

See https://ircv3.net/specs/extensions/account-tag-3.2

src/common/irccap.h
src/common/irctags.h
src/core/ircparser.cpp

index 132bd71..0bcb66d 100644 (file)
@@ -48,6 +48,13 @@ namespace IrcCap {
      */
     const uint ACCOUNT_NOTIFY_WHOX_NUM = 369;
 
+    /**
+     * Send account information as a tag with all commands sent by a user.
+     *
+     * http://ircv3.net/specs/extensions/account-notify-3.1.html
+     */
+    const QString ACCOUNT_TAG = "account-tag";
+
     /**
      * Away change notification.
      *
@@ -155,6 +162,7 @@ namespace IrcCap {
      * List of capabilities currently implemented and requested during capability negotiation.
      */
     const QStringList knownCaps = QStringList{ACCOUNT_NOTIFY,
+                                              ACCOUNT_TAG,
                                               AWAY_NOTIFY,
                                               CAP_NOTIFY,
                                               CHGHOST,
index 0687a27..ed1f5a7 100644 (file)
  */
 namespace IrcTags
 {
+    /**
+     * Services account status with user messages
+     *
+     * https://ircv3.net/specs/extensions/account-tag-3.2
+     */
+    const IrcTagKey ACCOUNT = IrcTagKey{"", "account", false};
+
     /**
      * Server time for messages.
      *
index ccd3950..1a2a1fd 100644 (file)
@@ -129,6 +129,26 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent* e)
         }
     }
 
+    if (net->capEnabled(IrcCap::ACCOUNT_TAG) && tags.contains(IrcTags::ACCOUNT)) {
+        // Whenever account-tag is specified, update the relevant IrcUser if it exists
+        // Logged-out status is handled in specific commands (PRIVMSG, NOTICE, etc)
+        //
+        // Don't use "updateNickFromMask" here to ensure this only updates existing IrcUsers and
+        // won't create a new IrcUser.  This guards against an IRC server setting "account" tag in
+        // nonsensical places, e.g. for messages that are not user sent.
+        IrcUser* ircuser = net->ircUser(prefix);
+        if (ircuser) {
+            ircuser->setAccount(tags[IrcTags::ACCOUNT]);
+        }
+
+        // NOTE: if "account-tag" is enabled and no "account" tag is sent, the given user isn't
+        // logged in ONLY IF it is a user-initiated command.  Quassel follows a mixture of what
+        // other clients do here - only marking as logged out via PRIVMSG/NOTICE, but marking logged
+        // in via any message.
+        //
+        // See https://ircv3.net/specs/extensions/account-tag-3.2
+    }
+
     QList<Event*> events;
     EventManager::EventType type = EventManager::Invalid;
 
@@ -169,7 +189,24 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent* e)
 
         if (checkParamCount(cmd, params, 1)) {
             QString senderNick = nickFromMask(prefix);
-            net->updateNickFromMask(prefix);
+            // Fetch/create the relevant IrcUser, and store it for later updates
+            IrcUser* ircuser = net->updateNickFromMask(prefix);
+
+            // Handle account-tag
+            if (ircuser && net->capEnabled(IrcCap::ACCOUNT_TAG)) {
+                if (tags.contains(IrcTags::ACCOUNT)) {
+                    // Account tag available, set account.
+                    // This duplicates the generic account-tag handling in case a new IrcUser object
+                    // was just created.
+                    ircuser->setAccount(tags[IrcTags::ACCOUNT]);
+                }
+                else {
+                    // PRIVMSG is user sent; it's safe to assume the user has logged out.
+                    // "*" is used to represent logged-out.
+                    ircuser->setAccount("*");
+                }
+            }
+
             // Check if the sender is our own nick.  If so, treat message as if sent by ourself.
             // See http://ircv3.net/specs/extensions/echo-message-3.2.html
             // Cache the result to avoid multiple redundant comparisons
@@ -212,6 +249,9 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent* e)
             // Cache the result to avoid multiple redundant comparisons
             bool isSelfMessage = net->isMyNick(nickFromMask(prefix));
 
+            // Only update from the prefix once during the loop
+            bool updatedFromPrefix = false;
+
             QStringList targets = net->serverDecode(params.at(0)).split(',', QString::SkipEmptyParts);
             QStringList::const_iterator targetIter;
             for (targetIter = targets.constBegin(); targetIter != targets.constEnd(); ++targetIter) {
@@ -248,7 +288,29 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent* e)
                         if (!isSelfMessage) {
                             target = nickFromMask(prefix);
                         }
-                        net->updateNickFromMask(prefix);
+
+                        if (!updatedFromPrefix) {
+                            // Don't repeat this within the loop, the prefix doesn't change
+                            updatedFromPrefix = true;
+
+                            // Fetch/create the relevant IrcUser, and store it for later updates
+                            IrcUser* ircuser = net->updateNickFromMask(prefix);
+
+                            // Handle account-tag
+                            if (ircuser && net->capEnabled(IrcCap::ACCOUNT_TAG)) {
+                                if (tags.contains(IrcTags::ACCOUNT)) {
+                                    // Account tag available, set account.
+                                    // This duplicates the generic account-tag handling in case a
+                                    // new IrcUser object was just created.
+                                    ircuser->setAccount(tags[IrcTags::ACCOUNT]);
+                                }
+                                else {
+                                    // NOTICE is user sent; it's safe to assume the user has
+                                    // logged out.  "*" is used to represent logged-out.
+                                    ircuser->setAccount("*");
+                                }
+                            }
+                        }
                     }
                 }