modernize: Replace most remaining old-style connects by PMF ones
[quassel.git] / src / core / corenetwork.cpp
index 30f319b..81e1b5e 100644 (file)
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
-#include <QHostInfo>
-
 #include "corenetwork.h"
 
+#include <QDebug>
+#include <QHostInfo>
+
 #include "core.h"
 #include "coreidentity.h"
 #include "corenetworkconfig.h"
@@ -32,7 +33,6 @@
 // IRCv3 capabilities
 #include "irccap.h"
 
-INIT_SYNCABLE_OBJECT(CoreNetwork)
 CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
     : Network(networkid, session),
     _coreSession(session),
@@ -44,16 +44,17 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
     _previousConnectionAttemptFailed(false),
     _lastUsedServerIndex(0),
 
-    _lastPingTime(0),
-    _pingCount(0),
-    _sendPings(false),
     _requestedUserModes('-')
 {
+    // Check if raw IRC logging is enabled
+    _debugLogRawIrc = (Quassel::isOptionSet("debug-irc") || Quassel::isOptionSet("debug-irc-id"));
+    _debugLogRawNetId = Quassel::optionValue("debug-irc-id").toInt();
+
     _autoReconnectTimer.setSingleShot(true);
-    connect(&_socketCloseTimer, SIGNAL(timeout()), this, SLOT(socketCloseTimeout()));
+    connect(&_socketCloseTimer, &QTimer::timeout, this, &CoreNetwork::onSocketCloseTimeout);
 
     setPingInterval(networkConfig()->pingInterval());
-    connect(&_pingTimer, SIGNAL(timeout()), this, SLOT(sendPing()));
+    connect(&_pingTimer, &QTimer::timeout, this, &CoreNetwork::sendPing);
 
     setAutoWhoDelay(networkConfig()->autoWhoDelay());
     setAutoWhoInterval(networkConfig()->autoWhoInterval());
@@ -68,66 +69,60 @@ CoreNetwork::CoreNetwork(const NetworkId &networkid, CoreSession *session)
         storeChannelCipherKey(buffer.toLower(), bufferCiphers[buffer]);
     }
 
-    connect(networkConfig(), SIGNAL(pingTimeoutEnabledSet(bool)), SLOT(enablePingTimeout(bool)));
-    connect(networkConfig(), SIGNAL(pingIntervalSet(int)), SLOT(setPingInterval(int)));
-    connect(networkConfig(), SIGNAL(autoWhoEnabledSet(bool)), SLOT(setAutoWhoEnabled(bool)));
-    connect(networkConfig(), SIGNAL(autoWhoIntervalSet(int)), SLOT(setAutoWhoInterval(int)));
-    connect(networkConfig(), SIGNAL(autoWhoDelaySet(int)), SLOT(setAutoWhoDelay(int)));
+    connect(networkConfig(), &NetworkConfig::pingTimeoutEnabledSet, this, &CoreNetwork::enablePingTimeout);
+    connect(networkConfig(), &NetworkConfig::pingIntervalSet, this, &CoreNetwork::setPingInterval);
+    connect(networkConfig(), &NetworkConfig::autoWhoEnabledSet, this, &CoreNetwork::setAutoWhoEnabled);
+    connect(networkConfig(), &NetworkConfig::autoWhoIntervalSet, this, &CoreNetwork::setAutoWhoInterval);
+    connect(networkConfig(), &NetworkConfig::autoWhoDelaySet, this, &CoreNetwork::setAutoWhoDelay);
 
-    connect(&_autoReconnectTimer, SIGNAL(timeout()), this, SLOT(doAutoReconnect()));
-    connect(&_autoWhoTimer, SIGNAL(timeout()), this, SLOT(sendAutoWho()));
-    connect(&_autoWhoCycleTimer, SIGNAL(timeout()), this, SLOT(startAutoWhoCycle()));
-    connect(&_tokenBucketTimer, SIGNAL(timeout()), this, SLOT(checkTokenBucket()));
+    connect(&_autoReconnectTimer, &QTimer::timeout, this, &CoreNetwork::doAutoReconnect);
+    connect(&_autoWhoTimer, &QTimer::timeout, this, &CoreNetwork::sendAutoWho);
+    connect(&_autoWhoCycleTimer, &QTimer::timeout, this, &CoreNetwork::startAutoWhoCycle);
+    connect(&_tokenBucketTimer, &QTimer::timeout, this, &CoreNetwork::checkTokenBucket);
 
-    connect(&socket, SIGNAL(connected()), this, SLOT(socketInitialized()));
-    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()));
+    connect(&socket, &QAbstractSocket::connected, this, &CoreNetwork::onSocketInitialized);
+    connect(&socket, selectOverload<QAbstractSocket::SocketError>(&QAbstractSocket::error), this, &CoreNetwork::onSocketError);
+    connect(&socket, &QAbstractSocket::stateChanged, this, &CoreNetwork::onSocketStateChanged);
+    connect(&socket, &QIODevice::readyRead, this, &CoreNetwork::onSocketHasData);
 #ifdef HAVE_SSL
-    connect(&socket, SIGNAL(encrypted()), this, SLOT(socketInitialized()));
-    connect(&socket, SIGNAL(sslErrors(const QList<QSslError> &)), this, SLOT(sslErrors(const QList<QSslError> &)));
+    connect(&socket, &QSslSocket::encrypted, this, &CoreNetwork::onSocketInitialized);
+    connect(&socket, selectOverload<const QList<QSslError>&>(&QSslSocket::sslErrors), this, &CoreNetwork::onSslErrors);
 #endif
-    connect(this, SIGNAL(newEvent(Event *)), coreSession()->eventManager(), SLOT(postEvent(Event *)));
+    connect(this, &CoreNetwork::newEvent, coreSession()->eventManager(), &EventManager::postEvent);
 
     // Custom rate limiting
     // These react to the user changing settings in the client
-    connect(this, SIGNAL(useCustomMessageRateSet(bool)), SLOT(updateRateLimiting()));
-    connect(this, SIGNAL(messageRateBurstSizeSet(quint32)), SLOT(updateRateLimiting()));
-    connect(this, SIGNAL(messageRateDelaySet(quint32)), SLOT(updateRateLimiting()));
-    connect(this, SIGNAL(unlimitedMessageRateSet(bool)), SLOT(updateRateLimiting()));
+    connect(this, &Network::useCustomMessageRateSet, this, &CoreNetwork::updateRateLimiting);
+    connect(this, &Network::messageRateBurstSizeSet, this, &CoreNetwork::updateRateLimiting);
+    connect(this, &Network::messageRateDelaySet, this, &CoreNetwork::updateRateLimiting);
+    connect(this, &Network::unlimitedMessageRateSet, this, &CoreNetwork::updateRateLimiting);
 
     // 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)));
+    connect(this, &Network::capAdded, this, &CoreNetwork::serverCapAdded);
+    connect(this, &Network::capAcknowledged, this, &CoreNetwork::serverCapAcknowledged);
+    connect(this, &Network::capRemoved, this, &CoreNetwork::serverCapRemoved);
 
     if (Quassel::isOptionSet("oidentd")) {
-        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)));
+        connect(this, &CoreNetwork::socketInitialized, Core::instance()->oidentdConfigGenerator(), &OidentdConfigGenerator::addSocket);
+        connect(this, &CoreNetwork::socketDisconnected, Core::instance()->oidentdConfigGenerator(), &OidentdConfigGenerator::removeSocket);
+    }
+
+    if (Quassel::isOptionSet("ident-daemon")) {
+        connect(this, &CoreNetwork::socketInitialized, Core::instance()->identServer(), &IdentServer::addSocket);
+        connect(this, &CoreNetwork::socketDisconnected, Core::instance()->identServer(), &IdentServer::removeSocket);
     }
 }
 
 
 CoreNetwork::~CoreNetwork()
 {
-    // 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() << ")";
-        }
+    // Ensure we don't get any more signals from the socket while shutting down
+    disconnect(&socket, nullptr, this, nullptr);
+    if (!forceDisconnect()) {
+        qWarning() << QString{"Could not disconnect from network %1 (network ID: %2, user ID: %3)"}
+                      .arg(networkName()).arg(networkId().toInt()).arg(userId().toInt());
     }
-    disconnect(&socket, 0, this, 0); // this keeps the socket from triggering events during clean up
-    delete _userInputHandler;
 }
 
 
@@ -139,8 +134,10 @@ bool CoreNetwork::forceDisconnect(int msecs)
     }
     // 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);
+    if (socket.state() != QAbstractSocket::UnconnectedState) {
+        return socket.waitForDisconnected(msecs);
+    }
+    return true;
 }
 
 
@@ -186,6 +183,14 @@ QByteArray CoreNetwork::userEncode(const QString &userNick, const QString &strin
 
 void CoreNetwork::connectToIrc(bool reconnecting)
 {
+    if (_shuttingDown) {
+        return;
+    }
+
+    if (Core::instance()->identServer()) {
+        _socketId = Core::instance()->identServer()->addWaitingSocket();
+    }
+
     if (!reconnecting && useAutoReconnect() && _autoReconnectCount == 0) {
         _autoReconnectTimer.setInterval(autoReconnectInterval() * 1000);
         if (unlimitedReconnectRetries())
@@ -220,7 +225,7 @@ void CoreNetwork::connectToIrc(bool reconnecting)
     else if (_previousConnectionAttemptFailed) {
         // cycle to next server if previous connection attempt failed
         _previousConnectionAttemptFailed = false;
-        displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Connection failed. Cycling to next Server"));
+        showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("Connection failed. Cycling to next Server"));
         if (++_lastUsedServerIndex >= serverList().size()) {
             _lastUsedServerIndex = 0;
         }
@@ -232,7 +237,7 @@ void CoreNetwork::connectToIrc(bool reconnecting)
 
     Server server = usedServer();
     displayStatusMsg(tr("Connecting to %1:%2...").arg(server.host).arg(server.port));
-    displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Connecting to %1:%2...").arg(server.host).arg(server.port));
+    showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("Connecting to %1:%2...").arg(server.host).arg(server.port));
 
     if (server.useProxy) {
         QNetworkProxy proxy((QNetworkProxy::ProxyType)server.proxyType, server.proxyHost, server.proxyPort, server.proxyUser, server.proxyPass);
@@ -244,6 +249,9 @@ void CoreNetwork::connectToIrc(bool reconnecting)
 
     enablePingTimeout();
 
+    // Reset tracking for valid timestamps in PONG replies
+    setPongTimestampValid(false);
+
     // Qt caches DNS entries for a minute, resulting in round-robin (e.g. for chat.freenode.net) not working if several users
     // connect at a similar time. QHostInfo::fromName(), however, always performs a fresh lookup, overwriting the cache entry.
     if (! server.useProxy) {
@@ -269,8 +277,7 @@ void CoreNetwork::connectToIrc(bool reconnecting)
 }
 
 
-void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool withReconnect,
-                                    bool forceImmediate)
+void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool withReconnect)
 {
     // Disconnecting from the network, should expect a socket close or error
     _disconnectExpected = true;
@@ -295,32 +302,50 @@ void CoreNetwork::disconnectFromIrc(bool requested, const QString &reason, bool
     else
         _quitReason = reason;
 
-    displayMsg(Message::Server, BufferInfo::StatusBuffer, "", tr("Disconnecting. (%1)").arg((!requested && !withReconnect) ? tr("Core Shutdown") : _quitReason));
+    showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("Disconnecting. (%1)").arg((!requested && !withReconnect) ? tr("Core Shutdown") : _quitReason));
     if (socket.state() == QAbstractSocket::UnconnectedState) {
-        socketDisconnected();
-    } else {
+        onSocketDisconnected();
+    }
+    else {
         if (socket.state() == QAbstractSocket::ConnectedState) {
-            userInputHandler()->issueQuit(_quitReason, forceImmediate);
-        } else {
+            // If shutting down, prioritize the QUIT command
+            userInputHandler()->issueQuit(_quitReason, _shuttingDown);
+        }
+        else {
             socket.close();
         }
-        if (requested || withReconnect) {
-            // the irc server has 10 seconds to close the socket
+        if (socket.state() != QAbstractSocket::UnconnectedState) {
+            // Wait for up to 10 seconds for the socket to close cleanly, then it will be forcefully aborted
             _socketCloseTimer.start(10000);
         }
     }
 }
 
 
-void CoreNetwork::userInput(BufferInfo buf, QString msg)
+void CoreNetwork::onSocketCloseTimeout()
+{
+    qWarning() << QString{"Timed out quitting network %1 (network ID: %2, user ID: %3)"}
+                  .arg(networkName()).arg(networkId().toInt()).arg(userId().toInt());
+    socket.abort();
+}
+
+
+void CoreNetwork::shutdown()
+{
+    _shuttingDown = true;
+    disconnectFromIrc(false, {}, false);
+}
+
+
+void CoreNetwork::userInput(const BufferInfo &buf, QString msg)
 {
     userInputHandler()->handleUserInput(buf, msg);
 }
 
 
-void CoreNetwork::putRawLine(const QByteArray s, const bool prepend)
+void CoreNetwork::putRawLine(const QByteArray &s, bool prepend)
 {
-    if (_tokenBucket > 0 || (_skipMessageRates && _msgQueue.size() == 0)) {
+    if (_tokenBucket > 0 || (_skipMessageRates && _msgQueue.isEmpty())) {
         // If there's tokens remaining, ...
         // Or rate limits don't apply AND no messages are in queue (to prevent out-of-order), ...
         // Send the message now.
@@ -409,32 +434,32 @@ void CoreNetwork::removeChannelKey(const QString &channel)
 Cipher *CoreNetwork::cipher(const QString &target)
 {
     if (target.isEmpty())
-        return 0;
+        return nullptr;
 
     if (!Cipher::neededFeaturesAvailable())
-        return 0;
+        return nullptr;
 
-    CoreIrcChannel *channel = qobject_cast<CoreIrcChannel *>(ircChannel(target));
+    auto *channel = qobject_cast<CoreIrcChannel *>(ircChannel(target));
     if (channel) {
         return channel->cipher();
     }
-    CoreIrcUser *user = qobject_cast<CoreIrcUser *>(ircUser(target));
+    auto *user = qobject_cast<CoreIrcUser *>(ircUser(target));
     if (user) {
         return user->cipher();
     } else if (!isChannelName(target)) {
         return qobject_cast<CoreIrcUser*>(newIrcUser(target))->cipher();
     }
-    return 0;
+    return nullptr;
 }
 
 
 QByteArray CoreNetwork::cipherKey(const QString &target) const
 {
-    CoreIrcChannel *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+    auto *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
     if (c)
         return c->cipher()->key();
 
-    CoreIrcUser *u = qobject_cast<CoreIrcUser*>(ircUser(target));
+    auto *u = qobject_cast<CoreIrcUser*>(ircUser(target));
     if (u)
         return u->cipher()->key();
 
@@ -444,14 +469,14 @@ QByteArray CoreNetwork::cipherKey(const QString &target) const
 
 void CoreNetwork::setCipherKey(const QString &target, const QByteArray &key)
 {
-    CoreIrcChannel *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+    auto *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
     if (c) {
         c->setEncrypted(c->cipher()->setKey(key));
         coreSession()->setBufferCipher(networkId(), target, key);
         return;
     }
 
-    CoreIrcUser *u = qobject_cast<CoreIrcUser*>(ircUser(target));
+    auto *u = qobject_cast<CoreIrcUser*>(ircUser(target));
     if (!u && !isChannelName(target))
         u = qobject_cast<CoreIrcUser*>(newIrcUser(target));
 
@@ -465,10 +490,10 @@ void CoreNetwork::setCipherKey(const QString &target, const QByteArray &key)
 
 bool CoreNetwork::cipherUsesCBC(const QString &target)
 {
-    CoreIrcChannel *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+    auto *c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
     if (c)
         return c->cipher()->usesCBC();
-    CoreIrcUser *u = qobject_cast<CoreIrcUser*>(ircUser(target));
+    auto *u = qobject_cast<CoreIrcUser*>(ircUser(target));
     if (u)
         return u->cipher()->usesCBC();
 
@@ -476,13 +501,13 @@ bool CoreNetwork::cipherUsesCBC(const QString &target)
 }
 #endif /* HAVE_QCA2 */
 
-bool CoreNetwork::setAutoWhoDone(const QString &channel)
+bool CoreNetwork::setAutoWhoDone(const QString &name)
 {
-    QString chan = channel.toLower();
-    if (_autoWhoPending.value(chan, 0) <= 0)
+    QString chanOrNick = name.toLower();
+    if (_autoWhoPending.value(chanOrNick, 0) <= 0)
         return false;
-    if (--_autoWhoPending[chan] <= 0)
-        _autoWhoPending.remove(chan);
+    if (--_autoWhoPending[chanOrNick] <= 0)
+        _autoWhoPending.remove(chanOrNick);
     return true;
 }
 
@@ -495,7 +520,7 @@ void CoreNetwork::setMyNick(const QString &mynick)
 }
 
 
-void CoreNetwork::socketHasData()
+void CoreNetwork::onSocketHasData()
 {
     while (socket.canReadLine()) {
         QByteArray s = socket.readLine();
@@ -510,7 +535,7 @@ void CoreNetwork::socketHasData()
 }
 
 
-void CoreNetwork::socketError(QAbstractSocket::SocketError error)
+void CoreNetwork::onSocketError(QAbstractSocket::SocketError error)
 {
     // Ignore socket closed errors if expected
     if (_disconnectExpected && error == QAbstractSocket::RemoteHostClosedError) {
@@ -520,15 +545,15 @@ void CoreNetwork::socketError(QAbstractSocket::SocketError error)
     _previousConnectionAttemptFailed = true;
     qWarning() << qPrintable(tr("Could not connect to %1 (%2)").arg(networkName(), socket.errorString()));
     emit connectionError(socket.errorString());
-    displayMsg(Message::Error, BufferInfo::StatusBuffer, "", tr("Connection failure: %1").arg(socket.errorString()));
+    showMessage(Message::Error, BufferInfo::StatusBuffer, "", tr("Connection failure: %1").arg(socket.errorString()));
     emitConnectionError(socket.errorString());
     if (socket.state() < QAbstractSocket::ConnectedState) {
-        socketDisconnected();
+        onSocketDisconnected();
     }
 }
 
 
-void CoreNetwork::socketInitialized()
+void CoreNetwork::onSocketInitialized()
 {
     CoreIdentity *identity = identityPtr();
     if (!identity) {
@@ -543,7 +568,7 @@ void CoreNetwork::socketInitialized()
     // 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());
+        emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort(), _socketId);
     }
 
     if (server.useSsl && !socket.isEncrypted()) {
@@ -551,7 +576,7 @@ void CoreNetwork::socketInitialized()
         return;
     }
 #else
-    emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort());
+    emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort(), _socketId);
 #endif
 
     socket.setSocketOption(QAbstractSocket::KeepAliveOption, true);
@@ -568,7 +593,7 @@ void CoreNetwork::socketInitialized()
 
     // 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..."));
+    showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("Requesting capability list..."));
     putRawLine(serverEncode(QString("CAP LS 302")));
 
     if (!server.password.isEmpty()) {
@@ -583,11 +608,14 @@ void CoreNetwork::socketInitialized()
         nick = identity->nicks()[0];
     }
     putRawLine(serverEncode(QString("NICK %1").arg(nick)));
-    putRawLine(serverEncode(QString("USER %1 8 * :%2").arg(identity->ident(), identity->realName())));
+    // Only allow strict-compliant idents when strict mode is enabled
+    putRawLine(serverEncode(QString("USER %1 8 * :%2").arg(
+                                coreSession()->strictCompliantIdent(identity),
+                                identity->realName())));
 }
 
 
-void CoreNetwork::socketDisconnected()
+void CoreNetwork::onSocketDisconnected()
 {
     disablePingTimeout();
     _msgQueue.clear();
@@ -604,12 +632,12 @@ void CoreNetwork::socketDisconnected()
     IrcUser *me_ = me();
     if (me_) {
         foreach(QString channel, me_->channels())
-        displayMsg(Message::Quit, BufferInfo::ChannelBuffer, channel, _quitReason, me_->hostmask());
+        showMessage(Message::Quit, BufferInfo::ChannelBuffer, channel, _quitReason, me_->hostmask());
     }
 
     setConnected(false);
     emit disconnected(networkId());
-    emit socketDisconnected(identityPtr(), localAddress(), localPort(), peerAddress(), peerPort());
+    emit socketDisconnected(identityPtr(), localAddress(), localPort(), peerAddress(), peerPort(), _socketId);
     // Reset disconnect expectations
     _disconnectExpected = false;
     if (_quitRequested) {
@@ -627,13 +655,13 @@ void CoreNetwork::socketDisconnected()
 }
 
 
-void CoreNetwork::socketStateChanged(QAbstractSocket::SocketState socketState)
+void CoreNetwork::onSocketStateChanged(QAbstractSocket::SocketState socketState)
 {
     Network::ConnectionState state;
     switch (socketState) {
     case QAbstractSocket::UnconnectedState:
         state = Network::Disconnected;
-        socketDisconnected();
+        onSocketDisconnected();
         break;
     case QAbstractSocket::HostLookupState:
     case QAbstractSocket::ConnectingState:
@@ -707,8 +735,8 @@ void CoreNetwork::sendPerform()
             restoreUserModes();
         }
         else {
-            connect(me_, SIGNAL(userModesSet(QString)), this, SLOT(restoreUserModes()));
-            connect(me_, SIGNAL(userModesAdded(QString)), this, SLOT(restoreUserModes()));
+            connect(me_, &IrcUser::userModesSet, this, &CoreNetwork::restoreUserModes);
+            connect(me_, &IrcUser::userModesAdded, this, &CoreNetwork::restoreUserModes);
         }
     }
 
@@ -742,8 +770,8 @@ void CoreNetwork::restoreUserModes()
     IrcUser *me_ = me();
     Q_ASSERT(me_);
 
-    disconnect(me_, SIGNAL(userModesSet(QString)), this, SLOT(restoreUserModes()));
-    disconnect(me_, SIGNAL(userModesAdded(QString)), this, SLOT(restoreUserModes()));
+    disconnect(me_, &IrcUser::userModesSet, this, &CoreNetwork::restoreUserModes);
+    disconnect(me_, &IrcUser::userModesAdded, this, &CoreNetwork::restoreUserModes);
 
     QString modesDelta = Core::userModes(userId(), networkId());
     QString currentModes = me_->userModes();
@@ -782,20 +810,20 @@ void CoreNetwork::updateIssuedModes(const QString &requestedModes)
     QString removeModes;
     bool addMode = true;
 
-    for (int i = 0; i < requestedModes.length(); i++) {
-        if (requestedModes[i] == '+') {
+    for (auto requestedMode : requestedModes) {
+        if (requestedMode == '+') {
             addMode = true;
             continue;
         }
-        if (requestedModes[i] == '-') {
+        if (requestedMode == '-') {
             addMode = false;
             continue;
         }
         if (addMode) {
-            addModes += requestedModes[i];
+            addModes += requestedMode;
         }
         else {
-            removeModes += requestedModes[i];
+            removeModes += requestedMode;
         }
     }
 
@@ -908,12 +936,17 @@ void CoreNetwork::doAutoReconnect()
 
 void CoreNetwork::sendPing()
 {
-    uint now = QDateTime::currentDateTime().toTime_t();
+    qint64 now = QDateTime::currentDateTime().toMSecsSinceEpoch();
     if (_pingCount != 0) {
         qDebug() << "UserId:" << userId() << "Network:" << networkName() << "missed" << _pingCount << "pings."
                  << "BA:" << socket.bytesAvailable() << "BTW:" << socket.bytesToWrite();
     }
-    if ((int)_pingCount >= networkConfig()->maxPingCount() && now - _lastPingTime <= (uint)(_pingTimer.interval() / 1000) + 1) {
+    if ((int)_pingCount >= networkConfig()->maxPingCount()
+            && (now - _lastPingTime) <= (_pingTimer.interval() + (1 * 1000))) {
+        // In transitioning to 64-bit time, the interval no longer needs converted down to seconds.
+        // However, to reduce the risk of breaking things by changing past behavior, we still allow
+        // up to 1 second missed instead of enforcing a stricter 1 millisecond allowance.
+        //
         // the second check compares the actual elapsed time since the last ping and the pingTimer interval
         // if the interval is shorter then the actual elapsed time it means that this thread was somehow blocked
         // and unable to even handle a ping answer. So we ignore those misses.
@@ -923,8 +956,12 @@ void CoreNetwork::sendPing()
         _lastPingTime = now;
         _pingCount++;
         // Don't send pings until the network is initialized
-        if(_sendPings)
+        if(_sendPings) {
+            // Mark as waiting for a reply
+            _pongReplyPending = true;
+            // Send default timestamp ping
             userInputHandler()->handlePing(BufferInfo(), QString());
+        }
     }
 }
 
@@ -935,6 +972,7 @@ void CoreNetwork::enablePingTimeout(bool enable)
         disablePingTimeout();
     else {
         resetPingTimeout();
+        resetPongReplyPending();
         if (networkConfig()->pingTimeoutEnabled())
             _pingTimer.start();
     }
@@ -946,6 +984,7 @@ void CoreNetwork::disablePingTimeout()
     _pingTimer.stop();
     _sendPings = false;
     resetPingTimeout();
+    resetPongReplyPending();
 }
 
 
@@ -955,6 +994,12 @@ void CoreNetwork::setPingInterval(int interval)
 }
 
 
+void CoreNetwork::setPongTimestampValid(bool validTimestamp)
+{
+    _pongTimestampValid = validTimestamp;
+}
+
+
 /******** Custom Rate Limiting ********/
 
 void CoreNetwork::updateRateLimiting(const bool forceUnlimited)
@@ -992,7 +1037,7 @@ void CoreNetwork::updateRateLimiting(const bool forceUnlimited)
         if (_skipMessageRates) {
             // If the message queue already contains messages, they need sent before disabling the
             // timer.  Set the timer to a rapid pace and let it disable itself.
-            if (_msgQueue.size() > 0) {
+            if (!_msgQueue.isEmpty()) {
                 qDebug() << "Outgoing message queue contains messages while disabling rate "
                             "limiting.  Sending remaining queued messages...";
                 // Promptly run the timer again to clear the messages.  Rate limiting is disabled,
@@ -1074,8 +1119,7 @@ void CoreNetwork::serverCapAcknowledged(const QString &capability)
                 // EXTERNAL authentication supported, send request
                 putRawLine(serverEncode("AUTHENTICATE EXTERNAL"));
             } else {
-                displayMsg(Message::Error, BufferInfo::StatusBuffer, "",
-                           tr("SASL EXTERNAL authentication not supported"));
+                showMessage(Message::Error, BufferInfo::StatusBuffer, "", tr("SASL EXTERNAL authentication not supported"));
                 sendNextCap();
             }
         } else {
@@ -1085,8 +1129,7 @@ void CoreNetwork::serverCapAcknowledged(const QString &capability)
                 // Only working with PLAIN atm, blowfish later
                 putRawLine(serverEncode("AUTHENTICATE PLAIN"));
             } else {
-                displayMsg(Message::Error, BufferInfo::StatusBuffer, "",
-                           tr("SASL PLAIN authentication not supported"));
+                showMessage(Message::Error, BufferInfo::StatusBuffer, "", tr("SASL PLAIN authentication not supported"));
                 sendNextCap();
             }
 #ifdef HAVE_SSL
@@ -1191,9 +1234,9 @@ void CoreNetwork::retryCapsIndividually()
     // 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(", ")));
+    showMessage(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
@@ -1207,14 +1250,13 @@ void CoreNetwork::beginCapNegotiation()
     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"));
+        showMessage(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(", ")));
+    showMessage(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 (both individual and bundled caps exist).
@@ -1222,8 +1264,7 @@ void CoreNetwork::beginCapNegotiation()
             _capsQueuedIndividual.join(", ")
             + ((!_capsQueuedIndividual.empty() && !_capsQueuedBundled.empty()) ? ", " : "")
             + _capsQueuedBundled.join(", ");
-    displayMsg(Message::Server, BufferInfo::StatusBuffer, "",
-               tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay));
+    showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay));
 
     sendNextCap();
 }
@@ -1237,12 +1278,10 @@ void CoreNetwork::sendNextCap()
         // 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"));
+            showMessage(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(", ")));
+            showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", ")));
             _capNegotiationActive = false;
         }
 
@@ -1270,12 +1309,12 @@ void CoreNetwork::startAutoWhoCycle()
     _autoWhoQueue = channels();
 }
 
-void CoreNetwork::queueAutoWhoOneshot(const QString &channelOrNick)
+void CoreNetwork::queueAutoWhoOneshot(const QString &name)
 {
     // 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 (!_autoWhoQueue.contains(name.toLower())) {
+        _autoWhoQueue.prepend(name.toLower());
     }
     if (capEnabled(IrcCap::AWAY_NOTIFY)) {
         // When away-notify is active, the timer's stopped.  Start a new cycle to who this channel.
@@ -1337,11 +1376,23 @@ void CoreNetwork::sendAutoWho()
         }
         if (supports("WHOX")) {
             // Use WHO extended to poll away users and/or user accounts
+            // Explicitly only match on nickname ("n"), don't rely on server defaults
+            //
+            // WHO <nickname> n%chtsunfra,<unique_number>
+            //
             // 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))));
+            // And https://github.com/quakenet/snircd/blob/master/doc/readme.who
+            // And https://github.com/hexchat/hexchat/blob/57478b65758e6b697b1d82ce21075e74aa475efc/src/common/proto-irc.c#L752
+            putRawLine(serverEncode(QString("WHO %1 n%chtsunfra,%2")
+                                    .arg(serverEncode(chanOrNick),
+                                         QString::number(IrcCap::ACCOUNT_NOTIFY_WHOX_NUM))));
         } else {
+            // Fall back to normal WHO
+            //
+            // Note: According to RFC 1459, "WHO <phrase>" can fall back to searching realname,
+            // hostmask, etc.  There's nothing we can do about that :(
+            //
+            // See https://tools.ietf.org/html/rfc1459#section-4.5.1
             putRawLine(serverEncode(QString("WHO %1").arg(chanOrNick)));
         }
         break;
@@ -1361,7 +1412,7 @@ void CoreNetwork::sendAutoWho()
 
 
 #ifdef HAVE_SSL
-void CoreNetwork::sslErrors(const QList<QSslError> &sslErrors)
+void CoreNetwork::onSslErrors(const QList<QSslError> &sslErrors)
 {
     Server server = usedServer();
     if (server.sslVerify) {
@@ -1372,7 +1423,7 @@ void CoreNetwork::sslErrors(const QList<QSslError> &sslErrors)
             // Add the error reason if known
             sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
         }
-        displayMsg(Message::Error, BufferInfo::StatusBuffer, "", sslErrorMessage);
+        showMessage(Message::Error, BufferInfo::StatusBuffer, "", sslErrorMessage);
 
         // Disconnect, triggering a reconnect in case it's a temporary issue with certificate
         // validity, network trouble, etc.
@@ -1385,7 +1436,7 @@ void CoreNetwork::sslErrors(const QList<QSslError> &sslErrors)
             // Add the error reason if known
             sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
         }
-        displayMsg(Message::Info, BufferInfo::StatusBuffer, "", sslErrorMessage);
+        showMessage(Message::Info, BufferInfo::StatusBuffer, "", sslErrorMessage);
 
         // Proceed with the connection
         socket.ignoreSslErrors();
@@ -1427,6 +1478,12 @@ void CoreNetwork::fillBucketAndProcessQueue()
 
 void CoreNetwork::writeToSocket(const QByteArray &data)
 {
+    // Log the message if enabled and network ID matches or allows all
+    if (_debugLogRawIrc
+            && (_debugLogRawNetId == -1 || networkId().toInt() == _debugLogRawNetId)) {
+        // Include network ID
+        qDebug() << "IRC net" << networkId() << ">>" << data;
+    }
     socket.write(data);
     socket.write("\r\n");
     if (!_skipMessageRates) {
@@ -1450,6 +1507,9 @@ Network::Server CoreNetwork::usedServer() const
 
 void CoreNetwork::requestConnect() const
 {
+    if (_shuttingDown) {
+        return;
+    }
     if (connectionState() != Disconnected) {
         qWarning() << "Requesting connect while already being connected!";
         return;
@@ -1460,6 +1520,9 @@ void CoreNetwork::requestConnect() const
 
 void CoreNetwork::requestDisconnect() const
 {
+    if (_shuttingDown) {
+        return;
+    }
     if (connectionState() == Disconnected) {
         qWarning() << "Requesting disconnect while not being connected!";
         return;
@@ -1487,7 +1550,7 @@ void CoreNetwork::requestSetNetworkInfo(const NetworkInfo &info)
 }
 
 
-QList<QList<QByteArray>> CoreNetwork::splitMessage(const QString &cmd, const QString &message, std::function<QList<QByteArray>(QString &)> cmdGenerator)
+QList<QList<QByteArray>> CoreNetwork::splitMessage(const QString &cmd, const QString &message, const std::function<QList<QByteArray>(QString &)> &cmdGenerator)
 {
     QString wrkMsg(message);
     QList<QList<QByteArray>> msgsToSend;