Batch request capabilities during negotiation
[quassel.git] / src / core / corenetwork.cpp
index 7e9ce26..76f430c 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2015 by the Quassel Project                        *
+ *   Copyright (C) 2005-2016 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
@@ -29,6 +29,9 @@
 #include "coreuserinputhandler.h"
 #include "networkevent.h"
 
+// IRCv3 capabilities
+#include "irccap.h"
+
 INIT_SYNCABLE_OBJECT(CoreNetwork)
 CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
     : Network(networkid, session),
@@ -36,6 +39,7 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
     _userInputHandler(new CoreUserInputHandler(this)),
     _autoReconnectCount(0),
     _quitRequested(false),
+    _disconnectExpected(false),
 
     _previousConnectionAttemptFailed(false),
     _lastUsedServerIndex(0),
@@ -71,7 +75,6 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
     connect(&_tokenBucketTimer, SIGNAL(timeout()), this, SLOT(fillBucketAndProcessQueue()));
 
     connect(&socket, SIGNAL(connected()), this, SLOT(socketInitialized()));
-    connect(&socket, SIGNAL(disconnected()), this, SLOT(socketDisconnected()));
     connect(&socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError)));
     connect(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(socketStateChanged(QAbstractSocket::SocketState)));
     connect(&socket, SIGNAL(readyRead()), this, SLOT(socketHasData()));
@@ -81,8 +84,14 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
 #endif
     connect(this, SIGNAL(newEvent(Event *)), coreSession()->eventManager(), SLOT(postEvent(Event *)));
 
+    // IRCv3 capability handling
+    // These react to CAP messages from the server
+    connect(this, SIGNAL(capAdded(QString)), this, SLOT(serverCapAdded(QString)));
+    connect(this, SIGNAL(capAcknowledged(QString)), this, SLOT(serverCapAcknowledged(QString)));
+    connect(this, SIGNAL(capRemoved(QString)), this, SLOT(serverCapRemoved(QString)));
+
     if (Quassel::isOptionSet("oidentd")) {
-        connect(this, SIGNAL(socketOpen(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Core::instance()->oidentdConfigGenerator(), SLOT(addSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Qt::BlockingQueuedConnection);
+        connect(this, SIGNAL(socketInitialized(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Core::instance()->oidentdConfigGenerator(), SLOT(addSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Qt::BlockingQueuedConnection);
         connect(this, SIGNAL(socketDisconnected(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)), Core::instance()->oidentdConfigGenerator(), SLOT(removeSocket(const CoreIdentity*, QHostAddress, quint16, QHostAddress, quint16)));
     }
 }
@@ -90,13 +99,39 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
 
 CoreNetwork::~CoreNetwork()
 {
-    if (connectionState() != Disconnected && connectionState() != Network::Reconnecting)
-        disconnectFromIrc(false);  // clean up, but this does not count as requested disconnect!
+    // Request a proper disconnect, but don't count as user-requested disconnect
+    if (socketConnected()) {
+        // Only try if the socket's fully connected (not initializing or disconnecting).
+        // Force an immediate disconnect, jumping the command queue.  Ensures the proper QUIT is
+        // shown even if other messages are queued.
+        disconnectFromIrc(false, QString(), false, true);
+        // Process the putCmd events that trigger the quit.  Without this, shutting down the core
+        // results in abrubtly closing the socket rather than sending the QUIT as expected.
+        QCoreApplication::processEvents();
+        // Wait briefly for each network to disconnect.  Sometimes it takes a little while to send.
+        if (!forceDisconnect()) {
+            qWarning() << "Timed out quitting network" << networkName() <<
+                          "(user ID " << userId() << ")";
+        }
+    }
     disconnect(&socket, 0, this, 0); // this keeps the socket from triggering events during clean up
     delete _userInputHandler;
 }
 
 
+bool CoreNetwork::forceDisconnect(int msecs)
+{
+    if (socket.state() == QAbstractSocket::UnconnectedState) {
+        // Socket already disconnected.
+        return true;
+    }
+    // Request a socket-level disconnect if not already happened
+    socket.disconnectFromHost();
+    // Return the result of waiting for disconnect; true if successful, otherwise false
+    return socket.waitForDisconnected(msecs);
+}
+
+
 QString CoreNetwork::channelDecode(const QString &bufferName, const QByteArray &string) const
 {
     if (!bufferName.isEmpty()) {
@@ -159,6 +194,13 @@ void CoreNetwork::connectToIrc(bool reconnecting)
     // cleaning up old quit reason
     _quitReason.clear();
 
+    // Reset capability negotiation tracking, also handling server changes during reconnect
+    _capsQueuedIndividual.clear();
+    _capsQueuedBundled.clear();
+    clearCaps();
+    _capNegotiationActive = false;
+    _capInitialNegotiationEnded = false;
+
     // use a random server?
     if (useRandomServer()) {
         _lastUsedServerIndex = qrand() % serverList().size();
@@ -208,8 +250,11 @@ void CoreNetwork::connectToIrc(bool reconnecting)
 }
 
 
-void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool withReconnect)
+void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool withReconnect,
+                                    bool forceImmediate)
 {
+    // Disconnecting from the network, should expect a socket close or error
+    _disconnectExpected = true;
     _quitRequested = requested; // see socketDisconnected();
     if (!withReconnect) {
         _autoReconnectTimer.stop();
@@ -236,7 +281,7 @@ void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool
         socketDisconnected();
     } else {
         if (socket.state() == QAbstractSocket::ConnectedState) {
-            userInputHandler()->issueQuit(_quitReason);
+            userInputHandler()->issueQuit(_quitReason, forceImmediate);
         } else {
             socket.close();
         }
@@ -254,16 +299,21 @@ void CoreNetwork::userInput(BufferInfo buf, QString msg)
 }
 
 
-void CoreNetwork::putRawLine(QByteArray s)
+void CoreNetwork::putRawLine(const QByteArray s, const bool prepend)
 {
-    if (_tokenBucket > 0)
+    if (_tokenBucket > 0) {
         writeToSocket(s);
-    else
-        _msgQueue.append(s);
+    } else {
+        if (prepend) {
+            _msgQueue.prepend(s);
+        } else {
+            _msgQueue.append(s);
+        }
+    }
 }
 
 
-void CoreNetwork::putCmd(const QString &cmd, const QList<QByteArray> &params, const QByteArray &prefix)
+void CoreNetwork::putCmd(const QString &cmd, const QList<QByteArray> &params, const QByteArray &prefix, const bool prepend)
 {
     QByteArray msg;
 
@@ -280,13 +330,23 @@ void CoreNetwork::putCmd(const QString &cmd, const QList<QByteArray> &params, co
         msg += params[i];
     }
 
-    putRawLine(msg);
+    putRawLine(msg, prepend);
+}
+
+
+void CoreNetwork::putCmd(const QString &cmd, const QList<QList<QByteArray>> &params, const QByteArray &prefix, const bool prependAll)
+{
+    QListIterator<QList<QByteArray>> i(params);
+    while (i.hasNext()) {
+        QList<QByteArray> msg = i.next();
+        putCmd(cmd, msg, prefix, prependAll);
+    }
 }
 
 
 void CoreNetwork::setChannelJoined(const QString &channel)
 {
-    _autoWhoQueue.prepend(channel.toLower()); // prepend so this new chan is the first to be checked
+    queueAutoWhoOneshot(channel); // check this new channel first
 
     Core::setChannelPersistent(userId(), networkId(), channel, true);
     Core::setPersistentChannelKey(userId(), networkId(), channel, _channelKeys[channel.toLower()]);
@@ -425,8 +485,10 @@ void CoreNetwork::socketHasData()
 
 void CoreNetwork::socketError(QAbstractSocket::SocketError error)
 {
-    if (_quitRequested && error == QAbstractSocket::RemoteHostClosedError)
+    // Ignore socket closed errors if expected
+    if (_disconnectExpected && error == QAbstractSocket::RemoteHostClosedError) {
         return;
+    }
 
     _previousConnectionAttemptFailed = true;
     qWarning() << qPrintable(tr("Could not connect to %1 (%2)").arg(networkName(), socket.errorString()));
@@ -448,16 +510,24 @@ void CoreNetwork::socketInitialized()
         return;
     }
 
-    emit socketOpen(identity, localAddress(), localPort(), peerAddress(), peerPort());
-
     Server server = usedServer();
+
 #ifdef HAVE_SSL
-    if (server.useSsl && !socket.isEncrypted())
+    // Non-SSL connections enter here only once, always emit socketInitialized(...) in these cases
+    // SSL connections call socketInitialized() twice, only emit socketInitialized(...) on the first (not yet encrypted) run
+    if (!server.useSsl || !socket.isEncrypted()) {
+        emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort());
+    }
+
+    if (server.useSsl && !socket.isEncrypted()) {
+        // We'll finish setup once we're encrypted, and called again
         return;
+    }
+#else
+    emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort());
 #endif
-    socket.setSocketOption(QAbstractSocket::KeepAliveOption, true);
 
-    emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort());
+    socket.setSocketOption(QAbstractSocket::KeepAliveOption, true);
 
     // TokenBucket to avoid sending too much at once
     _messageDelay = 2200;  // this seems to be a safe value (2.2 seconds delay)
@@ -465,9 +535,11 @@ void CoreNetwork::socketInitialized()
     _tokenBucket = _burstSize; // init with a full bucket
     _tokenBucketTimer.start(_messageDelay);
 
-    if (networkInfo().useSasl) {
-        putRawLine(serverEncode(QString("CAP REQ :sasl")));
-    }
+    // Request capabilities as per IRCv3.2 specifications
+    // Older servers should ignore this; newer servers won't downgrade to RFC1459
+    displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Requesting capability list..."));
+    putRawLine(serverEncode(QString("CAP LS 302")));
+
     if (!server.password.isEmpty()) {
         putRawLine(serverEncode(QString("PASS %1").arg(server.password)));
     }
@@ -479,7 +551,7 @@ void CoreNetwork::socketInitialized()
     else {
         nick = identity->nicks()[0];
     }
-    putRawLine(serverEncode(QString("NICK :%1").arg(nick)));
+    putRawLine(serverEncode(QString("NICK %1").arg(nick)));
     putRawLine(serverEncode(QString("USER %1 8 * :%2").arg(identity->ident(), identity->realName())));
 }
 
@@ -507,6 +579,8 @@ void CoreNetwork::socketDisconnected()
     setConnected(false);
     emit disconnected(networkId());
     emit socketDisconnected(identityPtr(), localAddress(), localPort(), peerAddress(), peerPort());
+    // Reset disconnect expectations
+    _disconnectExpected = false;
     if (_quitRequested) {
         _quitRequested = false;
         setConnectionState(Network::Disconnected);
@@ -528,6 +602,7 @@ void CoreNetwork::socketStateChanged(QAbstractSocket::SocketState socketState)
     switch (socketState) {
     case QAbstractSocket::UnconnectedState:
         state = Network::Disconnected;
+        socketDisconnected();
         break;
     case QAbstractSocket::HostLookupState:
     case QAbstractSocket::ConnectingState:
@@ -550,6 +625,7 @@ void CoreNetwork::networkInitialized()
 {
     setConnectionState(Network::Initialized);
     setConnected(true);
+    _disconnectExpected = false;
     _quitRequested = false;
 
     if (useAutoReconnect()) {
@@ -840,6 +916,227 @@ void CoreNetwork::setPingInterval(int interval)
     _pingTimer.setInterval(interval * 1000);
 }
 
+/******** IRCv3 Capability Negotiation ********/
+
+void CoreNetwork::serverCapAdded(const QString &capability)
+{
+    // Check if it's a known capability; if so, add it to the list
+    // Handle special cases first
+    if (capability == IrcCap::SASL) {
+        // Only request SASL if it's enabled
+        if (networkInfo().useSasl)
+            queueCap(capability);
+    } else if (IrcCap::knownCaps.contains(capability)) {
+        // Handling for general known capabilities
+        queueCap(capability);
+    }
+}
+
+void CoreNetwork::serverCapAcknowledged(const QString &capability)
+{
+    // This may be called multiple times in certain situations.
+
+    // Handle core-side configuration
+    if (capability == IrcCap::AWAY_NOTIFY) {
+        // away-notify enabled, stop the autoWho timers, handle manually
+        setAutoWhoEnabled(false);
+    }
+
+    // Handle capabilities that require further messages sent to the IRC server
+    // If you change this list, ALSO change the list in CoreNetwork::capsRequiringServerMessages
+    if (capability == IrcCap::SASL) {
+        // If SASL mechanisms specified, limit to what's accepted for authentication
+        // if the current identity has a cert set, use SASL EXTERNAL
+        // FIXME use event
+#ifdef HAVE_SSL
+        if (!identityPtr()->sslCert().isNull()) {
+            if (IrcCap::SaslMech::maybeSupported(capValue(IrcCap::SASL), IrcCap::SaslMech::EXTERNAL)) {
+                // EXTERNAL authentication supported, send request
+                putRawLine(serverEncode("AUTHENTICATE EXTERNAL"));
+            } else {
+                displayMsg(Message::Error, BufferInfo::StatusBuffer, "",
+                           tr("SASL EXTERNAL authentication not supported"));
+                sendNextCap();
+            }
+        } else {
+#endif
+            if (IrcCap::SaslMech::maybeSupported(capValue(IrcCap::SASL), IrcCap::SaslMech::PLAIN)) {
+                // PLAIN authentication supported, send request
+                // Only working with PLAIN atm, blowfish later
+                putRawLine(serverEncode("AUTHENTICATE PLAIN"));
+            } else {
+                displayMsg(Message::Error, BufferInfo::StatusBuffer, "",
+                           tr("SASL PLAIN authentication not supported"));
+                sendNextCap();
+            }
+#ifdef HAVE_SSL
+        }
+#endif
+    }
+}
+
+void CoreNetwork::serverCapRemoved(const QString &capability)
+{
+    // This may be called multiple times in certain situations.
+
+    // Handle special cases here
+    if (capability == IrcCap::AWAY_NOTIFY) {
+        // away-notify disabled, enable autoWho according to configuration
+        setAutoWhoEnabled(networkConfig()->autoWhoEnabled());
+    }
+}
+
+void CoreNetwork::queueCap(const QString &capability)
+{
+    // IRCv3 specs all use lowercase capability names
+    QString _capLowercase = capability.toLower();
+
+    if(capsRequiringConfiguration.contains(_capLowercase)) {
+        // The capability requires additional configuration before being acknowledged (e.g. SASL),
+        // so we should negotiate it separately from all other capabilities.  Otherwise new
+        // capabilities will be requested while still configuring the previous one.
+        if (!_capsQueuedIndividual.contains(_capLowercase)) {
+            _capsQueuedIndividual.append(_capLowercase);
+        }
+    } else {
+        // The capability doesn't need any special configuration, so it should be safe to try
+        // bundling together with others.  "Should" being the imperative word, as IRC servers can do
+        // anything.
+        if (!_capsQueuedBundled.contains(_capLowercase)) {
+            _capsQueuedBundled.append(_capLowercase);
+        }
+    }
+}
+
+QString CoreNetwork::takeQueuedCaps()
+{
+    // Clear the record of the most recently negotiated capability bundle.  Does nothing if the list
+    // is empty.
+    _capsQueuedLastBundle.clear();
+
+    // First, negotiate all the standalone capabilities that require additional configuration.
+    if (!_capsQueuedIndividual.empty()) {
+        // We have an individual capability available.  Take the first and pass it back.
+        return _capsQueuedIndividual.takeFirst();
+    } else if (!_capsQueuedBundled.empty()) {
+        // We have capabilities available that can be grouped.  Try to fit in as many as within the
+        // maximum length.
+        // See CoreNetwork::maxCapRequestLength
+
+        // Response must have at least one capability regardless of max length for anything to
+        // happen.
+        QString capBundle = _capsQueuedBundled.takeFirst();
+        QString nextCap("");
+        while (!_capsQueuedBundled.empty()) {
+            // As long as capabilities remain, get the next...
+            nextCap = _capsQueuedBundled.first();
+            if ((capBundle.length() + 1 + nextCap.length()) <= maxCapRequestLength) {
+                // [capability + 1 for a space + this new capability] fit within length limits
+                // Add it to formatted list
+                capBundle.append(" " + nextCap);
+                // Add it to most recent bundle of requested capabilities (simplifies retry logic)
+                _capsQueuedLastBundle.append(nextCap);
+                // Then remove it from the queue
+                _capsQueuedBundled.removeFirst();
+            } else {
+                // We've reached the length limit for a single capability request, stop adding more
+                break;
+            }
+        }
+        // Return this space-separated set of capabilities, removing any extra spaces
+        return capBundle.trimmed();
+    } else {
+        // No capabilities left to negotiate, return an empty string.
+        return QString();
+    }
+}
+
+void CoreNetwork::retryCapsIndividually()
+{
+    // The most recent set of capabilities got denied by the IRC server.  As we don't know what got
+    // denied, try each capability individually.
+    if (_capsQueuedLastBundle.empty()) {
+        // No most recently tried capability set, just return.
+        return;
+        // Note: there's little point in retrying individually requested caps during negotiation.
+        // We know the individual capability was the one that failed, and it's not likely it'll
+        // suddenly start working within a few seconds.  'cap-notify' provides a better system for
+        // handling capability removal and addition.
+    }
+
+    // This should be fairly rare, e.g. services restarting during negotiation, so simplicity wins
+    // over efficiency.  If this becomes an issue, implement a binary splicing system instead,
+    // keeping track of which halves of the group fail, dividing the set each time.
+
+    // Add most recently tried capability set to individual list, re-requesting them one at a time
+    _capsQueuedIndividual.append(_capsQueuedLastBundle);
+    // Warn of this issue to explain the slower login.  Servers usually shouldn't trigger this.
+    displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
+               tr("Could not negotiate some capabilities, retrying individually (%1)...")
+               .arg(_capsQueuedLastBundle.join(", ")));
+    // Capabilities are already removed from the capability bundle queue via takeQueuedCaps(), no
+    // need to remove them here.
+    // Clear the most recently tried set to reduce risk that mistakes elsewhere causes retrying
+    // indefinitely.
+    _capsQueuedLastBundle.clear();
+}
+
+void CoreNetwork::beginCapNegotiation()
+{
+    // Don't begin negotiation if no capabilities are queued to request
+    if (!capNegotiationInProgress()) {
+        // If the server doesn't have any capabilities, but supports CAP LS, continue on with the
+        // normal connection.
+        displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("No capabilities available"));
+        endCapNegotiation();
+        return;
+    }
+
+    _capNegotiationActive = true;
+    displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
+               tr("Ready to negotiate (found: %1)").arg(caps().join(", ")));
+
+    // Build a list of queued capabilities, starting with individual, then bundled, only adding the
+    // comma separator between the two if needed.
+    QString queuedCapsDisplay =
+            (!_capsQueuedIndividual.empty() ? _capsQueuedIndividual.join(", ") + ", " : "")
+            + _capsQueuedBundled.join(", ");
+    displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
+               tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay));
+
+    sendNextCap();
+}
+
+void CoreNetwork::sendNextCap()
+{
+    if (capNegotiationInProgress()) {
+        // Request the next set of capabilities and remove them from the list
+        putRawLine(serverEncode(QString("CAP REQ :%1").arg(takeQueuedCaps())));
+    } else {
+        // No pending desired capabilities, capability negotiation finished
+        // If SASL requested but not available, print a warning
+        if (networkInfo().useSasl && !capEnabled(IrcCap::SASL))
+            displayMsg(Message::Error, BufferInfo::StatusBuffer, "",
+                       tr("SASL authentication currently not supported by server"));
+
+        if (_capNegotiationActive) {
+            displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
+                   tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", ")));
+            _capNegotiationActive = false;
+        }
+
+        endCapNegotiation();
+    }
+}
+
+void CoreNetwork::endCapNegotiation()
+{
+    // If nick registration is already complete, CAP END is not required
+    if (!_capInitialNegotiationEnded) {
+        putRawLine(serverEncode(QString("CAP END")));
+        _capInitialNegotiationEnded = true;
+    }
+}
 
 /******** AutoWHO ********/
 
@@ -852,6 +1149,19 @@ void CoreNetwork::startAutoWhoCycle()
     _autoWhoQueue = channels();
 }
 
+void CoreNetwork::queueAutoWhoOneshot(const QString &channelOrNick)
+{
+    // Prepend so these new channels/nicks are the first to be checked
+    // Don't allow duplicates
+    if (!_autoWhoQueue.contains(channelOrNick.toLower())) {
+        _autoWhoQueue.prepend(channelOrNick.toLower());
+    }
+    if (capEnabled(IrcCap::AWAY_NOTIFY)) {
+        // When away-notify is active, the timer's stopped.  Start a new cycle to who this channel.
+        setAutoWhoEnabled(true);
+    }
+}
+
 
 void CoreNetwork::setAutoWhoDelay(int delay)
 {
@@ -883,19 +1193,48 @@ void CoreNetwork::sendAutoWho()
         return;
 
     while (!_autoWhoQueue.isEmpty()) {
-        QString chan = _autoWhoQueue.takeFirst();
-        IrcChannel *ircchan = ircChannel(chan);
-        if (!ircchan) continue;
-        if (networkConfig()->autoWhoNickLimit() > 0 && ircchan->ircUsers().count() >= networkConfig()->autoWhoNickLimit())
+        QString chanOrNick = _autoWhoQueue.takeFirst();
+        // Check if it's a known channel or nick
+        IrcChannel *ircchan = ircChannel(chanOrNick);
+        IrcUser *ircuser = ircUser(chanOrNick);
+        if (ircchan) {
+            // Apply channel limiting rules
+            // If using away-notify, don't impose channel size limits in order to capture away
+            // state of everyone.  Auto-who won't run on a timer so network impact is minimal.
+            if (networkConfig()->autoWhoNickLimit() > 0
+                && ircchan->ircUsers().count() >= networkConfig()->autoWhoNickLimit()
+                && !capEnabled(IrcCap::AWAY_NOTIFY))
+                continue;
+            _autoWhoPending[chanOrNick.toLower()]++;
+        } else if (ircuser) {
+            // Checking a nick, add it to the pending list
+            _autoWhoPending[ircuser->nick().toLower()]++;
+        } else {
+            // Not a channel or a nick, skip it
+            qDebug() << "Skipping who polling of unknown channel or nick" << chanOrNick;
             continue;
-        _autoWhoPending[chan]++;
-        putRawLine("WHO " + serverEncode(chan));
+        }
+        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;
     }
-    if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive()) {
+
+    if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive()
+        && !capEnabled(IrcCap::AWAY_NOTIFY)) {
         // Timer was stopped, means a new cycle is due immediately
+        // Don't run a new cycle if using away-notify; server will notify as appropriate
         _autoWhoCycleTimer.start();
         startAutoWhoCycle();
+    } else if (capEnabled(IrcCap::AWAY_NOTIFY) && _autoWhoCycleTimer.isActive()) {
+        // Don't run another who cycle if away-notify is enabled
+        _autoWhoCycleTimer.stop();
     }
 }
 
@@ -903,9 +1242,33 @@ void CoreNetwork::sendAutoWho()
 #ifdef HAVE_SSL
 void CoreNetwork::sslErrors(const QList<QSslError> &sslErrors)
 {
-    Q_UNUSED(sslErrors)
-    socket.ignoreSslErrors();
-    // TODO errorhandling
+    Server server = usedServer();
+    if (server.sslVerify) {
+        // Treat the SSL error as a hard error
+        QString sslErrorMessage = tr("Encrypted connection couldn't be verified, disconnecting "
+                                     "since verification is required");
+        if (!sslErrors.empty()) {
+            // Add the error reason if known
+            sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
+        }
+        displayMsg(Message::Error, BufferInfo::StatusBuffer, "", sslErrorMessage);
+
+        // Disconnect, triggering a reconnect in case it's a temporary issue with certificate
+        // validity, network trouble, etc.
+        disconnectFromIrc(false, QString("Encrypted connection not verified"), true /* withReconnect */);
+    } else {
+        // Treat the SSL error as a warning, continue to connect anyways
+        QString sslErrorMessage = tr("Encrypted connection couldn't be verified, continuing "
+                                     "since verification is not required");
+        if (!sslErrors.empty()) {
+            // Add the error reason if known
+            sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
+        }
+        displayMsg(Message::Info, BufferInfo::StatusBuffer, "", sslErrorMessage);
+
+        // Proceed with the connection
+        socket.ignoreSslErrors();
+    }
 }
 
 
@@ -980,3 +1343,79 @@ void CoreNetwork::requestSetNetworkInfo(const NetworkInfo &info)
         }
     }
 }
+
+
+QList<QList<QByteArray>> CoreNetwork::splitMessage(const QString &cmd, const QString &message, std::function<QList<QByteArray>(QString &)> cmdGenerator)
+{
+    QString wrkMsg(message);
+    QList<QList<QByteArray>> msgsToSend;
+
+    // do while (wrkMsg.size() > 0)
+    do {
+        // First, check to see if the whole message can be sent at once.  The
+        // cmdGenerator function is passed in by the caller and is used to encode
+        // and encrypt (if applicable) the message, since different callers might
+        // want to use different encoding or encode different values.
+        int splitPos = wrkMsg.size();
+        QList<QByteArray> initialSplitMsgEnc = cmdGenerator(wrkMsg);
+        int initialOverrun = userInputHandler()->lastParamOverrun(cmd, initialSplitMsgEnc);
+
+        if (initialOverrun) {
+            // If the message was too long to be sent, first try splitting it along
+            // word boundaries with QTextBoundaryFinder.
+            QString splitMsg(wrkMsg);
+            QTextBoundaryFinder qtbf(QTextBoundaryFinder::Word, splitMsg);
+            qtbf.setPosition(initialSplitMsgEnc[1].size() - initialOverrun);
+            QList<QByteArray> splitMsgEnc;
+            int overrun = initialOverrun;
+
+            while (overrun) {
+                splitPos = qtbf.toPreviousBoundary();
+
+                // splitPos==-1 means the QTBF couldn't find a split point at all and
+                // splitPos==0 means the QTBF could only find a boundary at the beginning of
+                // the string.  Neither one of these works for us.
+                if (splitPos > 0) {
+                    // If a split point could be found, split the message there, calculate the
+                    // overrun, and continue with the loop.
+                    splitMsg = splitMsg.left(splitPos);
+                    splitMsgEnc = cmdGenerator(splitMsg);
+                    overrun = userInputHandler()->lastParamOverrun(cmd, splitMsgEnc);
+                }
+                else {
+                    // If a split point could not be found (the beginning of the message
+                    // is reached without finding a split point short enough to send) and we
+                    // are still in Word mode, switch to Grapheme mode.  We also need to restore
+                    // the full wrkMsg to splitMsg, since splitMsg may have been cut down during
+                    // the previous attempt to find a split point.
+                    if (qtbf.type() == QTextBoundaryFinder::Word) {
+                        splitMsg = wrkMsg;
+                        splitPos = splitMsg.size();
+                        QTextBoundaryFinder graphemeQtbf(QTextBoundaryFinder::Grapheme, splitMsg);
+                        graphemeQtbf.setPosition(initialSplitMsgEnc[1].size() - initialOverrun);
+                        qtbf = graphemeQtbf;
+                    }
+                    else {
+                        // If the QTBF fails to find a split point in Grapheme mode, we give up.
+                        // This should never happen, but it should be handled anyway.
+                        qWarning() << "Unexpected failure to split message!";
+                        return msgsToSend;
+                    }
+                }
+            }
+
+            // Once a message of sendable length has been found, remove it from the wrkMsg and
+            // add it to the list of messages to be sent.
+            wrkMsg.remove(0, splitPos);
+            msgsToSend.append(splitMsgEnc);
+        }
+        else{
+            // If the entire remaining message is short enough to be sent all at once, remove
+            // it from the wrkMsg and add it to the list of messages to be sent.
+            wrkMsg.remove(0, splitPos);
+            msgsToSend.append(initialSplitMsgEnc);
+        }
+    } while (wrkMsg.size() > 0);
+
+    return msgsToSend;
+}