From: Daniel Albers Date: Fri, 1 Apr 2016 00:34:03 +0000 (+0200) Subject: Merge pull request #183 from romibi/enableLinkPreview X-Git-Tag: travis-deploy-test~499 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=de8cb8bb24c3bfd5600726e99acccb080a5beb87;hp=70dc0ed09887bd499ac52636519a50cea1c61b88 Merge pull request #183 from romibi/enableLinkPreview Enable Link-Preview on appveyor builds --- diff --git a/CMakeLists.txt b/CMakeLists.txt index 046b66d8..f5ca7803 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,6 +131,11 @@ if (CMAKE_MAJOR_VERSION GREATER 2) cmake_policy(SET CMP0043 OLD) endif() +# Honor visibility settings for all target types +if (CMAKE_VERSION VERSION_GREATER 3.3) + cmake_policy(SET CMP0063 NEW) +endif() + # Simplify later checks ##################################################################### @@ -231,7 +236,7 @@ if (USE_QT5) PURPOSE "Enable support for the snorenotify framework" ) endif() - + if (WITH_WEBKIT) find_package(Qt5WebKit QUIET) @@ -275,17 +280,22 @@ if (USE_QT5) if (ECM_FOUND) list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) - endif() - - if (WITH_KDE) - find_package(KF5 COMPONENTS ConfigWidgets CoreAddons Notifications NotifyConfig TextWidgets WidgetsAddons XmlGui QUIET) - set_package_properties(KF5 PROPERTIES TYPE REQUIRED - URL "http://www.kde.org" - DESCRIPTION "KDE Frameworks" - PURPOSE "Required for integration into the Plasma desktop" - ) - - endif() + if (WITH_KDE) + find_package(KF5 COMPONENTS ConfigWidgets CoreAddons Notifications NotifyConfig TextWidgets WidgetsAddons XmlGui QUIET) + set_package_properties(KF5 PROPERTIES TYPE REQUIRED + URL "http://www.kde.org" + DESCRIPTION "KDE Frameworks" + PURPOSE "Required for integration into the Plasma desktop" + ) + else(WITH_KDE) + find_package(KF5Sonnet QUIET) + set_package_properties(KF5Sonnet PROPERTIES TYPE RECOMMENDED + URL "http://api.kde.org/frameworks-api/frameworks5-apidocs/sonnet/html" + DESCRIPTION "framework for providing spell-checking capabilities" + PURPOSE "Enables spell-checking support in input widgets" + ) + endif(WITH_KDE) + endif(ECM_FOUND) endif(BUILD_GUI) @@ -570,7 +580,7 @@ endif() include(GetGitRevisionDescription) get_git_head_revision(GIT_REFSPEC GIT_HEAD) -git_describe(GIT_DESCRIBE --long) +git_describe(GIT_DESCRIBE --long --dirty) # If not in a Git repo try to read GIT_HEAD and GIT_DESCRIBE from # enviroment diff --git a/data/quassel.notifyrc b/data/quassel.notifyrc index 2d0e202a..3cf96ff6 100644 --- a/data/quassel.notifyrc +++ b/data/quassel.notifyrc @@ -141,7 +141,7 @@ Comment[ru]=Получено личное сообщение (запрос) Comment[sq]=një mesazh privat (kërkesë) ka mbërritur Comment[tr]=Bir özel mesaj (sorgusu) ulaştı Comment[uk]=Надійшло особисте повідомлення (повідомлення діалогу) -Comment[zh_CN]=收到一条死人短信息(疑问) +Comment[zh_CN]=收到一条私人短信息(询问) Sound=KDE-Im-Message-In.ogg Action=Sound|Popup|Taskbar @@ -172,6 +172,6 @@ Comment[nl]=Een privé-bericht (query) is ontvangen terwijl het Quassel-venster Comment[pt_BR]=Uma mensagem privada (consulta) chegou enquanto o Quassel está focado Comment[sq]=Një masazh privat (kërkesë) ka mbërritur kur Quassel ishte i fokusuar Comment[uk]=Під час перебування Quassel у фокусі надійшло особисте повідомлення (повідомлення з діалогу) -Comment[zh_CN]=当Quassel被聚焦时,收到一条私人短信息(疑问) +Comment[zh_CN]=当Quassel被聚焦时,收到一条私人短信息(询问) Sound=KDE-Im-Message-In.ogg Action=Taskbar diff --git a/src/client/clienttransfer.cpp b/src/client/clienttransfer.cpp index 83980979..fe678818 100644 --- a/src/client/clienttransfer.cpp +++ b/src/client/clienttransfer.cpp @@ -27,7 +27,16 @@ ClientTransfer::ClientTransfer(const QUuid &uuid, QObject *parent) : Transfer(uuid, parent), _file(0) { - connect(this, SIGNAL(stateChanged(State)), SLOT(onStateChanged(State))); + connect(this, SIGNAL(statusChanged(Transfer::Status)), SLOT(onStatusChanged(Transfer::Status))); +} + + +quint64 ClientTransfer::transferred() const +{ + if (status() == Status::Completed) + return fileSize(); + + return _file ? _file->size() : 0; } @@ -82,17 +91,19 @@ void ClientTransfer::dataReceived(PeerPtr, const QByteArray &data) qWarning() << Q_FUNC_INFO << "Could not write to file:" << _file->errorString(); return; } + + emit transferredChanged(transferred()); } -void ClientTransfer::onStateChanged(Transfer::State state) +void ClientTransfer::onStatusChanged(Transfer::Status status) { - switch(state) { - case Completed: + switch(status) { + case Status::Completed: if (_file) _file->close(); break; - case Failed: + case Status::Failed: if (_file) _file->remove(); break; diff --git a/src/client/clienttransfer.h b/src/client/clienttransfer.h index 04637c78..a1d062b7 100644 --- a/src/client/clienttransfer.h +++ b/src/client/clienttransfer.h @@ -37,17 +37,19 @@ public: QString savePath() const; + quint64 transferred() const override; + public slots: // called on the client side - void accept(const QString &savePath) const; - void reject() const; + void accept(const QString &savePath) const override; + void reject() const override; private slots: - void dataReceived(PeerPtr peer, const QByteArray &data); - void onStateChanged(State state); + void dataReceived(PeerPtr peer, const QByteArray &data) override; + void onStatusChanged(Transfer::Status status); private: - virtual void cleanUp(); + void cleanUp() override; mutable QString _savePath; diff --git a/src/client/clienttransfermanager.cpp b/src/client/clienttransfermanager.cpp index ec5c49fa..29342fc3 100644 --- a/src/client/clienttransfermanager.cpp +++ b/src/client/clienttransfermanager.cpp @@ -32,12 +32,6 @@ ClientTransferManager::ClientTransferManager(QObject *parent) } -const ClientTransfer *ClientTransferManager::transfer(const QUuid &uuid) const -{ - return qobject_cast(transfer_(uuid)); -} - - void ClientTransferManager::onCoreTransferAdded(const QUuid &uuid) { if (uuid.isNull()) { diff --git a/src/client/clienttransfermanager.h b/src/client/clienttransfermanager.h index 3c50a869..f837694d 100644 --- a/src/client/clienttransfermanager.h +++ b/src/client/clienttransfermanager.h @@ -35,8 +35,6 @@ class ClientTransferManager : public TransferManager public: ClientTransferManager(QObject *parent = 0); - const ClientTransfer *transfer(const QUuid &uuid) const; - public slots: void onCoreTransferAdded(const QUuid &uuid); void onTransferInitDone(); diff --git a/src/common/aliasmanager.cpp b/src/common/aliasmanager.cpp index cd21f991..fceb7b48 100644 --- a/src/common/aliasmanager.cpp +++ b/src/common/aliasmanager.cpp @@ -128,11 +128,14 @@ void AliasManager::processInput(const BufferInfo &info, const QString &msg_, Com QString msg = msg_; // leading slashes indicate there's a command to call unless there is another one in the first section (like a path /proc/cpuinfo) + // For those habitally tied to irssi, "/ " also makes the rest of the line a literal message int secondSlashPos = msg.indexOf('/', 1); int firstSpacePos = msg.indexOf(' '); - if (!msg.startsWith('/') || (secondSlashPos != -1 && (secondSlashPos < firstSpacePos || firstSpacePos == -1))) { + if (!msg.startsWith('/') || firstSpacePos == 1 || (secondSlashPos != -1 && (secondSlashPos < firstSpacePos || firstSpacePos == -1))) { if (msg.startsWith("//")) - msg.remove(0, 1); // //asdf is transformed to /asdf + msg.remove(0, 1); // "//asdf" is transformed to "/asdf" + else if (msg.startsWith("/ ")) + msg.remove(0, 2); // "/ /asdf" is transformed to "/asdf" msg.prepend("/SAY "); // make sure we only send proper commands to the core } else { diff --git a/src/common/eventmanager.h b/src/common/eventmanager.h index ec140649..16b83bc4 100644 --- a/src/common/eventmanager.h +++ b/src/common/eventmanager.h @@ -88,6 +88,8 @@ public : IrcEvent = 0x00030000, IrcEventAuthenticate, + IrcEventAccount, + IrcEventAway, IrcEventCap, IrcEventInvite, IrcEventJoin, diff --git a/src/common/ircchannel.cpp b/src/common/ircchannel.cpp index c111b9fa..6673a799 100644 --- a/src/common/ircchannel.cpp +++ b/src/common/ircchannel.cpp @@ -178,7 +178,15 @@ void IrcChannel::joinIrcUsers(const QList &users, const QStringList & for (int i = 0; i < users.count(); i++) { ircuser = users[i]; if (!ircuser || _userModes.contains(ircuser)) { - addUserMode(ircuser, modes[i]); + if (modes[i].count() > 1) { + // Multiple modes received, do it one at a time + // TODO Better way of syncing this without breaking protocol? + for (int i_m = 0; i_m < modes[i].count(); ++i_m) { + addUserMode(ircuser, modes[i][i_m]); + } + } else { + addUserMode(ircuser, modes[i]); + } continue; } @@ -190,6 +198,9 @@ void IrcChannel::joinIrcUsers(const QList &users, const QStringList & // If you wonder why there is no counterpart to ircUserJoined: // the joins are propagated by the ircuser. The signal ircUserJoined is only for convenience + // Also update the IRC user's record of modes; this allows easier tracking + ircuser->addUserModes(modes[i]); + newNicks << ircuser->nick(); newModes << modes[i]; newUsers << ircuser; @@ -280,6 +291,8 @@ void IrcChannel::addUserMode(IrcUser *ircuser, const QString &mode) if (!_userModes[ircuser].contains(mode)) { _userModes[ircuser] += mode; + // Also update the IRC user's record of modes; this allows easier tracking + ircuser->addUserModes(mode); QString nick = ircuser->nick(); SYNC_OTHER(addUserMode, ARG(nick), ARG(mode)) emit ircUserModeAdded(ircuser, mode); @@ -301,6 +314,8 @@ void IrcChannel::removeUserMode(IrcUser *ircuser, const QString &mode) if (_userModes[ircuser].contains(mode)) { _userModes[ircuser].remove(mode); + // Also update the IRC user's record of modes; this allows easier tracking + ircuser->removeUserModes(mode); QString nick = ircuser->nick(); SYNC_OTHER(removeUserMode, ARG(nick), ARG(mode)); emit ircUserModeRemoved(ircuser, mode); diff --git a/src/common/ircuser.cpp b/src/common/ircuser.cpp index 32ff7944..15220f76 100644 --- a/src/common/ircuser.cpp +++ b/src/common/ircuser.cpp @@ -346,9 +346,11 @@ void IrcUser::channelDestroyed() void IrcUser::setUserModes(const QString &modes) { - _userModes = modes; - SYNC(ARG(modes)) - emit userModesSet(modes); + if (_userModes != modes) { + _userModes = modes; + SYNC(ARG(modes)) + emit userModesSet(modes); + } } @@ -357,13 +359,19 @@ void IrcUser::addUserModes(const QString &modes) if (modes.isEmpty()) return; + // Don't needlessly sync when no changes are made + bool changesMade = false; for (int i = 0; i < modes.count(); i++) { - if (!_userModes.contains(modes[i])) + if (!_userModes.contains(modes[i])) { _userModes += modes[i]; + changesMade = true; + } } - SYNC(ARG(modes)) - emit userModesAdded(modes); + if (changesMade) { + SYNC(ARG(modes)) + emit userModesAdded(modes); + } } diff --git a/src/common/quassel.cpp b/src/common/quassel.cpp index c166ee64..954d91ed 100644 --- a/src/common/quassel.cpp +++ b/src/common/quassel.cpp @@ -194,7 +194,9 @@ void Quassel::registerMetaTypes() qRegisterMetaType("MsgId"); qRegisterMetaType("QHostAddress"); + qRegisterMetaTypeStreamOperators("QHostAddress"); qRegisterMetaType("QUuid"); + qRegisterMetaTypeStreamOperators("QUuid"); qRegisterMetaTypeStreamOperators("IdentityId"); qRegisterMetaTypeStreamOperators("BufferId"); diff --git a/src/common/transfer.cpp b/src/common/transfer.cpp index 460b0676..9141e8a9 100644 --- a/src/common/transfer.cpp +++ b/src/common/transfer.cpp @@ -23,8 +23,8 @@ INIT_SYNCABLE_OBJECT(Transfer) Transfer::Transfer(const QUuid &uuid, QObject *parent) : SyncableObject(parent), - _state(New), - _direction(Receive), + _status(Status::New), + _direction(Direction::Receive), _port(0), _fileSize(0), _uuid(uuid) @@ -34,7 +34,7 @@ Transfer::Transfer(const QUuid &uuid, QObject *parent) Transfer::Transfer(Direction direction, const QString &nick, const QString &fileName, const QHostAddress &address, quint16 port, quint64 fileSize, QObject *parent) : SyncableObject(parent), - _state(New), + _status(Status::New), _direction(direction), _fileName(fileName), _address(address), @@ -49,6 +49,15 @@ Transfer::Transfer(Direction direction, const QString &nick, const QString &file void Transfer::init() { + static auto regTypes = []() -> bool { + qRegisterMetaType("Transfer::Status"); + qRegisterMetaType("Transfer::Direction"); + qRegisterMetaTypeStreamOperators("Transfer::Status"); + qRegisterMetaTypeStreamOperators("Transfer::Direction"); + return true; + }(); + Q_UNUSED(regTypes); + renameObject(QString("Transfer/%1").arg(_uuid.toString())); setAllowClientUpdates(true); } @@ -60,19 +69,44 @@ QUuid Transfer::uuid() const } -Transfer::State Transfer::state() const +Transfer::Status Transfer::status() const { - return _state; + return _status; } -void Transfer::setState(Transfer::State state) +void Transfer::setStatus(Transfer::Status status) { - if (_state != state) { - _state = state; - SYNC(ARG(state)); - emit stateChanged(state); + if (_status != status) { + _status = status; + SYNC(ARG(status)); + emit statusChanged(status); + } +} + + +QString Transfer::prettyStatus() const +{ + switch(status()) { + case Status::New: + return tr("New"); + case Status::Pending: + return tr("Pending"); + case Status::Connecting: + return tr("Connecting"); + case Status::Transferring: + return tr("Transferring"); + case Status::Paused: + return tr("Paused"); + case Status::Completed: + return tr("Completed"); + case Status::Failed: + return tr("Failed"); + case Status::Rejected: + return tr("Rejected"); } + + return QString(); } @@ -177,6 +211,31 @@ void Transfer::setError(const QString &errorString) { qWarning() << Q_FUNC_INFO << errorString; emit error(errorString); - setState(Failed); + setStatus(Status::Failed); cleanUp(); } + + +QDataStream &operator<<(QDataStream &out, Transfer::Status state) { + out << static_cast(state); + return out; +} + +QDataStream &operator>>(QDataStream &in, Transfer::Status &state) { + qint8 s; + in >> s; + state = static_cast(s); + return in; +} + +QDataStream &operator<<(QDataStream &out, Transfer::Direction direction) { + out << static_cast(direction); + return out; +} + +QDataStream &operator>>(QDataStream &in, Transfer::Direction &direction) { + qint8 d; + in >> d; + direction = static_cast(d); + return in; +} diff --git a/src/common/transfer.h b/src/common/transfer.h index 00948514..5091b3f1 100644 --- a/src/common/transfer.h +++ b/src/common/transfer.h @@ -33,8 +33,8 @@ class Transfer : public SyncableObject SYNCABLE_OBJECT Q_PROPERTY(QUuid uuid READ uuid); - Q_PROPERTY(State state READ state WRITE setState NOTIFY stateChanged); - Q_PROPERTY(Direction direction READ direction WRITE setDirection NOTIFY directionChanged); + Q_PROPERTY(Transfer::Status status READ status WRITE setStatus NOTIFY statusChanged); + Q_PROPERTY(Transfer::Direction direction READ direction WRITE setDirection NOTIFY directionChanged); Q_PROPERTY(QHostAddress address READ address WRITE setAddress NOTIFY addressChanged); Q_PROPERTY(quint16 port READ port WRITE setPort NOTIFY portChanged); Q_PROPERTY(QString fileName READ fileName WRITE setFileName NOTIFY fileNameChanged); @@ -42,7 +42,7 @@ class Transfer : public SyncableObject Q_PROPERTY(QString nick READ nick WRITE setNick NOTIFY nickChanged); public: - enum State { + enum class Status { New, Pending, Connecting, @@ -54,7 +54,7 @@ public: }; Q_ENUMS(State) - enum Direction { + enum class Direction { Send, Receive }; @@ -62,10 +62,11 @@ public: Transfer(const QUuid &uuid, QObject *parent = 0); // for creating a syncable object client-side Transfer(Direction direction, const QString &nick, const QString &fileName, const QHostAddress &address, quint16 port, quint64 size = 0, QObject *parent = 0); - inline virtual const QMetaObject *syncMetaObject() const { return &staticMetaObject; } + inline const QMetaObject *syncMetaObject() const override { return &staticMetaObject; } QUuid uuid() const; - State state() const; + Status status() const; + QString prettyStatus() const; Direction direction() const; QString fileName() const; QHostAddress address() const; @@ -73,6 +74,8 @@ public: quint64 fileSize() const; QString nick() const; + virtual quint64 transferred() const = 0; + public slots: // called on the client side virtual void accept(const QString &savePath) const { Q_UNUSED(savePath); } @@ -83,12 +86,13 @@ public slots: virtual void requestRejected(PeerPtr peer) { Q_UNUSED(peer); } signals: - void stateChanged(State state); - void directionChanged(Direction direction); + void statusChanged(Transfer::Status state); + void directionChanged(Transfer::Direction direction); void addressChanged(const QHostAddress &address); void portChanged(quint16 port); void fileNameChanged(const QString &fileName); void fileSizeChanged(quint64 fileSize); + void transferredChanged(quint64 transferred); void nickChanged(const QString &nick); void error(const QString &errorString); @@ -97,7 +101,7 @@ signals: void rejected(PeerPtr peer = 0) const; protected slots: - void setState(State state); + void setStatus(Transfer::Status status); void setError(const QString &errorString); // called on the client side through sync calls @@ -116,7 +120,7 @@ private: void setNick(const QString &nick); - State _state; + Status _status; Direction _direction; QString _fileName; QHostAddress _address; @@ -126,4 +130,12 @@ private: QUuid _uuid; }; +Q_DECLARE_METATYPE(Transfer::Status) +Q_DECLARE_METATYPE(Transfer::Direction) + +QDataStream &operator<<(QDataStream &out, Transfer::Status state); +QDataStream &operator>>(QDataStream &in, Transfer::Status &state); +QDataStream &operator<<(QDataStream &out, Transfer::Direction direction); +QDataStream &operator>>(QDataStream &in, Transfer::Direction &direction); + #endif diff --git a/src/common/transfermanager.cpp b/src/common/transfermanager.cpp index 5e5af46e..c90adfb3 100644 --- a/src/common/transfermanager.cpp +++ b/src/common/transfermanager.cpp @@ -31,9 +31,9 @@ TransferManager::TransferManager(QObject *parent) } -Transfer *TransferManager::transfer_(const QUuid &uuid) const +Transfer *TransferManager::transfer(const QUuid &uuid) const { - return _transfers.value(uuid, 0); + return _transfers.value(uuid, nullptr); } diff --git a/src/common/transfermanager.h b/src/common/transfermanager.h index 941e11ce..72f9fb99 100644 --- a/src/common/transfermanager.h +++ b/src/common/transfermanager.h @@ -36,13 +36,13 @@ public: TransferManager(QObject *parent = 0); inline virtual const QMetaObject *syncMetaObject() const { return &staticMetaObject; } + Transfer *transfer(const QUuid &uuid) const; QList transferIds() const; signals: void transferAdded(const Transfer *transfer); protected: - Transfer *transfer_(const QUuid &uuid) const; void addTransfer(Transfer *transfer); protected slots: diff --git a/src/core/corenetwork.cpp b/src/core/corenetwork.cpp index a6dc7855..4784edb7 100644 --- a/src/core/corenetwork.cpp +++ b/src/core/corenetwork.cpp @@ -158,6 +158,11 @@ void CoreNetwork::connectToIrc(bool reconnecting) // cleaning up old quit reason _quitReason.clear(); + // reset capability negotiation in case server changes during a reconnect + _capsQueued.clear(); + _capsPending.clear(); + _capsSupported.clear(); + // use a random server? if (useRandomServer()) { _lastUsedServerIndex = qrand() % serverList().size(); @@ -295,7 +300,7 @@ void CoreNetwork::putCmd(const QString &cmd, const QList> &par 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()]); @@ -482,9 +487,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))); } @@ -858,6 +865,81 @@ void CoreNetwork::setPingInterval(int interval) _pingTimer.setInterval(interval * 1000); } +/******** IRCv3 Capability Negotiation ********/ + +void CoreNetwork::addCap(const QString &capability, const QString &value) +{ + // Clear from pending list, add to supported list + if (!_capsSupported.contains(capability)) { + if (value != "") { + // Value defined, just use it + _capsSupported[capability] = value; + } else if (_capsPending.contains(capability)) { + // Value not defined, but a pending capability had a value. + // E.g. CAP * LS :sasl=PLAIN multi-prefix + // Preserve the capability value for later use. + _capsSupported[capability] = _capsPending[capability]; + } else { + // No value ever given, assign to blank + _capsSupported[capability] = QString(); + } + } + if (_capsPending.contains(capability)) + _capsPending.remove(capability); + + // Handle special cases here + // TODO Use events if it makes sense + if (capability == "away-notify") { + // away-notify enabled, stop the automatic timers, handle manually + setAutoWhoEnabled(false); + } +} + +void CoreNetwork::removeCap(const QString &capability) +{ + // Clear from pending list, remove from supported list + if (_capsPending.contains(capability)) + _capsPending.remove(capability); + if (_capsSupported.contains(capability)) + _capsSupported.remove(capability); + + // Handle special cases here + // TODO Use events if it makes sense + if (capability == "away-notify") { + // away-notify disabled, enable autowho according to configuration + setAutoWhoEnabled(networkConfig()->autoWhoEnabled()); + } +} + +QString CoreNetwork::capValue(const QString &capability) const +{ + // If a supported capability exists, good; if not, return pending value. + // If capability isn't supported after all, the pending entry will be removed. + if (_capsSupported.contains(capability)) + return _capsSupported[capability]; + else if (_capsPending.contains(capability)) + return _capsPending[capability]; + else + return QString(); +} + +void CoreNetwork::queuePendingCap(const QString &capability, const QString &value) +{ + if (!_capsQueued.contains(capability)) { + _capsQueued.append(capability); + // Some capabilities may have values attached, preserve them as pending + _capsPending[capability] = value; + } +} + +QString CoreNetwork::takeQueuedCap() +{ + if (!_capsQueued.empty()) { + return _capsQueued.takeFirst(); + } else { + return QString(); + } +} /******** AutoWHO ********/ @@ -870,6 +952,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 (useCapAwayNotify()) { + // When away-notify is active, the timer's stopped. Start a new cycle to who this channel. + setAutoWhoEnabled(true); + } +} + void CoreNetwork::setAutoWhoDelay(int delay) { @@ -901,19 +996,43 @@ 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() + && !useCapAwayNotify()) + 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)); + } + // TODO Use WHO extended to poll away users and/or user accounts + // If a server supports it, supports("WHOX") will be true + // See: http://faerion.sourceforge.net/doc/irc/whox.var and HexChat + putRawLine("WHO " + serverEncode(chanOrNick)); break; } - if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive()) { + + if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive() + && !useCapAwayNotify()) { // 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 (useCapAwayNotify() && _autoWhoCycleTimer.isActive()) { + // Don't run another who cycle if away-notify is enabled + _autoWhoCycleTimer.stop(); } } diff --git a/src/core/corenetwork.h b/src/core/corenetwork.h index 359d032e..ae1ebd8e 100644 --- a/src/core/corenetwork.h +++ b/src/core/corenetwork.h @@ -100,6 +100,92 @@ public: QList> splitMessage(const QString &cmd, const QString &message, std::function(QString &)> cmdGenerator); + // IRCv3 capability negotiation + + /** + * Checks if a given capability is enabled. + * + * @returns True if enabled, otherwise false + */ + inline bool capEnabled(const QString &capability) const { return _capsSupported.contains(capability); } + + /** + * Checks if capability negotiation is currently ongoing. + * + * @returns True if in progress, otherwise false + */ + inline bool capNegotiationInProgress() const { return !_capsQueued.empty(); } + + /** + * Gets the value of an enabled or pending capability, e.g. sasl=plain. + * + * @returns Value of capability if one was specified, otherwise empty string + */ + QString capValue(const QString &capability) const; + + /** + * Gets the next capability to request, removing it from the queue. + * + * @returns Name of capability to request + */ + QString takeQueuedCap(); + + // Specific capabilities for easy reference + + /** + * Gets the status of the sasl authentication capability. + * + * http://ircv3.net/specs/extensions/sasl-3.2.html + * + * @returns True if SASL authentication is enabled, otherwise false + */ + inline bool useCapSASL() const { return capEnabled("sasl"); } + + /** + * Gets the status of the away-notify capability. + * + * http://ircv3.net/specs/extensions/away-notify-3.1.html + * + * @returns True if away-notify is enabled, otherwise false + */ + inline bool useCapAwayNotify() const { return capEnabled("away-notify"); } + + /** + * Gets the status of the account-notify capability. + * + * http://ircv3.net/specs/extensions/account-notify-3.1.html + * + * @returns True if account-notify is enabled, otherwise false + */ + inline bool useCapAccountNotify() const { return capEnabled("account-notify"); } + + /** + * Gets the status of the extended-join capability. + * + * http://ircv3.net/specs/extensions/extended-join-3.1.html + * + * @returns True if extended-join is enabled, otherwise false + */ + inline bool useCapExtendedJoin() const { return capEnabled("extended-join"); } + + /** + * Gets the status of the userhost-in-names capability. + * + * http://ircv3.net/specs/extensions/userhost-in-names-3.2.html + * + * @returns True if userhost-in-names is enabled, otherwise false + */ + inline bool useCapUserhostInNames() const { return capEnabled("userhost-in-names"); } + + /** + * Gets the status of the multi-prefix capability. + * + * http://ircv3.net/specs/extensions/multi-prefix-3.1.html + * + * @returns True if multi-prefix is enabled, otherwise false + */ + inline bool useCapMultiPrefix() const { return capEnabled("multi-prefix"); } + public slots: virtual void setMyNick(const QString &mynick); @@ -134,10 +220,58 @@ public slots: bool cipherUsesCBC(const QString &target); #endif + // IRCv3 capability negotiation (can be connected to signals) + + /** + * Marks a capability as accepted, providing an optional value. + * + * Removes it from queue of pending capabilities and triggers any capability-specific + * activation. + * + * @param[in] capability Name of the capability + * @param[in] value + * @parblock + * Optional value of the capability, e.g. sasl=plain. If left empty, will be copied from the + * pending capability. + * @endparblock + */ + void addCap(const QString &capability, const QString &value = QString()); + + /** + * Marks a capability as denied. + * + * Removes it from the queue of pending capabilities and triggers any capability-specific + * deactivation. + * + * @param[in] capability Name of the capability + */ + void removeCap(const QString &capability); + + /** + * Queues a capability as available but not yet accepted or denied. + * + * Capabilities should be queued when registration pauses for CAP LS for capabilities are only + * requested during login. + * + * @param[in] capability Name of the capability + * @param[in] value Optional value of the capability, e.g. sasl=plain + */ + void queuePendingCap(const QString &capability, const QString &value = QString()); + void setAutoWhoEnabled(bool enabled); void setAutoWhoInterval(int interval); void setAutoWhoDelay(int delay); + /** + * Appends the given channel/nick to the front of the AutoWho queue. + * + * When 'away-notify' is enabled, this will trigger an immediate AutoWho since regular + * who-cycles are disabled as per IRCv3 specifications. + * + * @param[in] channelOrNick Channel or nickname to WHO + */ + void queueAutoWhoOneshot(const QString &channelOrNick); + bool setAutoWhoDone(const QString &channel); void updateIssuedModes(const QString &requestedModes); @@ -240,6 +374,12 @@ private: QHash _autoWhoPending; QTimer _autoWhoTimer, _autoWhoCycleTimer; + // CAPs may have parameter values + // See http://ircv3.net/specs/core/capability-negotiation-3.2.html + QStringList _capsQueued; /// Capabilities to be checked + QHash _capsPending; /// Capabilities pending 'CAP ACK' from server + QHash _capsSupported; /// Enabled capabilities that received 'CAP ACK' + QTimer _tokenBucketTimer; int _messageDelay; // token refill speed in ms int _burstSize; // size of the token bucket diff --git a/src/core/coresessioneventprocessor.cpp b/src/core/coresessioneventprocessor.cpp index eef0dd2e..0468d970 100644 --- a/src/core/coresessioneventprocessor.cpp +++ b/src/core/coresessioneventprocessor.cpp @@ -92,13 +92,16 @@ void CoreSessionEventProcessor::tryNextNick(NetworkEvent *e, const QString &errn void CoreSessionEventProcessor::processIrcEventNumeric(IrcEventNumeric *e) { switch (e->number()) { - // CAP stuff - case 903: - case 904: - case 905: - case 906: - case 907: - qobject_cast(e->network())->putRawLine("CAP END"); + // SASL authentication replies + // See: http://ircv3.net/specs/extensions/sasl-3.1.html + // TODO Handle errors to stop connection if appropriate + case 903: // RPL_SASLSUCCESS + case 904: // ERR_SASLFAIL + case 905: // ERR_SASLTOOLONG + case 906: // ERR_SASLABORTED + case 907: // ERR_SASLALREADY + // Move on to the next capability + sendNextCap(e->network()); break; default: @@ -137,30 +140,166 @@ void CoreSessionEventProcessor::processIrcEventAuthenticate(IrcEvent *e) #endif } +void CoreSessionEventProcessor::sendNextCap(Network *net) +{ + CoreNetwork *coreNet = qobject_cast(net); + if (coreNet->capNegotiationInProgress()) { + // Request the next capability and remove it from the list + // Handle one at a time so one capability failing won't NAK all of 'em + coreNet->putRawLine(coreNet->serverEncode(QString("CAP REQ :%1").arg(coreNet->takeQueuedCap()))); + } else { + // If SASL requested but not available, print a warning + if (coreNet->networkInfo().useSasl && !coreNet->useCapSASL()) + emit newEvent(new MessageEvent(Message::Error, net, tr("SASL authentication not supported by server, continuing without"), QString(), QString(), Message::None, QDateTime::currentDateTimeUtc())); + + // No pending desired capabilities, end negotiation + coreNet->putRawLine(coreNet->serverEncode(QString("CAP END"))); + emit newEvent(new MessageEvent(Message::Server, net, tr("Capability negotiation finished"), QString(), QString(), Message::None, QDateTime::currentDateTimeUtc())); + } +} void CoreSessionEventProcessor::processIrcEventCap(IrcEvent *e) { - // for SASL, there will only be a single param of 'sasl', however you can check here for - // additional CAP messages (ls, multi-prefix, et cetera). - - if (e->params().count() == 3) { - if (e->params().at(2).startsWith("sasl")) { // Freenode (at least) sends "sasl " with a trailing space for some reason! - // FIXME use event - // if the current identity has a cert set, use SASL EXTERNAL -#ifdef HAVE_SSL - if (!coreNetwork(e)->identityPtr()->sslCert().isNull()) { - coreNetwork(e)->putRawLine(coreNetwork(e)->serverEncode("AUTHENTICATE EXTERNAL")); + // Handle capability negotiation + // See: http://ircv3.net/specs/core/capability-negotiation-3.2.html + // And: http://ircv3.net/specs/core/capability-negotiation-3.1.html + if (e->params().count() >= 3) { + CoreNetwork *coreNet = coreNetwork(e); + if (e->params().at(1).compare("LS", Qt::CaseInsensitive) == 0) { + // Server: CAP * LS * :multi-prefix extended-join account-notify batch invite-notify tls + // Server: CAP * LS * :cap-notify server-time example.org/dummy-cap=dummyvalue example.org/second-dummy-cap + // Server: CAP * LS :userhost-in-names sasl=EXTERNAL,DH-AES,DH-BLOWFISH,ECDSA-NIST256P-CHALLENGE,PLAIN + bool capListFinished; + QStringList availableCaps; + if (e->params().count() == 4) { + // Middle of multi-line reply, ignore the asterisk + capListFinished = false; + availableCaps = e->params().at(3).split(' '); } else { + // Single line reply + capListFinished = true; + availableCaps = e->params().at(2).split(' '); + } + // We know what capabilities are available, request what we want. + QStringList availableCapPair; + bool queueCurrentCap; + for (int i = 0; i < availableCaps.count(); ++i) { + // Capability may include values, e.g. CAP * LS :multi-prefix sasl=EXTERNAL + availableCapPair = availableCaps[i].trimmed().split('='); + queueCurrentCap = false; + if (availableCapPair.at(0).startsWith("sasl")) { + // Only request SASL if it's enabled + if (coreNet->networkInfo().useSasl) + queueCurrentCap = true; + } else if (availableCapPair.at(0).startsWith("away-notify") || + availableCapPair.at(0).startsWith("account-notify") || + availableCapPair.at(0).startsWith("extended-join") || + availableCapPair.at(0).startsWith("userhost-in-names") || + availableCapPair.at(0).startsWith("multi-prefix")) { + // Always request these capabilities if available + queueCurrentCap = true; + } + if (queueCurrentCap) { + if(availableCapPair.count() >= 2) + coreNet->queuePendingCap(availableCapPair.at(0).trimmed(), availableCapPair.at(1).trimmed()); + else + coreNet->queuePendingCap(availableCapPair.at(0).trimmed()); + } + } + // Begin capability requests when capability listing complete + if (capListFinished) { + emit newEvent(new MessageEvent(Message::Server, e->network(), tr("Negotiating capabilities..."), QString(), QString(), Message::None, e->timestamp())); + sendNextCap(coreNet); + } + } else if (e->params().at(1).compare("ACK", Qt::CaseInsensitive) == 0) { + // Server: CAP * ACK :multi-prefix sasl + // Got the capability we want, enable, handle as needed. + // As only one capability is requested at a time, no need to split + // Lower-case the list to make later comparisons easier + // Capability may include values, e.g. CAP * LS :multi-prefix sasl=EXTERNAL + QStringList acceptedCap = e->params().at(2).trimmed().split('='); + + // Mark this cap as accepted + if(acceptedCap.count() >= 2) + coreNet->addCap(acceptedCap.at(0), acceptedCap.at(1)); + else + coreNet->addCap(acceptedCap.at(0)); + + // Handle special cases + if (acceptedCap.at(0).startsWith("sasl")) { + // Freenode (at least) sends "sasl " with a trailing space for some reason! + // if the current identity has a cert set, use SASL EXTERNAL + // FIXME use event + // TODO If value of sasl capability is not empty, limit to accepted +#ifdef HAVE_SSL + if (!coreNet->identityPtr()->sslCert().isNull()) { + coreNet->putRawLine(coreNet->serverEncode("AUTHENTICATE EXTERNAL")); + } else { #endif - // Only working with PLAIN atm, blowfish later - coreNetwork(e)->putRawLine(coreNetwork(e)->serverEncode("AUTHENTICATE PLAIN")); + // Only working with PLAIN atm, blowfish later + coreNet->putRawLine(coreNet->serverEncode("AUTHENTICATE PLAIN")); #ifdef HAVE_SSL - } + } #endif + } else { + // Special handling not needed, move on to next cap + sendNextCap(coreNet); + } + } else if (e->params().at(1).compare("NAK", Qt::CaseInsensitive) == 0) { + // Something went wrong with this capability, disable, go to next cap + // As only one capability is requested at a time, no need to split + // Lower-case the list to make later comparisons easier + QString deniedCap = e->params().at(2).trimmed(); + coreNet->removeCap(deniedCap); + sendNextCap(coreNet); } } } +/* IRCv3 account-notify + * Log in: ":nick!user@host ACCOUNT accountname" + * Log out: ":nick!user@host ACCOUNT *" */ +void CoreSessionEventProcessor::processIrcEventAccount(IrcEvent *e) +{ + if (!checkParamCount(e, 1)) + return; + + IrcUser *ircuser = e->network()->updateNickFromMask(e->prefix()); + if (ircuser) { + // FIXME Keep track of authed user account, requires adding support to ircuser.h/cpp + /* + if (e->params().at(0) != "*") { + // Account logged in + qDebug() << "account-notify:" << ircuser->nick() << "logged in to" << e->params().at(0); + } else { + // Account logged out + qDebug() << "account-notify:" << ircuser->nick() << "logged out"; + } + */ + } else { + qDebug() << "Received account-notify data for unknown user" << e->prefix(); + } +} + +/* IRCv3 away-notify - ":nick!user@host AWAY [:message]" */ +void CoreSessionEventProcessor::processIrcEventAway(IrcEvent *e) +{ + if (!checkParamCount(e, 2)) + return; + + // Nick is sent as part of parameters in order to split user/server decoding + IrcUser *ircuser = e->network()->ircUser(e->params().at(0)); + if (ircuser) { + if (!e->params().at(1).isEmpty()) { + ircuser->setAway(true); + ircuser->setAwayMessage(e->params().at(1)); + } else { + ircuser->setAway(false); + } + } else { + qDebug() << "Received away-notify data for unknown user" << e->params().at(0); + } +} void CoreSessionEventProcessor::processIrcEventInvite(IrcEvent *e) { @@ -182,6 +321,17 @@ void CoreSessionEventProcessor::processIrcEventJoin(IrcEvent *e) QString channel = e->params()[0]; IrcUser *ircuser = net->updateNickFromMask(e->prefix()); + if (net->useCapExtendedJoin()) { + if (!checkParamCount(e, 3)) + return; + // If logged in, :nick!user@host JOIN #channelname accountname :Real Name + // If logged out, :nick!user@host JOIN #channelname * :Real Name + // See: http://ircv3.net/specs/extensions/extended-join-3.1.html + // FIXME Keep track of authed user account, requires adding support to ircuser.h/cpp + ircuser->setRealName(e->params()[2]); + } + // Else :nick!user@host JOIN #channelname + bool handledByNetsplit = false; foreach(Netsplit* n, _netsplits.value(e->network())) { handledByNetsplit = n->userJoined(e->prefix(), channel); @@ -189,6 +339,12 @@ void CoreSessionEventProcessor::processIrcEventJoin(IrcEvent *e) break; } + // If using away-notify, check new users. Works around buggy IRC servers + // forgetting to send :away messages for users who join channels when away. + if (coreNetwork(e)->useCapAwayNotify()) { + coreNetwork(e)->queueAutoWhoOneshot(ircuser->nick()); + } + if (!handledByNetsplit) ircuser->joinChannel(channel); else @@ -763,14 +919,40 @@ void CoreSessionEventProcessor::processIrcEvent352(IrcEvent *e) ircuser->setUser(e->params()[1]); ircuser->setHost(e->params()[2]); - bool away = e->params()[5].startsWith("G"); + bool away = e->params()[5].contains("G", Qt::CaseInsensitive); ircuser->setAway(away); ircuser->setServer(e->params()[3]); ircuser->setRealName(e->params().last().section(" ", 1)); + + if (coreNetwork(e)->useCapMultiPrefix()) { + // If multi-prefix is enabled, all modes will be sent in WHO replies. + // :kenny.chatspike.net 352 guest #test grawity broken.symlink *.chatspike.net grawity H@%+ :0 Mantas M. + // See: http://ircv3.net/specs/extensions/multi-prefix-3.1.html + QString uncheckedModes = e->params()[5]; + QString validModes = QString(); + while (!uncheckedModes.isEmpty()) { + // Mode found in 1 left-most character, add it to the list + if (e->network()->prefixes().contains(uncheckedModes[0])) { + validModes.append(e->network()->prefixToMode(uncheckedModes[0])); + } + // Remove this mode from the list of unchecked modes + uncheckedModes = uncheckedModes.remove(0, 1); + } + + // Some IRC servers decide to not follow the spec, returning only -some- of the user + // modes in WHO despite listing them all in NAMES. For now, assume it can only add + // and not take away. *sigh* + if (!validModes.isEmpty()) + ircuser->addUserModes(validModes); + } } - if (coreNetwork(e)->isAutoWhoInProgress(channel)) + // Check if channel name has a who in progress. + // If not, then check if user nick exists and has a who in progress. + if (coreNetwork(e)->isAutoWhoInProgress(channel) || + (ircuser && coreNetwork(e)->isAutoWhoInProgress(ircuser->nick()))) { e->setFlag(EventManager::Silent); + } } @@ -793,14 +975,35 @@ void CoreSessionEventProcessor::processIrcEvent353(IrcEvent *e) QStringList nicks; QStringList modes; + // Cache result of multi-prefix to avoid unneeded casts and lookups with each iteration. + bool _useCapMultiPrefix = coreNetwork(e)->useCapMultiPrefix(); + foreach(QString nick, e->params()[2].split(' ', QString::SkipEmptyParts)) { QString mode; - if (e->network()->prefixes().contains(nick[0])) { + if (_useCapMultiPrefix) { + // If multi-prefix is enabled, all modes will be sent in NAMES replies. + // :hades.arpa 353 guest = #tethys :~&@%+aji &@Attila @+alyx +KindOne Argure + // See: http://ircv3.net/specs/extensions/multi-prefix-3.1.html + while (e->network()->prefixes().contains(nick[0])) { + // Mode found in 1 left-most character, add it to the list. + // Note: sending multiple modes may cause a warning in older clients. + // In testing, the clients still seemed to function fine. + mode.append(e->network()->prefixToMode(nick[0])); + // Remove this mode from the nick + nick = nick.remove(0, 1); + } + } else if (e->network()->prefixes().contains(nick[0])) { + // Multi-prefix is disabled and a mode prefix was found. mode = e->network()->prefixToMode(nick[0]); nick = nick.mid(1); } + // If userhost-in-names capability is enabled, the following will be + // in the form "nick!user@host" rather than "nick". This works without + // special handling as the following use nickFromHost() as needed. + // See: http://ircv3.net/specs/extensions/userhost-in-names-3.2.html + nicks << nick; modes << mode; } @@ -1062,7 +1265,7 @@ void CoreSessionEventProcessor::handleCtcpDcc(CtcpEvent *e) } // TODO: check if target is the right thing to use for the partner - CoreTransfer *transfer = new CoreTransfer(Transfer::Receive, e->target(), filename, address, port, size, this); + CoreTransfer *transfer = new CoreTransfer(Transfer::Direction::Receive, e->target(), filename, address, port, size, this); coreSession()->signalProxy()->synchronize(transfer); coreSession()->transferManager()->addTransfer(transfer); } diff --git a/src/core/coresessioneventprocessor.h b/src/core/coresessioneventprocessor.h index 43e6754c..0fd0c9e2 100644 --- a/src/core/coresessioneventprocessor.h +++ b/src/core/coresessioneventprocessor.h @@ -46,8 +46,10 @@ public: Q_INVOKABLE void processIrcEventNumeric(IrcEventNumeric *event); - Q_INVOKABLE void processIrcEventAuthenticate(IrcEvent *event); // SASL auth - Q_INVOKABLE void processIrcEventCap(IrcEvent *event); // CAP framework + Q_INVOKABLE void processIrcEventAuthenticate(IrcEvent *event); /// SASL authentication + Q_INVOKABLE void processIrcEventCap(IrcEvent *event); /// CAP framework negotiation + Q_INVOKABLE void processIrcEventAccount(IrcEvent *event); /// account-notify received + Q_INVOKABLE void processIrcEventAway(IrcEvent *event); /// away-notify received Q_INVOKABLE void processIrcEventInvite(IrcEvent *event); Q_INVOKABLE void processIrcEventJoin(IrcEvent *event); Q_INVOKABLE void lateProcessIrcEventKick(IrcEvent *event); @@ -152,6 +154,17 @@ private: // key: quit message // value: the corresponding netsplit object QHash > _netsplits; + + // IRCv3 capability negotiation + /** + * Sends the next capability from the queue. + * + * During nick registration if any capabilities remain queued, this will take the next and + * request it. When no capabilities remain, capability negotiation is ended. + * + * @param[in,out] A network currently undergoing capability negotiation + */ + void sendNextCap(Network *net); }; diff --git a/src/core/coretransfer.cpp b/src/core/coretransfer.cpp index 3ccb5a39..ea07b374 100644 --- a/src/core/coretransfer.cpp +++ b/src/core/coretransfer.cpp @@ -39,6 +39,12 @@ CoreTransfer::CoreTransfer(Direction direction, const QString &nick, const QStri } +quint64 CoreTransfer::transferred() const +{ + return _pos; +} + + void CoreTransfer::cleanUp() { if (_socket) { @@ -54,7 +60,7 @@ void CoreTransfer::cleanUp() void CoreTransfer::onSocketDisconnected() { - if (state() == Connecting || state() == Transferring) { + if (status() == Status::Connecting || status() == Status::Transferring) { setError(tr("Socket closed while still transferring!")); } else @@ -66,7 +72,7 @@ void CoreTransfer::onSocketError(QAbstractSocket::SocketError error) { Q_UNUSED(error) - if (state() == Connecting || state() == Transferring) { + if (status() == Status::Connecting || status() == Status::Transferring) { setError(tr("DCC connection error: %1").arg(_socket->errorString())); } } @@ -74,11 +80,11 @@ void CoreTransfer::onSocketError(QAbstractSocket::SocketError error) void CoreTransfer::requestAccepted(PeerPtr peer) { - if (_peer || !peer || state() != New) + if (_peer || !peer || status() != Status::New) return; // transfer was already accepted _peer = peer; - setState(Pending); + setStatus(Status::Pending); emit accepted(peer); @@ -89,11 +95,11 @@ void CoreTransfer::requestAccepted(PeerPtr peer) void CoreTransfer::requestRejected(PeerPtr peer) { - if (_peer || state() != New) + if (_peer || status() != Status::New) return; _peer = peer; - setState(Rejected); + setStatus(Status::Rejected); emit rejected(peer); } @@ -101,7 +107,7 @@ void CoreTransfer::requestRejected(PeerPtr peer) void CoreTransfer::start() { - if (!_peer || state() != Pending || direction() != Receive) + if (!_peer || status() != Status::Pending || direction() != Direction::Receive) return; setupConnectionForReceive(); @@ -115,7 +121,7 @@ void CoreTransfer::setupConnectionForReceive() return; } - setState(Connecting); + setStatus(Status::Connecting); _socket = new QTcpSocket(this); connect(_socket, SIGNAL(connected()), SLOT(startReceiving())); @@ -129,7 +135,7 @@ void CoreTransfer::setupConnectionForReceive() void CoreTransfer::startReceiving() { - setState(Transferring); + setStatus(Status::Transferring); } @@ -142,6 +148,7 @@ void CoreTransfer::onDataReceived() while (_socket->bytesAvailable()) { QByteArray data = _socket->read(chunkSize); _pos += data.size(); + emit transferredChanged(transferred()); if (!relayData(data, true)) return; @@ -162,7 +169,7 @@ void CoreTransfer::onDataReceived() else if (_pos == fileSize()) { qDebug() << "DCC Receive: Transfer finished"; if (relayData(QByteArray(), false)) // empty buffer - setState(Completed); + setStatus(Status::Completed); } _reading = false; @@ -180,7 +187,8 @@ bool CoreTransfer::relayData(const QByteArray &data, bool requireChunkSize) // we only want to send data to the client once we have reached the chunksize if (_buffer.size() > 0 && (_buffer.size() >= chunkSize || !requireChunkSize)) { - SYNC_OTHER(dataReceived, ARG(_peer), ARG(_buffer)); + Peer *p = _peer.data(); + SYNC_OTHER(dataReceived, ARG(p), ARG(_buffer)); _buffer.clear(); } diff --git a/src/core/coretransfer.h b/src/core/coretransfer.h index 3244efdf..66028b5f 100644 --- a/src/core/coretransfer.h +++ b/src/core/coretransfer.h @@ -36,12 +36,14 @@ class CoreTransfer : public Transfer public: CoreTransfer(Direction direction, const QString &nick, const QString &fileName, const QHostAddress &address, quint16 port, quint64 size = 0, QObject *parent = 0); + quint64 transferred() const override; + public slots: void start(); // called through sync calls - void requestAccepted(PeerPtr peer); - void requestRejected(PeerPtr peer); + void requestAccepted(PeerPtr peer) override; + void requestRejected(PeerPtr peer) override; private slots: void startReceiving(); @@ -52,7 +54,7 @@ private slots: private: void setupConnectionForReceive(); bool relayData(const QByteArray &data, bool requireChunkSize); - virtual void cleanUp(); + void cleanUp() override; QPointer _peer; QTcpSocket *_socket; diff --git a/src/core/coretransfermanager.cpp b/src/core/coretransfermanager.cpp index eb01263f..aa370992 100644 --- a/src/core/coretransfermanager.cpp +++ b/src/core/coretransfermanager.cpp @@ -30,12 +30,6 @@ CoreTransferManager::CoreTransferManager(QObject *parent) } -CoreTransfer *CoreTransferManager::transfer(const QUuid &uuid) const -{ - return qobject_cast(transfer_(uuid)); -} - - void CoreTransferManager::addTransfer(CoreTransfer *transfer) { TransferManager::addTransfer(transfer); diff --git a/src/core/coretransfermanager.h b/src/core/coretransfermanager.h index 8d8d5760..7553502c 100644 --- a/src/core/coretransfermanager.h +++ b/src/core/coretransfermanager.h @@ -34,8 +34,6 @@ class CoreTransferManager : public TransferManager public: CoreTransferManager(QObject *parent = 0); - CoreTransfer *transfer(const QUuid &uuid) const; - public slots: void addTransfer(CoreTransfer *transfer); diff --git a/src/core/ircparser.cpp b/src/core/ircparser.cpp index f5363554..681346c1 100644 --- a/src/core/ircparser.cpp +++ b/src/core/ircparser.cpp @@ -180,6 +180,7 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent *e) if (checkParamCount(cmd, params, 1)) { QString senderNick = nickFromMask(prefix); + net->updateNickFromMask(prefix); QByteArray msg = params.count() < 2 ? QByteArray() : params.at(1); QStringList targets = net->serverDecode(params.at(0)).split(',', QString::SkipEmptyParts); @@ -226,8 +227,10 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent *e) else { if (!target.isEmpty() && net->prefixes().contains(target.at(0))) target = target.mid(1); - if (!net->isChannelName(target)) + if (!net->isChannelName(target)) { target = nickFromMask(prefix); + net->updateNickFromMask(prefix); + } } #ifdef HAVE_QCA2 @@ -256,12 +259,14 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent *e) QString channel = net->serverDecode(params.at(0)); decParams << channel; decParams << net->userDecode(nickFromMask(prefix), params.at(1)); + net->updateNickFromMask(prefix); } break; case EventManager::IrcEventQuit: if (params.count() >= 1) { decParams << net->userDecode(nickFromMask(prefix), params.at(0)); + net->updateNickFromMask(prefix); } break; @@ -273,6 +278,15 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent *e) } break; + case EventManager::IrcEventAway: + { + QString nick = nickFromMask(prefix); + decParams << nick; + decParams << (params.count() >= 1 ? net->userDecode(nick, params.at(0)) : QString()); + net->updateNickFromMask(prefix); + } + break; + case EventManager::IrcEventNumeric: switch (num) { case 301: /* RPL_AWAY */ @@ -298,6 +312,17 @@ void IrcParser::processNetworkIncoming(NetworkDataEvent *e) decParams << net->channelDecode(channel, params.at(2)); } break; + case 451: /* You have not registered... */ + if (target.compare("CAP", Qt::CaseInsensitive) == 0) { + // :irc.server.com 451 CAP :You have not registered + // If server doesn't support capabilities, it will report this message. Turn it + // into a nicer message since it's not a real error. + defaultHandling = false; + events << new MessageEvent(Message::Server, e->network(), + tr("Capability negotiation not supported"), + QString(), QString(), Message::None, e->timestamp()); + } + break; } default: diff --git a/src/qtui/CMakeLists.txt b/src/qtui/CMakeLists.txt index 8510a715..eedb69c6 100644 --- a/src/qtui/CMakeLists.txt +++ b/src/qtui/CMakeLists.txt @@ -156,6 +156,12 @@ if (WITH_NOTIFICATION_CENTER) list(APPEND LIBS "/System/Library/Frameworks/Foundation.framework") endif() +if (KF5Sonnet_FOUND) + add_definitions(-DHAVE_SONNET) + list(APPEND SOURCES settingspages/sonnetsettingspage.cpp) + list(APPEND LIBS KF5::SonnetUi) +endif() + foreach(FORM ${FORMS}) set(FORMPATH ${FORMPATH} ui/${FORM}) endforeach(FORM ${FORMS}) diff --git a/src/qtui/mainwin.cpp b/src/qtui/mainwin.cpp index 95d2e6f6..e27c2fe2 100644 --- a/src/qtui/mainwin.cpp +++ b/src/qtui/mainwin.cpp @@ -153,6 +153,11 @@ # include "settingspages/shortcutssettingspage.h" #endif +#ifdef HAVE_SONNET +# include "settingspages/sonnetsettingspage.h" +#endif + + MainWin::MainWin(QWidget *parent) #ifdef HAVE_KDE : KMainWindow(parent), _kHelpMenu(new KHelpMenu(this)), @@ -1367,6 +1372,9 @@ void MainWin::showSettingsDlg() dlg->registerSettingsPage(new BufferViewSettingsPage(dlg)); dlg->registerSettingsPage(new InputWidgetSettingsPage(dlg)); dlg->registerSettingsPage(new TopicWidgetSettingsPage(dlg)); +#ifdef HAVE_SONNET + dlg->registerSettingsPage(new SonnetSettingsPage(dlg)); +#endif dlg->registerSettingsPage(new HighlightSettingsPage(dlg)); dlg->registerSettingsPage(new NotificationsSettingsPage(dlg)); dlg->registerSettingsPage(new BacklogSettingsPage(dlg)); diff --git a/src/qtui/nicklistwidget.h b/src/qtui/nicklistwidget.h index 84fc4325..c2a626e8 100644 --- a/src/qtui/nicklistwidget.h +++ b/src/qtui/nicklistwidget.h @@ -49,13 +49,13 @@ signals: void nickSelectionChanged(const QModelIndexList &); protected: - virtual QSize sizeHint() const; - virtual void hideEvent(QHideEvent *); - virtual void showEvent(QShowEvent *); + QSize sizeHint() const override; + void hideEvent(QHideEvent *) override; + void showEvent(QShowEvent *) override; protected slots: - virtual void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); - virtual void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override; + void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) override; private slots: void removeBuffer(BufferId bufferId); diff --git a/src/qtui/settingspages/backlogsettingspage.ui b/src/qtui/settingspages/backlogsettingspage.ui index 789fd4af..8405f1dd 100644 --- a/src/qtui/settingspages/backlogsettingspage.ui +++ b/src/qtui/settingspages/backlogsettingspage.ui @@ -135,7 +135,7 @@ - amount of messages per buffer that are requested after the core connection has been established. + Amount of messages per buffer that are requested after the core connection has been established. Initial backlog amount: diff --git a/src/qtui/settingspages/chatviewsettingspage.ui b/src/qtui/settingspages/chatviewsettingspage.ui index 250022fa..208e7a55 100644 --- a/src/qtui/settingspages/chatviewsettingspage.ui +++ b/src/qtui/settingspages/chatviewsettingspage.ui @@ -177,7 +177,7 @@ - Web Search Url: + Web Search URL: diff --git a/src/qtui/settingspages/coreaccounteditdlg.ui b/src/qtui/settingspages/coreaccounteditdlg.ui index 379b6f23..647366f3 100644 --- a/src/qtui/settingspages/coreaccounteditdlg.ui +++ b/src/qtui/settingspages/coreaccounteditdlg.ui @@ -144,7 +144,7 @@ - Socks 5 + SOCKS 5 diff --git a/src/qtui/settingspages/networkssettingspage.ui b/src/qtui/settingspages/networkssettingspage.ui index 7ef574ac..5fa1c469 100644 --- a/src/qtui/settingspages/networkssettingspage.ui +++ b/src/qtui/settingspages/networkssettingspage.ui @@ -713,8 +713,8 @@ UTF-8 should be a sane choice for most networks. - Incoming messages encoded in Utf8 will always be treated as such. -This setting defines the encoding for messages that are not Utf8. + Incoming messages encoded in UTF-8 will always be treated as such. +This setting defines the encoding for messages that are not UTF-8. Receive fallback: @@ -733,8 +733,8 @@ This setting defines the encoding for messages that are not Utf8. - Incoming messages encoded in Utf8 will always be treated as such. -This setting defines the encoding for messages that are not Utf8. + Incoming messages encoded in UTF-8 will always be treated as such. +This setting defines the encoding for messages that are not UTF-8. QComboBox::InsertAlphabetically diff --git a/src/qtui/settingspages/servereditdlg.ui b/src/qtui/settingspages/servereditdlg.ui index 29177102..2627f689 100644 --- a/src/qtui/settingspages/servereditdlg.ui +++ b/src/qtui/settingspages/servereditdlg.ui @@ -193,7 +193,7 @@ - Socks 5 + SOCKS 5 diff --git a/src/qtui/settingspages/sonnetsettingspage.cpp b/src/qtui/settingspages/sonnetsettingspage.cpp new file mode 100644 index 00000000..73b739d6 --- /dev/null +++ b/src/qtui/settingspages/sonnetsettingspage.cpp @@ -0,0 +1,67 @@ +/*************************************************************************** + * Copyright (C) 2005-2014 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "sonnetsettingspage.h" + +#include + +#include "qtui.h" + +SonnetSettingsPage::SonnetSettingsPage(QWidget *parent) + : SettingsPage(tr("Interface"), tr("Spell Checking"), parent) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + _configWidget = new Sonnet::ConfigWidget(this); + layout->addWidget(_configWidget); + connect(_configWidget, SIGNAL(configChanged()), SLOT(widgetHasChanged())); +} + + +bool SonnetSettingsPage::hasDefaults() const +{ + return true; +} + + +void SonnetSettingsPage::defaults() +{ + _configWidget->slotDefault(); + widgetHasChanged(); +} + + +void SonnetSettingsPage::load() +{ + +} + + +void SonnetSettingsPage::save() +{ + _configWidget->save(); + setChangedState(false); +} + + +void SonnetSettingsPage::widgetHasChanged() +{ + if (!hasChanged()) + setChangedState(true); +} diff --git a/src/qtui/settingspages/sonnetsettingspage.h b/src/qtui/settingspages/sonnetsettingspage.h new file mode 100644 index 00000000..c9ad34a7 --- /dev/null +++ b/src/qtui/settingspages/sonnetsettingspage.h @@ -0,0 +1,47 @@ +/*************************************************************************** + * Copyright (C) 2005-2014 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#pragma once + +#include + +#include "settingspage.h" + +//! A settings page for configuring the Sonnet spell-checking engine +class SonnetSettingsPage : public SettingsPage +{ + Q_OBJECT + +public: + SonnetSettingsPage(QWidget *parent = 0); + + bool hasDefaults() const; + +public slots: + void save(); + void load(); + void defaults(); + +private slots: + void widgetHasChanged(); + +private: + Sonnet::ConfigWidget *_configWidget; +}; diff --git a/src/uisupport/CMakeLists.txt b/src/uisupport/CMakeLists.txt index 37ea3114..a72a9c73 100644 --- a/src/uisupport/CMakeLists.txt +++ b/src/uisupport/CMakeLists.txt @@ -60,4 +60,7 @@ endif() if (WITH_KF5) target_link_libraries(mod_uisupport KF5::CoreAddons KF5::TextWidgets KF5::XmlGui) +elseif (KF5Sonnet_FOUND) + add_definitions(-DHAVE_SONNET) + target_link_libraries(mod_uisupport KF5::SonnetUi) endif() diff --git a/src/uisupport/multilineedit.cpp b/src/uisupport/multilineedit.cpp index 37f1f62a..68de3578 100644 --- a/src/uisupport/multilineedit.cpp +++ b/src/uisupport/multilineedit.cpp @@ -19,10 +19,13 @@ ***************************************************************************/ #include -#include #include #include +#ifdef HAVE_SONNET +# include +#endif + #include "actioncollection.h" #include "bufferview.h" #include "graphicalui.h" @@ -51,6 +54,10 @@ MultiLineEdit::MultiLineEdit(QWidget *parent) enableFindReplace(false); #endif +#ifdef HAVE_SONNET + new Sonnet::SpellCheckDecorator(this); +#endif + setMode(SingleLine); setLineWrapEnabled(false); reset();