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
#####################################################################
PURPOSE "Enable support for the snorenotify framework"
)
endif()
-
+
if (WITH_WEBKIT)
find_package(Qt5WebKit QUIET)
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)
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
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
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
: 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;
}
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;
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;
}
-const ClientTransfer *ClientTransferManager::transfer(const QUuid &uuid) const
-{
- return qobject_cast<const ClientTransfer *>(transfer_(uuid));
-}
-
-
void ClientTransferManager::onCoreTransferAdded(const QUuid &uuid)
{
if (uuid.isNull()) {
public:
ClientTransferManager(QObject *parent = 0);
- const ClientTransfer *transfer(const QUuid &uuid) const;
-
public slots:
void onCoreTransferAdded(const QUuid &uuid);
void onTransferInitDone();
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 {
IrcEvent = 0x00030000,
IrcEventAuthenticate,
+ IrcEventAccount,
+ IrcEventAway,
IrcEventCap,
IrcEventInvite,
IrcEventJoin,
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;
}
// 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;
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);
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);
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);
+ }
}
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);
+ }
}
qRegisterMetaType<MsgId>("MsgId");
qRegisterMetaType<QHostAddress>("QHostAddress");
+ qRegisterMetaTypeStreamOperators<QHostAddress>("QHostAddress");
qRegisterMetaType<QUuid>("QUuid");
+ qRegisterMetaTypeStreamOperators<QUuid>("QUuid");
qRegisterMetaTypeStreamOperators<IdentityId>("IdentityId");
qRegisterMetaTypeStreamOperators<BufferId>("BufferId");
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)
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),
void Transfer::init()
{
+ static auto regTypes = []() -> bool {
+ qRegisterMetaType<Status>("Transfer::Status");
+ qRegisterMetaType<Direction>("Transfer::Direction");
+ qRegisterMetaTypeStreamOperators<Status>("Transfer::Status");
+ qRegisterMetaTypeStreamOperators<Direction>("Transfer::Direction");
+ return true;
+ }();
+ Q_UNUSED(regTypes);
+
renameObject(QString("Transfer/%1").arg(_uuid.toString()));
setAllowClientUpdates(true);
}
}
-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();
}
{
qWarning() << Q_FUNC_INFO << errorString;
emit error(errorString);
- setState(Failed);
+ setStatus(Status::Failed);
cleanUp();
}
+
+
+QDataStream &operator<<(QDataStream &out, Transfer::Status state) {
+ out << static_cast<qint8>(state);
+ return out;
+}
+
+QDataStream &operator>>(QDataStream &in, Transfer::Status &state) {
+ qint8 s;
+ in >> s;
+ state = static_cast<Transfer::Status>(s);
+ return in;
+}
+
+QDataStream &operator<<(QDataStream &out, Transfer::Direction direction) {
+ out << static_cast<qint8>(direction);
+ return out;
+}
+
+QDataStream &operator>>(QDataStream &in, Transfer::Direction &direction) {
+ qint8 d;
+ in >> d;
+ direction = static_cast<Transfer::Direction>(d);
+ return in;
+}
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);
Q_PROPERTY(QString nick READ nick WRITE setNick NOTIFY nickChanged);
public:
- enum State {
+ enum class Status {
New,
Pending,
Connecting,
};
Q_ENUMS(State)
- enum Direction {
+ enum class Direction {
Send,
Receive
};
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;
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); }
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);
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
void setNick(const QString &nick);
- State _state;
+ Status _status;
Direction _direction;
QString _fileName;
QHostAddress _address;
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
}
-Transfer *TransferManager::transfer_(const QUuid &uuid) const
+Transfer *TransferManager::transfer(const QUuid &uuid) const
{
- return _transfers.value(uuid, 0);
+ return _transfers.value(uuid, nullptr);
}
TransferManager(QObject *parent = 0);
inline virtual const QMetaObject *syncMetaObject() const { return &staticMetaObject; }
+ Transfer *transfer(const QUuid &uuid) const;
QList<QUuid> transferIds() const;
signals:
void transferAdded(const Transfer *transfer);
protected:
- Transfer *transfer_(const QUuid &uuid) const;
void addTransfer(Transfer *transfer);
protected slots:
// 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();
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()]);
_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)));
}
_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 ********/
_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)
{
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();
}
}
QList<QList<QByteArray>> splitMessage(const QString &cmd, const QString &message, std::function<QList<QByteArray>(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);
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);
QHash<QString, int> _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<QString, QString> _capsPending; /// Capabilities pending 'CAP ACK' from server
+ QHash<QString, QString> _capsSupported; /// Enabled capabilities that received 'CAP ACK'
+
QTimer _tokenBucketTimer;
int _messageDelay; // token refill speed in ms
int _burstSize; // size of the token bucket
void CoreSessionEventProcessor::processIrcEventNumeric(IrcEventNumeric *e)
{
switch (e->number()) {
- // CAP stuff
- case 903:
- case 904:
- case 905:
- case 906:
- case 907:
- qobject_cast<CoreNetwork *>(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:
#endif
}
+void CoreSessionEventProcessor::sendNextCap(Network *net)
+{
+ CoreNetwork *coreNet = qobject_cast<CoreNetwork *>(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)
{
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);
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
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);
+ }
}
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;
}
}
// 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);
}
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);
// key: quit message
// value: the corresponding netsplit object
QHash<Network *, QHash<QString, Netsplit *> > _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);
};
}
+quint64 CoreTransfer::transferred() const
+{
+ return _pos;
+}
+
+
void CoreTransfer::cleanUp()
{
if (_socket) {
void CoreTransfer::onSocketDisconnected()
{
- if (state() == Connecting || state() == Transferring) {
+ if (status() == Status::Connecting || status() == Status::Transferring) {
setError(tr("Socket closed while still transferring!"));
}
else
{
Q_UNUSED(error)
- if (state() == Connecting || state() == Transferring) {
+ if (status() == Status::Connecting || status() == Status::Transferring) {
setError(tr("DCC connection error: %1").arg(_socket->errorString()));
}
}
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);
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);
}
void CoreTransfer::start()
{
- if (!_peer || state() != Pending || direction() != Receive)
+ if (!_peer || status() != Status::Pending || direction() != Direction::Receive)
return;
setupConnectionForReceive();
return;
}
- setState(Connecting);
+ setStatus(Status::Connecting);
_socket = new QTcpSocket(this);
connect(_socket, SIGNAL(connected()), SLOT(startReceiving()));
void CoreTransfer::startReceiving()
{
- setState(Transferring);
+ setStatus(Status::Transferring);
}
while (_socket->bytesAvailable()) {
QByteArray data = _socket->read(chunkSize);
_pos += data.size();
+ emit transferredChanged(transferred());
if (!relayData(data, true))
return;
else if (_pos == fileSize()) {
qDebug() << "DCC Receive: Transfer finished";
if (relayData(QByteArray(), false)) // empty buffer
- setState(Completed);
+ setStatus(Status::Completed);
}
_reading = false;
// 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();
}
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();
private:
void setupConnectionForReceive();
bool relayData(const QByteArray &data, bool requireChunkSize);
- virtual void cleanUp();
+ void cleanUp() override;
QPointer<Peer> _peer;
QTcpSocket *_socket;
}
-CoreTransfer *CoreTransferManager::transfer(const QUuid &uuid) const
-{
- return qobject_cast<CoreTransfer *>(transfer_(uuid));
-}
-
-
void CoreTransferManager::addTransfer(CoreTransfer *transfer)
{
TransferManager::addTransfer(transfer);
public:
CoreTransferManager(QObject *parent = 0);
- CoreTransfer *transfer(const QUuid &uuid) const;
-
public slots:
void addTransfer(CoreTransfer *transfer);
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);
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
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;
}
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 */
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:
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})
# 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)),
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));
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);
<item>
<widget class="QLabel" name="label">
<property name="toolTip">
- <string>amount of messages per buffer that are requested after the core connection has been established.</string>
+ <string>Amount of messages per buffer that are requested after the core connection has been established.</string>
</property>
<property name="text">
<string>Initial backlog amount:</string>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
- <string>Web Search Url:</string>
+ <string>Web Search URL:</string>
</property>
</widget>
</item>
<widget class="QComboBox" name="proxyType">
<item>
<property name="text">
- <string>Socks 5</string>
+ <string>SOCKS 5</string>
</property>
</item>
<item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="toolTip">
- <string>Incoming messages encoded in Utf8 will always be treated as such.
-This setting defines the encoding for messages that are not Utf8.</string>
+ <string>Incoming messages encoded in UTF-8 will always be treated as such.
+This setting defines the encoding for messages that are not UTF-8.</string>
</property>
<property name="text">
<string>Receive fallback:</string>
</sizepolicy>
</property>
<property name="toolTip">
- <string>Incoming messages encoded in Utf8 will always be treated as such.
-This setting defines the encoding for messages that are not Utf8.</string>
+ <string>Incoming messages encoded in UTF-8 will always be treated as such.
+This setting defines the encoding for messages that are not UTF-8.</string>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum>
<widget class="QComboBox" name="proxyType">
<item>
<property name="text">
- <string>Socks 5</string>
+ <string>SOCKS 5</string>
</property>
</item>
<item>
--- /dev/null
+/***************************************************************************
+ * 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 <QVBoxLayout>
+
+#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);
+}
--- /dev/null
+/***************************************************************************
+ * 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 <Sonnet/ConfigWidget>
+
+#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;
+};
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()
***************************************************************************/
#include <QApplication>
-#include <QMenu>
#include <QMessageBox>
#include <QScrollBar>
+#ifdef HAVE_SONNET
+# include <Sonnet/SpellCheckDecorator>
+#endif
+
#include "actioncollection.h"
#include "bufferview.h"
#include "graphicalui.h"
enableFindReplace(false);
#endif
+#ifdef HAVE_SONNET
+ new Sonnet::SpellCheckDecorator(this);
+#endif
+
setMode(SingleLine);
setLineWrapEnabled(false);
reset();