+#include "coreuserinputhandler.h"
+#include "ircencoder.h"
+#include "irccap.h"
+#include "irctag.h"
+#include "networkevent.h"
+
+CoreNetwork::CoreNetwork(const NetworkId& networkid, CoreSession* session)
+ : Network(networkid, session)
+ , _coreSession(session)
+ , _userInputHandler(new CoreUserInputHandler(this))
+ , _autoReconnectCount(0)
+ , _quitRequested(false)
+ , _disconnectExpected(false)
+ , _previousConnectionAttemptFailed(false)
+ , _lastUsedServerIndex(0)
+ , _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, &QTimer::timeout, this, &CoreNetwork::onSocketCloseTimeout);
+
+ setPingInterval(networkConfig()->pingInterval());
+ connect(&_pingTimer, &QTimer::timeout, this, &CoreNetwork::sendPing);
+
+ setAutoWhoDelay(networkConfig()->autoWhoDelay());
+ setAutoWhoInterval(networkConfig()->autoWhoInterval());
+
+ QHash<QString, QString> channels = coreSession()->persistentChannels(networkId());
+ for (const QString& chan : channels.keys()) {
+ _channelKeys[chan.toLower()] = channels[chan];
+ }
+
+ QHash<QString, QByteArray> bufferCiphers = coreSession()->bufferCiphers(networkId());
+ for (const QString& buffer : bufferCiphers.keys()) {
+ storeChannelCipherKey(buffer.toLower(), bufferCiphers[buffer]);
+ }
+
+ 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, &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, &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, &QSslSocket::encrypted, this, &CoreNetwork::onSocketInitialized);
+ connect(&socket, selectOverload<const QList<QSslError>&>(&QSslSocket::sslErrors), this, &CoreNetwork::onSslErrors);
+#endif
+ connect(this, &CoreNetwork::newEvent, coreSession()->eventManager(), &EventManager::postEvent);
+
+ // Custom rate limiting
+ // These react to the user changing settings in the client
+ 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, &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, &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()
+{
+ // 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());
+ }
+}
+
+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();
+ if (socket.state() != QAbstractSocket::UnconnectedState) {
+ return socket.waitForDisconnected(msecs);
+ }
+ return true;
+}
+
+QString CoreNetwork::channelDecode(const QString& bufferName, const QByteArray& string) const
+{
+ if (!bufferName.isEmpty()) {
+ IrcChannel* channel = ircChannel(bufferName);
+ if (channel)
+ return channel->decodeString(string);
+ }
+ return decodeString(string);
+}
+
+QString CoreNetwork::userDecode(const QString& userNick, const QByteArray& string) const
+{
+ IrcUser* user = ircUser(userNick);
+ if (user)
+ return user->decodeString(string);
+ return decodeString(string);
+}
+
+QByteArray CoreNetwork::channelEncode(const QString& bufferName, const QString& string) const
+{
+ if (!bufferName.isEmpty()) {
+ IrcChannel* channel = ircChannel(bufferName);
+ if (channel)
+ return channel->encodeString(string);
+ }
+ return encodeString(string);
+}
+
+QByteArray CoreNetwork::userEncode(const QString& userNick, const QString& string) const
+{
+ IrcUser* user = ircUser(userNick);
+ if (user)
+ return user->encodeString(string);
+ return encodeString(string);
+}
+
+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())
+ _autoReconnectCount = -1;
+ else
+ _autoReconnectCount = autoReconnectRetries();
+ }
+ if (serverList().isEmpty()) {
+ qWarning() << "Server list empty, ignoring connect request!";
+ return;
+ }
+ CoreIdentity* identity = identityPtr();
+ if (!identity) {
+ qWarning() << "Invalid identity configures, ignoring connect request!";
+ return;
+ }
+
+ // 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();
+ }
+ else if (_previousConnectionAttemptFailed) {
+ // cycle to next server if previous connection attempt failed
+ _previousConnectionAttemptFailed = false;
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Connection failed. Cycling to next server...")
+ ));
+ if (++_lastUsedServerIndex >= serverList().size()) {
+ _lastUsedServerIndex = 0;
+ }
+ }
+ else {
+ // Start out with the top server in the list
+ _lastUsedServerIndex = 0;
+ }
+
+ Server server = usedServer();
+ displayStatusMsg(tr("Connecting to %1:%2...").arg(server.host).arg(server.port));
+ showMessage(NetworkInternalMessage(
+ 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);
+ socket.setProxy(proxy);
+ }
+ else {
+ socket.setProxy(QNetworkProxy::NoProxy);
+ }
+
+ 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) {
+ // Avoid hostname lookups when a proxy is specified. The lookups won't use the proxy and may therefore leak the DNS
+ // hostname of the server. Qt's DNS cache also isn't used by the proxy so we don't need to refresh the entry.
+ QHostInfo::fromName(server.host);
+ }
+#ifdef HAVE_SSL
+ if (server.useSsl) {
+ CoreIdentity* identity = identityPtr();
+ if (identity) {
+ socket.setLocalCertificate(identity->sslCert());
+ socket.setPrivateKey(identity->sslKey());
+ }
+ socket.connectToHostEncrypted(server.host, server.port);
+ }
+ else {
+ socket.connectToHost(server.host, server.port);
+ }
+#else
+ socket.connectToHost(server.host, server.port);
+#endif
+}
+
+void CoreNetwork::disconnectFromIrc(bool requested, const QString& reason, bool withReconnect)
+{
+ // Disconnecting from the network, should expect a socket close or error
+ _disconnectExpected = true;
+ _quitRequested = requested; // see socketDisconnected();
+ if (!withReconnect) {
+ _autoReconnectTimer.stop();
+ _autoReconnectCount = 0; // prohibiting auto reconnect
+ }
+ disablePingTimeout();
+ _msgQueue.clear();
+
+ IrcUser* me_ = me();
+ if (me_) {
+ QString awayMsg;
+ if (me_->isAway())
+ awayMsg = me_->awayMessage();
+ Core::setAwayMessage(userId(), networkId(), awayMsg);
+ }
+
+ if (reason.isEmpty() && identityPtr())
+ _quitReason = identityPtr()->quitReason();
+ else
+ _quitReason = reason;
+
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Disconnecting. (%1)").arg((!requested && !withReconnect) ? tr("Core Shutdown") : _quitReason)
+ ));
+ if (socket.state() == QAbstractSocket::UnconnectedState) {
+ onSocketDisconnected();
+ }
+ else {
+ if (socket.state() == QAbstractSocket::ConnectedState) {
+ // If shutting down, prioritize the QUIT command
+ userInputHandler()->issueQuit(_quitReason, _shuttingDown);
+ }
+ else {
+ socket.close();
+ }
+ 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::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, bool prepend)
+{
+ 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.
+ writeToSocket(s);
+ }
+ else {
+ // Otherwise, queue the message for later
+ if (prepend) {
+ // Jump to the start, skipping other messages
+ _msgQueue.prepend(s);
+ }
+ else {
+ // Add to back, waiting in order
+ _msgQueue.append(s);
+ }
+ }
+}
+
+void CoreNetwork::putCmd(const QString& cmd, const QList<QByteArray>& params, const QByteArray& prefix, const QHash<IrcTagKey, QString>& tags, bool prepend)
+{
+ putRawLine(IrcEncoder::writeMessage(tags, prefix, cmd, params), prepend);
+}
+
+void CoreNetwork::putCmd(const QString& cmd, const QList<QList<QByteArray>>& params, const QByteArray& prefix, const QHash<IrcTagKey, QString>& tags, bool prependAll)
+{
+ QListIterator<QList<QByteArray>> i(params);
+ while (i.hasNext()) {
+ QList<QByteArray> msg = i.next();
+ putCmd(cmd, msg, prefix, tags, prependAll);
+ }
+}
+
+void CoreNetwork::setChannelJoined(const QString& channel)
+{
+ queueAutoWhoOneshot(channel); // check this new channel first
+
+ Core::setChannelPersistent(userId(), networkId(), channel, true);
+ Core::setPersistentChannelKey(userId(), networkId(), channel, _channelKeys[channel.toLower()]);
+}
+
+void CoreNetwork::setChannelParted(const QString& channel)
+{
+ removeChannelKey(channel);
+ _autoWhoQueue.removeAll(channel.toLower());
+ _autoWhoPending.remove(channel.toLower());
+
+ Core::setChannelPersistent(userId(), networkId(), channel, false);
+}
+
+void CoreNetwork::addChannelKey(const QString& channel, const QString& key)
+{
+ if (key.isEmpty()) {
+ removeChannelKey(channel);
+ }
+ else {
+ _channelKeys[channel.toLower()] = key;
+ }
+}
+
+void CoreNetwork::removeChannelKey(const QString& channel)
+{
+ _channelKeys.remove(channel.toLower());
+}
+
+#ifdef HAVE_QCA2
+Cipher* CoreNetwork::cipher(const QString& target)
+{
+ if (target.isEmpty())
+ return nullptr;
+
+ if (!Cipher::neededFeaturesAvailable())
+ return nullptr;
+
+ auto* channel = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+ if (channel) {
+ return channel->cipher();
+ }
+ auto* user = qobject_cast<CoreIrcUser*>(ircUser(target));
+ if (user) {
+ return user->cipher();
+ }
+ else if (!isChannelName(target)) {
+ return qobject_cast<CoreIrcUser*>(newIrcUser(target))->cipher();
+ }
+ return nullptr;
+}
+
+QByteArray CoreNetwork::cipherKey(const QString& target) const
+{
+ auto* c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+ if (c)
+ return c->cipher()->key();
+
+ auto* u = qobject_cast<CoreIrcUser*>(ircUser(target));
+ if (u)
+ return u->cipher()->key();
+
+ return QByteArray();
+}
+
+void CoreNetwork::setCipherKey(const QString& target, const QByteArray& key)
+{
+ auto* c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+ if (c) {
+ c->setEncrypted(c->cipher()->setKey(key));
+ coreSession()->setBufferCipher(networkId(), target, key);
+ return;
+ }
+
+ auto* u = qobject_cast<CoreIrcUser*>(ircUser(target));
+ if (!u && !isChannelName(target))
+ u = qobject_cast<CoreIrcUser*>(newIrcUser(target));
+
+ if (u) {
+ u->setEncrypted(u->cipher()->setKey(key));
+ coreSession()->setBufferCipher(networkId(), target, key);
+ return;
+ }
+}
+
+bool CoreNetwork::cipherUsesCBC(const QString& target)
+{
+ auto* c = qobject_cast<CoreIrcChannel*>(ircChannel(target));
+ if (c)
+ return c->cipher()->usesCBC();
+ auto* u = qobject_cast<CoreIrcUser*>(ircUser(target));
+ if (u)
+ return u->cipher()->usesCBC();
+
+ return false;
+}
+#endif /* HAVE_QCA2 */
+
+bool CoreNetwork::setAutoWhoDone(const QString& name)
+{
+ QString chanOrNick = name.toLower();
+ if (_autoWhoPending.value(chanOrNick, 0) <= 0)
+ return false;
+ if (--_autoWhoPending[chanOrNick] <= 0)
+ _autoWhoPending.remove(chanOrNick);
+ return true;
+}
+
+void CoreNetwork::setMyNick(const QString& mynick)
+{
+ Network::setMyNick(mynick);
+ if (connectionState() == Network::Initializing)
+ networkInitialized();
+}
+
+void CoreNetwork::onSocketHasData()
+{
+ while (socket.canReadLine()) {
+ QByteArray s = socket.readLine();
+ if (s.endsWith("\r\n"))
+ s.chop(2);
+ else if (s.endsWith("\n"))
+ s.chop(1);
+ NetworkDataEvent* event = new NetworkDataEvent(EventManager::NetworkIncoming, this, s);
+ event->setTimestamp(QDateTime::currentDateTimeUtc());
+ emit newEvent(event);
+ }
+}
+
+void CoreNetwork::onSocketError(QAbstractSocket::SocketError error)
+{
+ // 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()));
+ emit connectionError(socket.errorString());
+ showMessage(NetworkInternalMessage(
+ Message::Error,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Connection failure: %1").arg(socket.errorString())
+ ));
+ emitConnectionError(socket.errorString());
+ if (socket.state() < QAbstractSocket::ConnectedState) {
+ onSocketDisconnected();
+ }
+}
+
+void CoreNetwork::onSocketInitialized()
+{
+ CoreIdentity* identity = identityPtr();
+ if (!identity) {
+ qCritical() << "Identity invalid!";
+ disconnectFromIrc();
+ return;
+ }
+
+ Server server = usedServer();
+
+#ifdef HAVE_SSL
+ // 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(), _socketId);
+ }
+
+ 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(), _socketId);
+#endif
+
+ socket.setSocketOption(QAbstractSocket::KeepAliveOption, true);
+
+ // Update the TokenBucket, force-enabling unlimited message rates for initial registration and
+ // capability negotiation. networkInitialized() will call updateRateLimiting() without the
+ // force flag to apply user preferences. When making changes, ensure that this still happens!
+ // As Quassel waits for CAP ACK/NAK and AUTHENTICATE replies, this shouldn't ever fill the IRC
+ // server receive queue and cause a kill. "Shouldn't" being the operative word; the real world
+ // is a scary place.
+ updateRateLimiting(true);
+ // Fill up the token bucket as we're connecting from scratch
+ resetTokenBucket();
+
+ // Request capabilities as per IRCv3.2 specifications
+ // Older servers should ignore this; newer servers won't downgrade to RFC1459
+ showMessage(NetworkInternalMessage(
+ 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)));
+ }
+ QString nick;
+ if (identity->nicks().isEmpty()) {
+ nick = "quassel";
+ qWarning() << "CoreNetwork::socketInitialized(): no nicks supplied for identity Id" << identity->id();
+ }
+ else {
+ nick = identity->nicks()[0];
+ }
+ putRawLine(serverEncode(QString("NICK %1").arg(nick)));
+ // 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::onSocketDisconnected()
+{
+ disablePingTimeout();
+ _msgQueue.clear();
+
+ _autoWhoCycleTimer.stop();
+ _autoWhoTimer.stop();
+ _autoWhoQueue.clear();
+ _autoWhoPending.clear();
+
+ _socketCloseTimer.stop();
+
+ _tokenBucketTimer.stop();
+
+ IrcUser* me_ = me();
+ if (me_) {
+ for (const QString& channel : me_->channels()) {
+ showMessage(NetworkInternalMessage(
+ Message::Quit,
+ BufferInfo::ChannelBuffer,
+ channel,
+ _quitReason, me_->hostmask()
+ ));
+ }
+ }
+
+ setConnected(false);
+ emit disconnected(networkId());
+ emit socketDisconnected(identityPtr(), localAddress(), localPort(), peerAddress(), peerPort(), _socketId);
+ // Reset disconnect expectations
+ _disconnectExpected = false;
+ if (_quitRequested) {
+ _quitRequested = false;
+ setConnectionState(Network::Disconnected);
+ Core::setNetworkConnected(userId(), networkId(), false);
+ }
+ else if (_autoReconnectCount != 0) {
+ setConnectionState(Network::Reconnecting);
+ if (_autoReconnectCount == -1 || _autoReconnectCount == autoReconnectRetries())
+ doAutoReconnect(); // first try is immediate
+ else
+ _autoReconnectTimer.start();
+ }
+}
+
+void CoreNetwork::onSocketStateChanged(QAbstractSocket::SocketState socketState)
+{
+ Network::ConnectionState state;
+ switch (socketState) {
+ case QAbstractSocket::UnconnectedState:
+ state = Network::Disconnected;
+ onSocketDisconnected();
+ break;
+ case QAbstractSocket::HostLookupState:
+ case QAbstractSocket::ConnectingState:
+ state = Network::Connecting;
+ break;
+ case QAbstractSocket::ConnectedState:
+ state = Network::Initializing;
+ break;
+ case QAbstractSocket::ClosingState:
+ state = Network::Disconnecting;
+ break;
+ default:
+ state = Network::Disconnected;
+ }
+ setConnectionState(state);
+}
+
+void CoreNetwork::networkInitialized()
+{
+ setConnectionState(Network::Initialized);
+ setConnected(true);
+ _disconnectExpected = false;
+ _quitRequested = false;
+
+ // Update the TokenBucket with specified rate-limiting settings, removing the force-unlimited
+ // flag used for initial registration and capability negotiation.
+ updateRateLimiting();
+
+ if (useAutoReconnect()) {
+ // reset counter
+ _autoReconnectCount = unlimitedReconnectRetries() ? -1 : autoReconnectRetries();
+ }
+
+ // restore away state
+ QString awayMsg = Core::awayMessage(userId(), networkId());
+ if (!awayMsg.isEmpty()) {
+ // Don't re-apply any timestamp formatting in order to preserve escaped percent signs, e.g.
+ // '%%%%%%%%' -> '%%%%' If processed again, it'd result in '%%'.
+ userInputHandler()->handleAway(BufferInfo(), awayMsg, true);
+ }
+
+ sendPerform();
+
+ _sendPings = true;
+
+ if (networkConfig()->autoWhoEnabled()) {
+ _autoWhoCycleTimer.start();
+ _autoWhoTimer.start();
+ startAutoWhoCycle(); // FIXME wait for autojoin to be completed
+ }
+
+ Core::bufferInfo(userId(), networkId(), BufferInfo::StatusBuffer); // create status buffer
+ Core::setNetworkConnected(userId(), networkId(), true);
+}
+
+void CoreNetwork::sendPerform()
+{
+ BufferInfo statusBuf = BufferInfo::fakeStatusBuffer(networkId());
+
+ // do auto identify
+ if (useAutoIdentify() && !autoIdentifyService().isEmpty() && !autoIdentifyPassword().isEmpty()) {
+ userInputHandler()->handleMsg(statusBuf, QString("%1 IDENTIFY %2").arg(autoIdentifyService(), autoIdentifyPassword()));
+ }
+
+ // restore old user modes if server default mode is set.
+ IrcUser* me_ = me();
+ if (me_) {
+ if (!me_->userModes().isEmpty()) {
+ restoreUserModes();
+ }
+ else {
+ connect(me_, &IrcUser::userModesSet, this, &CoreNetwork::restoreUserModes);
+ connect(me_, &IrcUser::userModesAdded, this, &CoreNetwork::restoreUserModes);
+ }
+ }
+
+ // send perform list
+ for (const QString& line : perform()) {
+ if (!line.isEmpty())
+ userInput(statusBuf, line);
+ }
+
+ // rejoin channels we've been in
+ if (rejoinChannels()) {
+ QStringList channels, keys;
+ for (const QString& chan : coreSession()->persistentChannels(networkId()).keys()) {
+ QString key = channelKey(chan);
+ if (!key.isEmpty()) {
+ channels.prepend(chan);
+ keys.prepend(key);
+ }
+ else {
+ channels.append(chan);
+ }
+ }
+ QString joinString = QString("%1 %2").arg(channels.join(",")).arg(keys.join(",")).trimmed();
+ if (!joinString.isEmpty())
+ userInputHandler()->handleJoin(statusBuf, joinString);
+ }
+}
+
+void CoreNetwork::restoreUserModes()
+{
+ IrcUser* me_ = me();
+ Q_ASSERT(me_);
+
+ disconnect(me_, &IrcUser::userModesSet, this, &CoreNetwork::restoreUserModes);
+ disconnect(me_, &IrcUser::userModesAdded, this, &CoreNetwork::restoreUserModes);
+
+ QString modesDelta = Core::userModes(userId(), networkId());
+ QString currentModes = me_->userModes();
+
+ QString addModes, removeModes;
+ if (modesDelta.contains('-')) {
+ addModes = modesDelta.section('-', 0, 0);
+ removeModes = modesDelta.section('-', 1);
+ }
+ else {
+ addModes = modesDelta;
+ }
+
+ addModes.remove(QRegExp(QString("[%1]").arg(currentModes)));
+ if (currentModes.isEmpty())
+ removeModes = QString();
+ else
+ removeModes.remove(QRegExp(QString("[^%1]").arg(currentModes)));