From a2e096ed11221866f9707ad61265a9d359a21de3 Mon Sep 17 00:00:00 2001 From: Shane Synan Date: Tue, 31 May 2016 19:51:22 -0400 Subject: [PATCH] Sync IrcUser account, add WHOX for account-notify 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 | 8 ++ src/common/ircuser.cpp | 9 ++ src/common/ircuser.h | 14 ++ src/core/corenetwork.cpp | 13 +- src/core/coresessioneventprocessor.cpp | 175 ++++++++++++++++++------- src/core/coresessioneventprocessor.h | 21 +++ src/core/eventstringifier.cpp | 10 ++ src/core/eventstringifier.h | 1 + 8 files changed, 196 insertions(+), 55 deletions(-) diff --git a/src/common/irccap.h b/src/common/irccap.h index 61dac32c..14c0ed7c 100644 --- a/src/common/irccap.h +++ b/src/common/irccap.h @@ -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. * diff --git a/src/common/ircuser.cpp b/src/common/ircuser.cpp index 9821cf46..463df85e 100644 --- a/src/common/ircuser.cpp +++ b/src/common/ircuser.cpp @@ -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) { diff --git a/src/common/ircuser.h b/src/common/ircuser.h index d0a01493..87d9114a 100644 --- a/src/common/ircuser.h +++ b/src/common/ircuser.h @@ -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; diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index f049fa07..ca935d96 100644 --- a/src/core/corenetwork.cpp +++ b/src/core/corenetwork.cpp @@ -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; } diff --git a/src/core/coresessioneventprocessor.cpp b/src/core/coresessioneventprocessor.cpp index fd8a97c5..f86b1035 100644 --- a/src/core/coresessioneventprocessor.cpp +++ b/src/core/coresessioneventprocessor.cpp @@ -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: " :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: " 152 # ~ + ("H"/ "G") :" + is * if not specific to any channel + 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) { diff --git a/src/core/coresessioneventprocessor.h b/src/core/coresessioneventprocessor.h index 56124242..9d5917ec 100644 --- a/src/core/coresessioneventprocessor.h +++ b/src/core/coresessioneventprocessor.h @@ -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 > _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); }; diff --git a/src/core/eventstringifier.cpp b/src/core/eventstringifier.cpp index 186fe130..f18af109 100644 --- a/src/core/eventstringifier.cpp +++ b/src/core/eventstringifier.cpp @@ -657,6 +657,16 @@ void EventStringifier::processIrcEvent352(IrcEvent *e) } +/* RPL_WHOSPCRPL: " # ~ + ("H"/ "G") :" +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 - " :End of WHOWAS" */ void EventStringifier::processIrcEvent369(IrcEvent *e) { diff --git a/src/core/eventstringifier.h b/src/core/eventstringifier.h index 66066b98..bef6ab86 100644 --- a/src/core/eventstringifier.h +++ b/src/core/eventstringifier.h @@ -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 -- 2.20.1