Sync IrcUser account, add WHOX for account-notify
authorShane Synan <digitalcircuit36939@gmail.com>
Tue, 31 May 2016 23:51:22 +0000 (19:51 -0400)
committerShane Synan <digitalcircuit36939@gmail.com>
Mon, 13 Jun 2016 21:00:49 +0000 (17:00 -0400)
Add 'account' field to IrcUser, tracking logged-in account, e.g.
NickServ/SASL.  In testing, this works fine with older/newer
cores/servers.

Implement support for WHOX as in IRCv3 specifications, determining
logged-in accounts on first join to channels.  Mimics HexChat's
ordering of fields, reusing any testing they've done.

Display WHOX replies as [WhoX].  As fields are user-specified, more
processing would be difficult to manage.  WHO replies are unchanged.

Unify WHO reply processing in a common function.

Preliminary test results on Ubuntu:
> New core, new client - works fine
> New core, old client - client prints warnings to console but works
(Complains of no matching slot for sync call.  Old client doesn't
use the new information, though.)
> New client, old core - works fine, assumes unknown account state
> New core, Quasseldroid alpha - works fine, warnings in debug log

See http://faerion.sourceforge.net/doc/irc/whox.var
And https://github.com/hexchat/hexchat/blob/c874a9525c9b66f1d5ddcf6c4107d046eba7e2c5/src/common/proto-irc.c#L750

src/common/irccap.h
src/common/ircuser.cpp
src/common/ircuser.h
src/core/corenetwork.cpp
src/core/coresessioneventprocessor.cpp
src/core/coresessioneventprocessor.h
src/core/eventstringifier.cpp
src/core/eventstringifier.h

index 61dac32..14c0ed7 100644 (file)
@@ -41,6 +41,14 @@ namespace IrcCap {
      */
     const QString ACCOUNT_NOTIFY = "account-notify";
 
+    /**
+     * Magic number for WHOX, used to ignore user-requested WHOX replies from servers
+     *
+     * If a user initiates a WHOX, there's no easy way to tell what fields were requested.  It's
+     * simpler to not attempt to parse data from user-requested WHOX replies.
+     */
+    const uint ACCOUNT_NOTIFY_WHOX_NUM = 369;
+
     /**
      * Away change notification.
      *
index 9821cf4..463df85 100644 (file)
@@ -146,6 +146,15 @@ void IrcUser::setRealName(const QString &realName)
 }
 
 
+void IrcUser::setAccount(const QString &account)
+{
+    if (_account != account) {
+        _account = account;
+        SYNC(ARG(account))
+    }
+}
+
+
 void IrcUser::setAway(const bool &away)
 {
     if (away != _away) {
index d0a0149..87d9114 100644 (file)
@@ -43,6 +43,7 @@ class IrcUser : public SyncableObject
     Q_PROPERTY(QString host READ host WRITE setHost)
     Q_PROPERTY(QString nick READ nick WRITE setNick)
     Q_PROPERTY(QString realName READ realName WRITE setRealName)
+    Q_PROPERTY(QString account READ account WRITE setAccount)
     Q_PROPERTY(bool away READ isAway WRITE setAway)
     Q_PROPERTY(QString awayMessage READ awayMessage WRITE setAwayMessage)
     Q_PROPERTY(QDateTime idleTime READ idleTime WRITE setIdleTime)
@@ -65,6 +66,12 @@ public :
     inline QString host() const { return _host; }
     inline QString nick() const { return _nick; }
     inline QString realName() const { return _realName; }
+    /**
+     * Account name, e.g. NickServ/SASL account
+     *
+     * @return Account name if logged in, * if logged out, or empty string if unknown
+     */
+    inline QString account() const { return _account; }
     QString hostmask() const;
     inline bool isAway() const { return _away; }
     inline QString awayMessage() const { return _awayMessage; }
@@ -104,6 +111,12 @@ public slots:
     void setHost(const QString &host);
     void setNick(const QString &nick);
     void setRealName(const QString &realName);
+    /**
+     * Set account name, e.g. NickServ/SASL account
+     *
+     * @param[in] account Account name if logged in, * if logged out, or empty string if unknown
+     */
+    void setAccount(const QString &account);
     void setAway(const bool &away);
     void setAwayMessage(const QString &awayMessage);
     void setIdleTime(const QDateTime &idleTime);
@@ -182,6 +195,7 @@ private:
     QString _user;
     QString _host;
     QString _realName;
+    QString _account;      /// Account name, e.g. NickServ/SASL account
     QString _awayMessage;
     bool _away;
     QString _server;
index f049fa0..ca935d9 100644 (file)
@@ -1080,10 +1080,15 @@ void CoreNetwork::sendAutoWho()
             qDebug() << "Skipping who polling of unknown channel or nick" << chanOrNick;
             continue;
         }
-        // TODO Use WHO extended to poll away users and/or user accounts
-        // If a server supports it, supports("WHOX") will be true
-        // See: http://faerion.sourceforge.net/doc/irc/whox.var and HexChat
-        putRawLine("WHO " + serverEncode(chanOrNick));
+        if (supports("WHOX")) {
+            // Use WHO extended to poll away users and/or user accounts
+            // See http://faerion.sourceforge.net/doc/irc/whox.var
+            // And https://github.com/hexchat/hexchat/blob/c874a9525c9b66f1d5ddcf6c4107d046eba7e2c5/src/common/proto-irc.c#L750
+            putRawLine(serverEncode(QString("WHO %1 %%chtsunfra,%2")
+                                    .arg(serverEncode(chanOrNick), QString::number(IrcCap::ACCOUNT_NOTIFY_WHOX_NUM))));
+        } else {
+            putRawLine(serverEncode(QString("WHO %1").arg(chanOrNick)));
+        }
         break;
     }
 
index fd8a97c..f86b103 100644 (file)
@@ -242,16 +242,15 @@ void CoreSessionEventProcessor::processIrcEventAccount(IrcEvent *e)
 
     IrcUser *ircuser = e->network()->updateNickFromMask(e->prefix());
     if (ircuser) {
-        // FIXME Keep track of authed user account, requires adding support to ircuser.h/cpp
-        /*
-        if (e->params().at(0) != "*") {
-            // Account logged in
-            qDebug() << "account-notify:" << ircuser->nick() << "logged in to" << e->params().at(0);
+        QString newAccount = e->params().at(0);
+        // WHOX uses '0' to indicate logged-out, account-notify uses '*'
+        if (newAccount != "*") {
+            // Account logged in, set account name
+            ircuser->setAccount(newAccount);
         } else {
-            // Account logged out
-            qDebug() << "account-notify:" << ircuser->nick() << "logged out";
+            // Account logged out, set account name to logged-out
+            ircuser->setAccount("*");
         }
-        */
     } else {
         qDebug() << "Received account-notify data for unknown user" << e->prefix();
     }
@@ -875,6 +874,19 @@ void CoreSessionEventProcessor::processIrcEvent324(IrcEvent *e)
 }
 
 
+/*  RPL_WHOISACCOUNT: "<nick> <account> :is authed as */
+void CoreSessionEventProcessor::processIrcEvent330(IrcEvent *e)
+{
+    if (!checkParamCount(e, 3))
+        return;
+
+    IrcUser *ircuser = e->network()->ircUser(e->params().at(0));
+    if (ircuser) {
+        ircuser->setAccount(e->params().at(1));
+    }
+}
+
+
 /* RPL_NOTOPIC */
 void CoreSessionEventProcessor::processIrcEvent331(IrcEvent *e)
 {
@@ -909,49 +921,8 @@ void CoreSessionEventProcessor::processIrcEvent352(IrcEvent *e)
     QString channel = e->params()[0];
     IrcUser *ircuser = e->network()->ircUser(e->params()[4]);
     if (ircuser) {
-        ircuser->setUser(e->params()[1]);
-        ircuser->setHost(e->params()[2]);
-
-        bool away = e->params()[5].contains("G", Qt::CaseInsensitive);
-        ircuser->setAway(away);
-        ircuser->setServer(e->params()[3]);
-        ircuser->setRealName(e->params().last().section(" ", 1));
-
-        if (coreNetwork(e)->capEnabled(IrcCap::MULTI_PREFIX)) {
-            // If multi-prefix is enabled, all modes will be sent in WHO replies.
-            // :kenny.chatspike.net 352 guest #test grawity broken.symlink *.chatspike.net grawity H@%+ :0 Mantas M.
-            // See: http://ircv3.net/specs/extensions/multi-prefix-3.1.html
-            QString uncheckedModes = e->params()[5];
-            QString validModes = QString();
-            while (!uncheckedModes.isEmpty()) {
-                // Mode found in 1 left-most character, add it to the list
-                if (e->network()->prefixes().contains(uncheckedModes[0])) {
-                    validModes.append(e->network()->prefixToMode(uncheckedModes[0]));
-                }
-                // Remove this mode from the list of unchecked modes
-                uncheckedModes = uncheckedModes.remove(0, 1);
-            }
-
-            // Some IRC servers decide to not follow the spec, returning only -some- of the user
-            // modes in WHO despite listing them all in NAMES.  For now, assume it can only add
-            // and not take away.  *sigh*
-            if (!validModes.isEmpty()) {
-                if (channel != "*") {
-                    // Channel-specific modes received, apply to given channel only
-                    IrcChannel *ircChan = e->network()->ircChannel(channel);
-                    if (ircChan) {
-                        // Do one mode at a time
-                        // TODO Better way of syncing this without breaking protocol?
-                        for (int i = 0; i < validModes.count(); ++i) {
-                            ircChan->addUserMode(ircuser, validModes.at(i));
-                        }
-                    }
-                } else {
-                    // Modes apply to the user everywhere
-                    ircuser->addUserModes(validModes);
-                }
-            }
-        }
+        processWhoInformation(e->network(), channel, ircuser, e->params()[3], e->params()[1],
+                e->params()[2], e->params()[5], e->params().last().section(" ", 1));
     }
 
     // Check if channel name has a who in progress.
@@ -1019,6 +990,108 @@ void CoreSessionEventProcessor::processIrcEvent353(IrcEvent *e)
 }
 
 
+/*  RPL_WHOSPCRPL: "<yournick> 152 #<channel> ~<ident> <host> <servname> <nick>
+                    ("H"/ "G") <account> :<realname>"
+<channel> is * if not specific to any channel
+<account> is * if not logged in
+Follows HexChat's usage of 'whox'
+See https://github.com/hexchat/hexchat/blob/c874a9525c9b66f1d5ddcf6c4107d046eba7e2c5/src/common/proto-irc.c#L750
+And http://faerion.sourceforge.net/doc/irc/whox.var*/
+void CoreSessionEventProcessor::processIrcEvent354(IrcEvent *e)
+{
+    // First only check if at least one parameter exists.  Otherwise, it'll stop the result from
+    // being shown if the user chooses different parameters.
+    if (!checkParamCount(e, 1))
+        return;
+
+    if (e->params()[0].toUInt() != IrcCap::ACCOUNT_NOTIFY_WHOX_NUM) {
+        // Ignore WHOX replies without expected number for we have no idea what fields are specified
+        return;
+    }
+
+    // Now we're fairly certain this is supposed to be an automated WHOX.  Bail out if it doesn't
+    // match what we require - 9 parameters.
+    if (!checkParamCount(e, 9))
+        return;
+
+    QString channel = e->params()[1];
+    IrcUser *ircuser = e->network()->ircUser(e->params()[5]);
+    if (ircuser) {
+        processWhoInformation(e->network(), channel, ircuser, e->params()[4], e->params()[2],
+                e->params()[3], e->params()[6], e->params().last());
+        // Don't use .section(" ", 1) with WHOX replies, for there's no hopcount to trim out
+
+        // As part of IRCv3 account-notify, check account name
+        // WHOX uses '0' to indicate logged-out, account-notify uses '*'
+        QString newAccount = e->params()[7];
+        if (newAccount != "0") {
+            // Account logged in, set account name
+            ircuser->setAccount(newAccount);
+        } else {
+            // Account logged out, set account name to logged-out
+            ircuser->setAccount("*");
+        }
+    }
+
+    // Check if channel name has a who in progress.
+    // If not, then check if user nick exists and has a who in progress.
+    if (coreNetwork(e)->isAutoWhoInProgress(channel) ||
+        (ircuser && coreNetwork(e)->isAutoWhoInProgress(ircuser->nick()))) {
+        e->setFlag(EventManager::Silent);
+    }
+}
+
+
+void CoreSessionEventProcessor::processWhoInformation (Network *net, const QString &targetChannel, IrcUser *ircUser,
+                            const QString &server, const QString &user, const QString &host,
+                            const QString &awayStateAndModes, const QString &realname)
+{
+    ircUser->setUser(user);
+    ircUser->setHost(host);
+    ircUser->setServer(server);
+    ircUser->setRealName(realname);
+
+    bool away = awayStateAndModes.contains("G", Qt::CaseInsensitive);
+    ircUser->setAway(away);
+
+    if (net->capEnabled(IrcCap::MULTI_PREFIX)) {
+        // If multi-prefix is enabled, all modes will be sent in WHO replies.
+        // :kenny.chatspike.net 352 guest #test grawity broken.symlink *.chatspike.net grawity H@%+ :0 Mantas M.
+        // See: http://ircv3.net/specs/extensions/multi-prefix-3.1.html
+        QString uncheckedModes = awayStateAndModes;
+        QString validModes = QString();
+        while (!uncheckedModes.isEmpty()) {
+            // Mode found in 1 left-most character, add it to the list
+            if (net->prefixes().contains(uncheckedModes[0])) {
+                validModes.append(net->prefixToMode(uncheckedModes[0]));
+            }
+            // Remove this mode from the list of unchecked modes
+            uncheckedModes = uncheckedModes.remove(0, 1);
+        }
+
+        // Some IRC servers decide to not follow the spec, returning only -some- of the user
+        // modes in WHO despite listing them all in NAMES.  For now, assume it can only add
+        // and not take away.  *sigh*
+        if (!validModes.isEmpty()) {
+            if (targetChannel != "*") {
+                // Channel-specific modes received, apply to given channel only
+                IrcChannel *ircChan = net->ircChannel(targetChannel);
+                if (ircChan) {
+                    // Do one mode at a time
+                    // TODO Better way of syncing this without breaking protocol?
+                    for (int i = 0; i < validModes.count(); ++i) {
+                        ircChan->addUserMode(ircUser, validModes.at(i));
+                    }
+                }
+            } else {
+                // Modes apply to the user everywhere
+                ircUser->addUserModes(validModes);
+            }
+        }
+    }
+}
+
+
 /* ERR_ERRONEUSNICKNAME */
 void CoreSessionEventProcessor::processIrcEvent432(IrcEventNumeric *e)
 {
index 5612424..9d5917e 100644 (file)
@@ -85,10 +85,12 @@ public:
     Q_INVOKABLE void processIrcEvent322(IrcEvent *event);          // RPL_LIST
     Q_INVOKABLE void processIrcEvent323(IrcEvent *event);          // RPL_LISTEND
     Q_INVOKABLE void processIrcEvent324(IrcEvent *event);          // RPL_CHANNELMODEIS
+    Q_INVOKABLE void processIrcEvent330(IrcEvent *event);          // RPL_WHOISACCOUNT (quakenet/snircd/undernet)
     Q_INVOKABLE void processIrcEvent331(IrcEvent *event);          // RPL_NOTOPIC
     Q_INVOKABLE void processIrcEvent332(IrcEvent *event);          // RPL_TOPIC
     Q_INVOKABLE void processIrcEvent352(IrcEvent *event);          // RPL_WHOREPLY
     Q_INVOKABLE void processIrcEvent353(IrcEvent *event);          // RPL_NAMREPLY
+    Q_INVOKABLE void processIrcEvent354(IrcEvent *event);          // RPL_WHOSPCRPL
     Q_INVOKABLE void processIrcEvent432(IrcEventNumeric *event);   // ERR_ERRONEUSNICKNAME
     Q_INVOKABLE void processIrcEvent433(IrcEventNumeric *event);   // ERR_NICKNAMEINUSE
     Q_INVOKABLE void processIrcEvent437(IrcEventNumeric *event);   // ERR_UNAVAILRESOURCE
@@ -155,6 +157,25 @@ private:
     // key: quit message
     // value: the corresponding netsplit object
     QHash<Network *, QHash<QString, Netsplit *> > _netsplits;
+
+    /**
+     * Process given WHO reply information, updating user data, channel modes, etc as needed
+     *
+     * This takes information from WHO and WHOX replies, processing information that's common
+     * between them.
+     *
+     * @param[in] net                 Network object for the IRC server
+     * @param[in] targetChannel       Target channel, or * if unspecified
+     * @param[in] ircUser             IrcUser representing the desired nick
+     * @param[in] server              Nick server name
+     * @param[in] user                Nick username
+     * @param[in] host                Nick hostname
+     * @param[in] awayStateAndModes   Nick away-state and modes (e.g. G@)
+     * @param[in] realname            Nick realname
+     */
+    void processWhoInformation (Network *net, const QString &targetChannel, IrcUser *ircUser,
+                                const QString &server, const QString &user, const QString &host,
+                                const QString &awayStateAndModes, const QString &realname);
 };
 
 
index 186fe13..f18af10 100644 (file)
@@ -657,6 +657,16 @@ void EventStringifier::processIrcEvent352(IrcEvent *e)
 }
 
 
+/*  RPL_WHOSPCRPL: "<yournick> <num> #<channel> ~<ident> <host> <servname> <nick>
+                    ("H"/ "G") <account> :<realname>"
+Could be anything else, though.  User-specified fields.
+See http://faerion.sourceforge.net/doc/irc/whox.var */
+void EventStringifier::processIrcEvent354(IrcEvent *e)
+{
+    displayMsg(e, Message::Server, tr("[WhoX] %1").arg(e->params().join(" ")));
+}
+
+
 /*  RPL_ENDOFWHOWAS - "<nick> :End of WHOWAS" */
 void EventStringifier::processIrcEvent369(IrcEvent *e)
 {
index 66066b9..bef6ab8 100644 (file)
@@ -88,6 +88,7 @@ public:
     Q_INVOKABLE void processIrcEvent333(IrcEvent *event);    // RPL_??? (topic set by)
     Q_INVOKABLE void processIrcEvent341(IrcEvent *event);    // RPL_INVITING
     Q_INVOKABLE void processIrcEvent352(IrcEvent *event);    // RPL_WHOREPLY
+    Q_INVOKABLE void processIrcEvent354(IrcEvent *event);    // RPL_WHOSPCRPL
     Q_INVOKABLE void processIrcEvent369(IrcEvent *event);    // RPL_ENDOFWHOWAS
     Q_INVOKABLE void processIrcEvent432(IrcEvent *event);    // ERR_ERRONEUSNICKNAME
     Q_INVOKABLE void processIrcEvent433(IrcEvent *event);    // ERR_NICKNAMEINUSE