common: Port HighlightRule to ExpressionMatch
[quassel.git] / src / common / network.cpp
index 4e14af4..dcca4f4 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2016 by the Quassel Project                        *
+ *   Copyright (C) 2005-2018 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
+#include <algorithm>
+
 #include <QTextCodec>
 
 #include "network.h"
+#include "peer.h"
 
 QTextCodec *Network::_defaultCodecForServer = 0;
 QTextCodec *Network::_defaultCodecForEncoding = 0;
@@ -176,6 +179,42 @@ QString Network::modeToPrefix(const QString &mode) const
 }
 
 
+QString Network::sortPrefixModes(const QString &modes) const
+{
+    // If modes is empty or we don't have any modes, nothing can be sorted, bail out early
+    if (modes.isEmpty() || prefixModes().isEmpty()) {
+        return modes;
+    }
+
+    // Store a copy of the modes for modification
+    // QString should be efficient and not copy memory if nothing changes, but if mistaken,
+    // std::is_sorted could be called first.
+    QString sortedModes = QString(modes);
+
+    // Sort modes as if a QChar array
+    // See https://en.cppreference.com/w/cpp/algorithm/sort
+    // Defining lambda with [&] implicitly captures variables by reference
+    std::sort(sortedModes.begin(), sortedModes.end(), [&](const QChar &lmode, const QChar &rmode) {
+        // Compare characters according to prefix modes
+        // Return true if lmode comes before rmode (is "less than")
+
+        // Check for unknown modes...
+        if (!prefixModes().contains(lmode)) {
+            // Left mode not in prefix list, send to end
+            return false;
+        } else if (!prefixModes().contains(rmode)) {
+            // Right mode not in prefix list, send to end
+            return true;
+        } else {
+            // Both characters known, sort according to index in prefixModes()
+            return (prefixModes().indexOf(lmode) < prefixModes().indexOf(rmode));
+        }
+    });
+
+    return sortedModes;
+}
+
+
 QStringList Network::nicks() const
 {
     // we don't use _ircUsers.keys() since the keys may be
@@ -241,6 +280,28 @@ QString Network::support(const QString &param) const
 }
 
 
+bool Network::saslMaybeSupports(const QString &saslMechanism) const
+{
+    if (!capAvailable(IrcCap::SASL)) {
+        // If SASL's not advertised at all, it's likely the mechanism isn't supported, as per specs.
+        // Unfortunately, we don't know for sure, but Quassel won't request SASL without it being
+        // advertised, anyways.
+        // This may also occur if the network's disconnected or negotiation hasn't yet happened.
+        return false;
+    }
+
+    // Get the SASL capability value
+    QString saslCapValue = capValue(IrcCap::SASL);
+    // SASL mechanisms are only specified in capability values as part of SASL 3.2.  In SASL 3.1,
+    // it's handled differently.  If we don't know via capability value, assume it's supported to
+    // reduce the risk of breaking existing setups.
+    // See: http://ircv3.net/specs/extensions/sasl-3.1.html
+    // And: http://ircv3.net/specs/extensions/sasl-3.2.html
+    return (saslCapValue.length() == 0)
+            || (saslCapValue.contains(saslMechanism, Qt::CaseInsensitive));
+}
+
+
 IrcUser *Network::newIrcUser(const QString &hostmask, const QVariantMap &initData)
 {
     QString nick(nickFromMask(hostmask).toLower());
@@ -707,9 +768,10 @@ void Network::setUseCustomMessageRate(bool useCustomRate)
 void Network::setMessageRateBurstSize(quint32 burstSize)
 {
     if (burstSize < 1) {
-        // Can't go slower than one message at a time
-        qWarning() << "Received invalid setMessageRateBurstSize data, cannot have zero message "
-                      "burst size!" << burstSize;
+        // Can't go slower than one message at a time.  Also blocks old clients from trying to set
+        // this to 0.
+        qDebug() << "Received invalid setMessageRateBurstSize data - message burst size must be "
+                    "non-zero positive, given" << burstSize;
         return;
     }
     if (_messageRateBurstSize != burstSize) {
@@ -723,6 +785,13 @@ void Network::setMessageRateBurstSize(quint32 burstSize)
 
 void Network::setMessageRateDelay(quint32 messageDelay)
 {
+    if (messageDelay == 0) {
+        // Nonsensical to have no delay - just check the Unlimited box instead.  Also blocks old
+        // clients from trying to set this to 0.
+        qDebug() << "Received invalid setMessageRateDelay data - message delay must be non-zero "
+                    "positive, given" << messageDelay;
+        return;
+    }
     if (_messageRateDelay != messageDelay) {
         _messageRateDelay = messageDelay;
         SYNC(ARG(messageDelay))
@@ -851,6 +920,8 @@ QVariantMap Network::initCaps() const
 // (without compression) with a decent amount of IrcUsers.
 QVariantMap Network::initIrcUsersAndChannels() const
 {
+    Q_ASSERT(proxy());
+    Q_ASSERT(proxy()->targetPeer());
     QVariantMap usersAndChannels;
 
     if (_ircUsers.count()) {
@@ -858,7 +929,21 @@ QVariantMap Network::initIrcUsersAndChannels() const
         QHash<QString, IrcUser *>::const_iterator it = _ircUsers.begin();
         QHash<QString, IrcUser *>::const_iterator end = _ircUsers.end();
         while (it != end) {
-            const QVariantMap &map = it.value()->toVariantMap();
+            QVariantMap map = it.value()->toVariantMap();
+            // If the peer doesn't support LongTime, replace the lastAwayMessageTime field
+            // with the 32-bit numerical seconds value (lastAwayMessage) used in older versions
+            if (!proxy()->targetPeer()->hasFeature(Quassel::Feature::LongTime)) {
+#if QT_VERSION >= 0x050800
+                int lastAwayMessage = it.value()->lastAwayMessageTime().toSecsSinceEpoch();
+#else
+                // toSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for now.
+                // See https://doc.qt.io/qt-5/qdatetime.html#toMSecsSinceEpoch
+                int lastAwayMessage = it.value()->lastAwayMessageTime().toMSecsSinceEpoch() / 1000;
+#endif
+                map.remove("lastAwayMessageTime");
+                map["lastAwayMessage"] = lastAwayMessage;
+            }
+
             QVariantMap::const_iterator mapiter = map.begin();
             while (mapiter != map.end()) {
                 users[mapiter.key()] << mapiter.value();
@@ -900,6 +985,7 @@ QVariantMap Network::initIrcUsersAndChannels() const
 void Network::initSetIrcUsersAndChannels(const QVariantMap &usersAndChannels)
 {
     Q_ASSERT(proxy());
+    Q_ASSERT(proxy()->sourcePeer());
     if (isInitialized()) {
         qWarning() << "Network" << networkId() << "received init data for users and channels although there already are known users or channels!";
         return;
@@ -924,6 +1010,22 @@ void Network::initSetIrcUsersAndChannels(const QVariantMap &usersAndChannels)
         QVariantMap map;
         foreach(const QString &key, users.keys())
             map[key] = users[key].toList().at(i);
+
+        // If the peer doesn't support LongTime, upconvert the lastAwayMessageTime field
+        // from the 32-bit numerical seconds value used in older versions to QDateTime
+        if (!proxy()->sourcePeer()->hasFeature(Quassel::Feature::LongTime)) {
+            QDateTime lastAwayMessageTime = QDateTime();
+            lastAwayMessageTime.setTimeSpec(Qt::UTC);
+#if QT_VERSION >= 0x050800
+            lastAwayMessageTime.fromSecsSinceEpoch(map.take("lastAwayMessage").toInt());
+#else
+            // toSecsSinceEpoch() was added in Qt 5.8.  Manually downconvert to seconds for now.
+            // See https://doc.qt.io/qt-5/qdatetime.html#toMSecsSinceEpoch
+            lastAwayMessageTime.fromMSecsSinceEpoch(map.take("lastAwayMessage").toInt() * 1000);
+#endif
+            map["lastAwayMessageTime"] = lastAwayMessageTime;
+        }
+
         newIrcUser(map["nick"].toString(), map); // newIrcUser() properly handles the hostmask being just the nick
     }