info.serverList = serverList();
info.useRandomServer = useRandomServer();
info.perform = perform();
+ info.skipCaps = skipCaps();
info.useAutoIdentify = useAutoIdentify();
info.autoIdentifyService = autoIdentifyService();
info.autoIdentifyPassword = autoIdentifyPassword();
setUseRandomServer(info.useRandomServer);
if (info.perform != perform())
setPerform(info.perform);
+ if (info.skipCaps != skipCaps())
+ setSkipCaps(info.skipCaps);
if (info.useAutoIdentify != useAutoIdentify())
setUseAutoIdentify(info.useAutoIdentify);
if (info.autoIdentifyService != autoIdentifyService())
emit configChanged();
}
+void Network::setSkipCaps(const QStringList& skipCaps)
+{
+ _skipCaps = skipCaps;
+ // Ensure the list of skipped capabilities remains sorted
+ //
+ // This becomes important in CoreNetwork::beginCapNegotiation() when finding the intersection of
+ // available capabilities and skipped capabilities. It's a bit more efficient to sort on first
+ // initialization and changes afterwards instead of on every (re)connection to the IRC network.
+ _skipCaps.sort();
+ SYNC(ARG(skipCaps))
+ emit configChanged();
+}
+
void Network::setUseAutoIdentify(bool use)
{
_useAutoIdentify = use;
* NetworkInfo
************************************************************************/
+QString NetworkInfo::skipCapsToString() const {
+ // Sort the list of capabilities when rendering to a string. This isn't required as
+ // Network::setSkipCaps() will sort as well, but this looks nicer when displayed to the user.
+ // This also results in the list being sorted before storing in the database, too.
+ auto sortedSkipCaps = skipCaps;
+ sortedSkipCaps.sort();
+
+ // IRCv3 capabilities are transmitted space-separated, so it should be safe to assume spaces
+ // won't ever be inside them
+ //
+ // See https://ircv3.net/specs/core/capability-negotiation
+ return sortedSkipCaps.join(" ");
+}
+
+void NetworkInfo::skipCapsFromString(const QString& flattenedSkipCaps) {
+ // IRCv3 capabilities should all use lowercase capability names, though it's not strictly
+ // required by the specification. Quassel currently converts all caps to lowercase before doing
+ // any comparisons.
+ //
+ // This would only become an issue if two capabilities have the same name and only differ by
+ // case, or if an IRC server transmits an uppercase capability and compares case-sensitively.
+ //
+ // (QString::toLower() is always done in the C locale, so locale-dependent case-sensitivity
+ // won't ever be an issue, thankfully.)
+ //
+ // See Network::addCap(), Network::acknowledgeCap(), and friends
+ // And https://ircv3.net/specs/core/capability-negotiation
+ skipCaps = flattenedSkipCaps.toLower().split(" ", QString::SplitBehavior::SkipEmptyParts);
+}
+
bool NetworkInfo::operator==(const NetworkInfo& other) const
{
return networkName == other.networkName
&& serverList == other.serverList
&& perform == other.perform
+ && skipCaps == other.skipCaps
&& autoIdentifyService == other.autoIdentifyService
&& autoIdentifyPassword == other.autoIdentifyPassword
&& saslAccount == other.saslAccount
i["NetworkName"] = info.networkName;
i["ServerList"] = toVariantList(info.serverList);
i["Perform"] = info.perform;
+ i["SkipCaps"] = info.skipCaps;
i["AutoIdentifyService"] = info.autoIdentifyService;
i["AutoIdentifyPassword"] = info.autoIdentifyPassword;
i["SaslAccount"] = info.saslAccount;
info.networkName = i["NetworkName"].toString();
info.serverList = fromVariantList<Network::Server>(i["ServerList"].toList());
info.perform = i["Perform"].toStringList();
+ info.skipCaps = i["SkipCaps"].toStringList();
info.autoIdentifyService = i["AutoIdentifyService"].toString();
info.autoIdentifyPassword = i["AutoIdentifyPassword"].toString();
info.saslAccount = i["SaslAccount"].toString();
dbg.nospace() << "(id = " << i.networkId << " name = " << i.networkName << " identity = " << i.identity
<< " codecForServer = " << i.codecForServer << " codecForEncoding = " << i.codecForEncoding
<< " codecForDecoding = " << i.codecForDecoding << " serverList = " << i.serverList
- << " useRandomServer = " << i.useRandomServer << " perform = " << i.perform << " useAutoIdentify = " << i.useAutoIdentify
+ << " useRandomServer = " << i.useRandomServer << " perform = " << i.perform
+ << " skipCaps = " << i.skipCaps << " useAutoIdentify = " << i.useAutoIdentify
<< " autoIdentifyService = " << i.autoIdentifyService << " autoIdentifyPassword = " << i.autoIdentifyPassword
<< " useSasl = " << i.useSasl << " saslAccount = " << i.saslAccount << " saslPassword = " << i.saslPassword
<< " useAutoReconnect = " << i.useAutoReconnect << " autoReconnectInterval = " << i.autoReconnectInterval
Q_PROPERTY(int connectionState READ connectionState WRITE setConnectionState)
Q_PROPERTY(bool useRandomServer READ useRandomServer WRITE setUseRandomServer)
Q_PROPERTY(QStringList perform READ perform WRITE setPerform)
+ Q_PROPERTY(QStringList skipCaps READ skipCaps WRITE setSkipCaps)
Q_PROPERTY(bool useAutoIdentify READ useAutoIdentify WRITE setUseAutoIdentify)
Q_PROPERTY(QString autoIdentifyService READ autoIdentifyService WRITE setAutoIdentifyService)
Q_PROPERTY(QString autoIdentifyPassword READ autoIdentifyPassword WRITE setAutoIdentifyPassword)
inline const ServerList& serverList() const { return _serverList; }
inline bool useRandomServer() const { return _useRandomServer; }
inline const QStringList& perform() const { return _perform; }
+ /**
+ * Gets the list of skipped (not auto-negotiated) capabilities.
+ *
+ * @returns QStringList of skippped capabilities
+ */
+ inline const QStringList skipCaps() const { return _skipCaps; }
inline bool useAutoIdentify() const { return _useAutoIdentify; }
inline const QString& autoIdentifyService() const { return _autoIdentifyService; }
inline const QString& autoIdentifyPassword() const { return _autoIdentifyPassword; }
void setServerList(const QVariantList& serverList);
void setUseRandomServer(bool);
void setPerform(const QStringList&);
+ /**
+ * Sets the list of skipped (not auto-negotiated) capabilities
+ *
+ * @param skipCaps QStringList of skipped (not auto-negotiated) capabilities
+ */
+ void setSkipCaps(const QStringList& skipCaps);
void setUseAutoIdentify(bool);
void setAutoIdentifyService(const QString&);
void setAutoIdentifyPassword(const QString&);
/**
* Clears all capabilities from the list of available capabilities.
*
- * This also removes the capability from the list of acknowledged capabilities.
+ * This also removes all capabilities from the list of acknowledged capabilities.
*/
void clearCaps();
/**
* Initialize the list of enabled (acknowledged) capabilities.
*
- * @param[in] caps QVariantList of QString indicating enabled (acknowledged) capabilities and values
+ * @param[in] capsEnabled QVariantList of QString indicating enabled (acknowledged) capabilities
*/
inline void initSetCapsEnabled(const QVariantList& capsEnabled) { _capsEnabled = fromVariantList<QString>(capsEnabled); }
inline void initSetServerList(const QVariantList& serverList) { _serverList = fromVariantList<Server>(serverList); }
QStringList _capsEnabled; /// Enabled capabilities that received 'CAP ACK'
// _capsEnabled uses the same values from the <name>=<value> pairs stored in _caps
+
ServerList _serverList;
bool _useRandomServer;
QStringList _perform;
+ QStringList _skipCaps; ///< Capabilities to skip during negotiation (keep list sorted!)
bool _useAutoIdentify;
QString _autoIdentifyService;
Network::ServerList serverList;
QStringList perform;
+ QStringList skipCaps; ///< Capabilities to skip during negotiation
QString autoIdentifyService{"NickServ"};
QString autoIdentifyPassword;
public:
bool operator==(const NetworkInfo& other) const;
bool operator!=(const NetworkInfo& other) const;
+
+ /**
+ * Gets the list of skipped capabilities in a space-separated string
+ *
+ * @see skipCaps
+ * @return QString representing skipCaps with each cap separated by a space
+ */
+ QString skipCapsToString() const;
+ /**
+ * Sets the list of skipped capabilities from a space-separated string
+ *
+ * @param flattenedSkipCaps QString representing skipCaps with each cap separated by a space
+ */
+ void skipCapsFromString(const QString& flattenedSkipCaps);
};
COMMON_EXPORT QDataStream& operator<<(QDataStream& out, const NetworkInfo& info);
LongMessageId, ///< 64-bit IDs for messages
SyncedCoreInfo, ///< CoreInfo dynamically updated using signals
LoadBacklogForwards, ///< Allow loading backlog in ascending order, old to new
+ SkipIrcCaps, ///< Control what IRCv3 capabilities are skipped during negotiation
};
Q_ENUMS(Feature)
info.serverList = fromVariantList<Network::Server>(i["ServerList"].toList());
info.useRandomServer = i["UseRandomServer"].toBool();
info.perform = i["Perform"].toStringList();
+ info.skipCaps = i["SkipCaps"].toStringList();
info.useAutoIdentify = i["UseAutoIdentify"].toBool();
info.autoIdentifyService = i["AutoIdentifyService"].toString();
info.autoIdentifyPassword = i["AutoIdentifyPassword"].toString();
autoidentifypassword, useautoreconnect, autoreconnectinterval,
autoreconnectretries, unlimitedconnectretries, rejoinchannels, usesasl,
saslaccount, saslpassword, usecustomessagerate, messagerateburstsize,
- messageratedelay, unlimitedmessagerate)
+ messageratedelay, unlimitedmessagerate, skipcaps)
VALUES (:userid, :networkname, :identityid, :servercodec, :encodingcodec, :decodingcodec,
:userandomserver, :perform, :useautoidentify, :autoidentifyservice, :autoidentifypassword,
:useautoreconnect, :autoreconnectinterval, :autoreconnectretries, :unlimitedconnectretries,
:rejoinchannels, :usesasl, :saslaccount, :saslpassword, :usecustomessagerate,
- :messagerateburstsize, :messageratedelay, :unlimitedmessagerate)
+ :messagerateburstsize, :messageratedelay, :unlimitedmessagerate, :skipcaps)
RETURNING networkid
autoreconnectretries, unlimitedconnectretries, rejoinchannels, connected,
usermode, awaymessage, attachperform, detachperform, usesasl, saslaccount,
saslpassword, usecustomessagerate, messagerateburstsize, messageratedelay,
- unlimitedmessagerate)
-VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ unlimitedmessagerate, skipcaps)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
userandomserver, perform, useautoidentify, autoidentifyservice, autoidentifypassword,
useautoreconnect, autoreconnectinterval, autoreconnectretries, unlimitedconnectretries,
rejoinchannels, usesasl, saslaccount, saslpassword, usecustomessagerate,
- messagerateburstsize, messageratedelay, unlimitedmessagerate
+ messagerateburstsize, messageratedelay, unlimitedmessagerate, skipcaps
FROM network
WHERE userid = :userid
messagerateburstsize INTEGER NOT NULL DEFAULT 5, -- Maximum messages at once
messageratedelay INTEGER NOT NULL DEFAULT 2200, -- Delay between future messages (milliseconds)
unlimitedmessagerate boolean NOT NULL DEFAULT FALSE, -- Disable rate limits
+ skipcaps TEXT, -- Space-separated IRCv3 caps to not auto-negotiate
UNIQUE (userid, networkname)
)
rejoinchannels = :rejoinchannels,
usesasl = :usesasl,
saslaccount = :saslaccount,
-saslpassword = :saslpassword
+saslpassword = :saslpassword,
+skipcaps = :skipcaps
WHERE userid = :userid AND networkid = :networkid
--- /dev/null
+ALTER TABLE network ADD COLUMN skipcaps TEXT
if (Client::isCoreFeatureEnabled(Quassel::Feature::FeatureName)) { ... }
```
+Depending on the type of protocol change, you might also need to update
+[`serializers.cpp`][file-cpp-serializers] and related files.
+
8. **Test everything! Upgrade, migrate, new setups, new client/old core,
old client/new core, etc.**
[file-cpp-sqlite]: ../sqlitestorage.cpp
[file-sh-upgradeschema]: upgradeSchema.sh
[file-h-quassel]: ../../common/quassel.h
+[file-cpp-serializers]: ../../common/serializers/serializers.cpp
autoidentifypassword, useautoreconnect, autoreconnectinterval,
autoreconnectretries, unlimitedconnectretries, rejoinchannels, usesasl,
saslaccount, saslpassword, usecustomessagerate, messagerateburstsize,
- messageratedelay, unlimitedmessagerate)
+ messageratedelay, unlimitedmessagerate, skipcaps)
VALUES (:userid, :networkname, :identityid, :servercodec, :encodingcodec, :decodingcodec,
:userandomserver, :perform, :useautoidentify, :autoidentifyservice, :autoidentifypassword,
:useautoreconnect, :autoreconnectinterval, :autoreconnectretries, :unlimitedconnectretries,
:rejoinchannels, :usesasl, :saslaccount, :saslpassword, :usecustomessagerate,
- :messagerateburstsize, :messageratedelay, :unlimitedmessagerate)
+ :messagerateburstsize, :messageratedelay, :unlimitedmessagerate, :skipcaps)
useautoreconnect, autoreconnectinterval, autoreconnectretries, unlimitedconnectretries,
rejoinchannels, connected, usermode, awaymessage, attachperform, detachperform,
usesasl, saslaccount, saslpassword, usecustomessagerate, messagerateburstsize,
- messageratedelay, unlimitedmessagerate
+ messageratedelay, unlimitedmessagerate, skipcaps
FROM network
userandomserver, perform, useautoidentify, autoidentifyservice, autoidentifypassword,
useautoreconnect, autoreconnectinterval, autoreconnectretries, unlimitedconnectretries,
rejoinchannels, usesasl, saslaccount, saslpassword, usecustomessagerate,
- messagerateburstsize, messageratedelay, unlimitedmessagerate
+ messagerateburstsize, messageratedelay, unlimitedmessagerate, skipcaps
FROM network
WHERE userid = :userid
messagerateburstsize INTEGER NOT NULL DEFAULT 5, -- Maximum messages at once
messageratedelay INTEGER NOT NULL DEFAULT 2200, -- Delay between future messages (milliseconds)
unlimitedmessagerate INTEGER NOT NULL DEFAULT 0, -- BOOL - Disable rate limits
+ skipcaps TEXT, -- Space-separated IRCv3 caps to not auto-negotiate
UNIQUE (userid, networkname)
)
unlimitedmessagerate = :unlimitedmessagerate,
usesasl = :usesasl,
saslaccount = :saslaccount,
-saslpassword = :saslpassword
+saslpassword = :saslpassword,
+skipcaps = :skipcaps
WHERE networkid = :networkid AND userid = :userid
--- /dev/null
+ALTER TABLE network ADD COLUMN skipcaps TEXT
QString awaymessage;
QString attachperform;
QString detachperform;
+ QString skipcaps;
NetworkId networkid;
IdentityId identityid;
int messagerateburstsize;
#include "corenetwork.h"
+#include <algorithm>
+
#include <QDebug>
#include <QHostInfo>
#include <QTextBoundaryFinder>
void CoreNetwork::serverCapAdded(const QString& capability)
{
+ // Exclude skipped capabilities
+ if (skipCaps().contains(capability)) {
+ return;
+ }
+
// Check if it's a known capability; if so, add it to the list
// Handle special cases first
if (capability == IrcCap::SASL) {
}
// Handle capabilities that require further messages sent to the IRC server
- // If you change this list, ALSO change the list in CoreNetwork::capsRequiringServerMessages
+ // If you change this list, ALSO change the list in CoreNetwork::capsRequiringConfiguration
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
void CoreNetwork::beginCapNegotiation()
{
+ // Check if any available capabilities have been disabled
+ QStringList capsSkipped;
+ if (!skipCaps().isEmpty() && !caps().isEmpty()) {
+ // Find the entries that are common to skipCaps() and caps(). This represents any
+ // capabilities supported by the server that were skipped.
+
+ // Both skipCaps() and caps() are already lowercase
+ // std::set_intersection requires sorted lists, and we can't modify the original lists.
+ //
+ // skipCaps() should already be sorted. caps() is intentionally not sorted elsewhere so
+ // Quassel can show the capabilities in the order transmitted by the network.
+ auto sortedCaps = caps();
+ sortedCaps.sort();
+
+ // Find the intersection between skipped caps and server-supplied caps
+ std::set_intersection(skipCaps().cbegin(), skipCaps().cend(),
+ sortedCaps.cbegin(), sortedCaps.cend(),
+ std::back_inserter(capsSkipped));
+ }
+
if (!capsPendingNegotiation()) {
// No capabilities are queued for request, determine the reason why
QString capStatusMsg;
capStatusMsg
));
+ if (!capsSkipped.isEmpty()) {
+ // Mention that some capabilities are skipped
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Quassel is configured to ignore some capabilities (skipped: %1)").arg(capsSkipped.join(", "))
+ ));
+ }
+
// End any ongoing capability negotiation, allowing connection to continue
endCapNegotiation();
return;
tr("Ready to negotiate (found: %1)").arg(caps().join(", "))
));
+ if (!capsSkipped.isEmpty()) {
+ // Mention that some capabilities are skipped
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Quassel is configured to ignore some capabilities (skipped: %1)").arg(capsSkipped.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).
QString queuedCapsDisplay = _capsQueuedIndividual.join(", ")
query.bindValue(":messagerateburstsize", info.messageRateBurstSize);
query.bindValue(":messageratedelay", info.messageRateDelay);
query.bindValue(":unlimitedmessagerate", info.unlimitedMessageRate);
+ query.bindValue(":skipcaps", info.skipCapsToString());
+
if (info.networkId.isValid())
query.bindValue(":networkid", info.networkId.toInt());
}
net.messageRateBurstSize = networksQuery.value(20).toUInt();
net.messageRateDelay = networksQuery.value(21).toUInt();
net.unlimitedMessageRate = networksQuery.value(22).toBool();
+ net.skipCapsFromString(networksQuery.value(23).toString());
serversQuery.bindValue(":networkid", net.networkId.toInt());
safeExec(serversQuery);
bindValue(26, network.messagerateburstsize);
bindValue(27, network.messageratedelay);
bindValue(28, network.unlimitedmessagerate);
+ // Skipped IRCv3 caps
+ bindValue(29, network.skipcaps);
return exec();
}
query.bindValue(":messagerateburstsize", info.messageRateBurstSize);
query.bindValue(":messageratedelay", info.messageRateDelay);
query.bindValue(":unlimitedmessagerate", info.unlimitedMessageRate ? 1 : 0);
+ query.bindValue(":skipcaps", info.skipCapsToString());
if (info.networkId.isValid())
query.bindValue(":networkid", info.networkId.toInt());
}
net.messageRateBurstSize = networksQuery.value(20).toUInt();
net.messageRateDelay = networksQuery.value(21).toUInt();
net.unlimitedMessageRate = networksQuery.value(22).toInt() == 1 ? true : false;
+ net.skipCapsFromString(networksQuery.value(23).toString());
serversQuery.bindValue(":networkid", net.networkId.toInt());
safeExec(serversQuery);
network.messagerateburstsize = value(26).toInt();
network.messageratedelay = value(27).toUInt();
network.unlimitedmessagerate = value(28).toInt() == 1 ? true : false;
+ // Skipped IRCv3 caps
+ network.skipcaps = value(29).toString();
return true;
}