diff --git a/iris/include/iris/xmpp_carbons.h b/iris/include/iris/xmpp_carbons.h new file mode 100644 index 000000000..444f2293e --- /dev/null +++ b/iris/include/iris/xmpp_carbons.h @@ -0,0 +1 @@ +#include "../../src/xmpp/xmpp-im/xmpp_carbons.h" diff --git a/iris/include/iris/xmpp_forwarding.h b/iris/include/iris/xmpp_forwarding.h new file mode 100644 index 000000000..6225ec945 --- /dev/null +++ b/iris/include/iris/xmpp_forwarding.h @@ -0,0 +1 @@ +#include "../../src/xmpp/xmpp-im/xmpp_forwarding.h" diff --git a/iris/include/iris/xmpp_mammanager.h b/iris/include/iris/xmpp_mammanager.h new file mode 100644 index 000000000..15450e523 --- /dev/null +++ b/iris/include/iris/xmpp_mammanager.h @@ -0,0 +1 @@ +#include "xmpp/xmpp-im/xmpp_mammanager.h" diff --git a/iris/include/iris/xmpp_mamtask.h b/iris/include/iris/xmpp_mamtask.h new file mode 100644 index 000000000..f9ccc0c85 --- /dev/null +++ b/iris/include/iris/xmpp_mamtask.h @@ -0,0 +1 @@ +#include "xmpp/xmpp-im/xmpp_mamtask.h" diff --git a/iris/src/xmpp/CMakeLists.txt b/iris/src/xmpp/CMakeLists.txt index 2d754e6f5..69b4b74f9 100644 --- a/iris/src/xmpp/CMakeLists.txt +++ b/iris/src/xmpp/CMakeLists.txt @@ -31,10 +31,12 @@ set(XMPP_IM_HEADERS xmpp-im/xmpp_thumbs.h xmpp-im/xmpp_agentitem.h xmpp-im/xmpp_captcha.h + xmpp-im/xmpp_carbons.h xmpp-im/xmpp_chatstate.h xmpp-im/xmpp_discoitem.h xmpp-im/xmpp_features.h xmpp-im/xmpp_form.h + xmpp-im/xmpp_forwarding.h xmpp-im/xmpp_htmlelement.h xmpp-im/xmpp_httpauthrequest.h xmpp-im/xmpp_liveroster.h @@ -69,6 +71,8 @@ set(XMPP_IM_HEADERS xmpp-im/xmpp_bytestream.h xmpp-im/xmpp_client.h xmpp-im/xmpp_discoinfotask.h + xmpp-im/xmpp_mamtask.h + xmpp-im/xmpp_mammanager.h xmpp-im/xmpp_ibb.h xmpp-im/xmpp_serverinfomanager.h xmpp-im/xmpp_task.h @@ -121,10 +125,14 @@ target_sources(iris PRIVATE xmpp-im/xmpp_bitsofbinary.cpp xmpp-im/xmpp_bytestream.cpp xmpp-im/xmpp_caps.cpp + xmpp-im/xmpp_carbons.cpp xmpp-im/xmpp_discoinfotask.cpp xmpp-im/xmpp_discoitem.cpp xmpp-im/xmpp_hash.cpp xmpp-im/xmpp_ibb.cpp + xmpp-im/xmpp_forwarding.cpp + xmpp-im/xmpp_mamtask.cpp + xmpp-im/xmpp_mammanager.cpp xmpp-im/xmpp_reference.cpp xmpp-im/xmpp_serverinfomanager.cpp xmpp-im/xmpp_subsets.cpp diff --git a/iris/src/xmpp/xmpp-im/client.cpp b/iris/src/xmpp/xmpp-im/client.cpp index f9851b45e..36b4a84ed 100644 --- a/iris/src/xmpp/xmpp-im/client.cpp +++ b/iris/src/xmpp/xmpp-im/client.cpp @@ -79,6 +79,7 @@ #include "xmpp/xmpp-core/protocol.h" #include "xmpp_bitsofbinary.h" #include "xmpp_caps.h" +#include "xmpp_carbons.h" #include "xmpp_externalservicediscovery.h" #include "xmpp_hash.h" #include "xmpp_ibb.h" @@ -134,6 +135,7 @@ class Client::ClientPrivate { LiveRoster roster; ResourceList resourceList; CapsManager *capsman = nullptr; + CarbonsManager *carbonsman = nullptr; TcpPortReserver *tcpPortReserver = nullptr; S5BManager *s5bman = nullptr; Jingle::S5B::Manager *jingleS5BManager = nullptr; @@ -149,6 +151,7 @@ class Client::ClientPrivate { Jingle::Manager *jingleManager = nullptr; QList groupChatList; EncryptionHandler *encryptionHandler = nullptr; + JT_PushMessage *pushMessage = nullptr; }; Client::Client(QObject *par) : QObject(par) @@ -237,8 +240,9 @@ void Client::start(const QString &host, const QString &user, const QString &pass connect(pp, SIGNAL(subscription(Jid, QString, QString)), SLOT(ppSubscription(Jid, QString, QString))); connect(pp, SIGNAL(presence(Jid, Status)), SLOT(ppPresence(Jid, Status))); - JT_PushMessage *pm = new JT_PushMessage(rootTask(), d->encryptionHandler); - connect(pm, SIGNAL(message(Message)), SLOT(pmMessage(Message))); + d->pushMessage = new JT_PushMessage(rootTask(), d->encryptionHandler); + connect(d->pushMessage, SIGNAL(message(Message)), SLOT(pmMessage(Message))); + d->carbonsman = new CarbonsManager(d->pushMessage); JT_PushRoster *pr = new JT_PushRoster(rootTask()); connect(pr, SIGNAL(roster(Roster)), SLOT(prRoster(Roster))); @@ -305,6 +309,10 @@ Jingle::Manager *Client::jingleManager() const { return d->jingleManager; } bool Client::isActive() const { return d->active; } +CarbonsManager *Client::carbonsManager() const { return d->carbonsman; } + +JT_PushMessage *Client::pushMessage() const { return d->pushMessage; } + QString Client::groupChatPassword(const QString &host, const QString &room) const { Jid jid(room + "@" + host); diff --git a/iris/src/xmpp/xmpp-im/types.cpp b/iris/src/xmpp/xmpp-im/types.cpp index db99cd2d0..14e92c508 100644 --- a/iris/src/xmpp/xmpp-im/types.cpp +++ b/iris/src/xmpp/xmpp-im/types.cpp @@ -21,7 +21,9 @@ #include "xmpp/xmpp-core/protocol.h" #include "xmpp_bitsofbinary.h" #include "xmpp_captcha.h" +#include "xmpp_carbons.h" #include "xmpp_features.h" +#include "xmpp_forwarding.h" #include "xmpp_ibb.h" #include "xmpp_reference.h" #include "xmpp_xmlcommon.h" @@ -732,7 +734,6 @@ class Message::Private : public QSharedData { QMap htmlElements; QDomElement sxe; QList bobDataList; - Jid forwardedFrom; QList mucStatuses; QList mucInvites; @@ -743,14 +744,14 @@ class Message::Private : public QSharedData { bool spooled = false, wasEncrypted = false; // XEP-0280 Message Carbons - bool isDisabledCarbons = false; - Message::CarbonDir carbonDir = Message::NoCarbon; // it's a forwarded message + bool carbonsPrivate = false; Message::ProcessingHints processingHints; QString replaceId; QString originId; // XEP-0359 QString encryptionProtocol; // XEP-0380 Message::StanzaId stanzaId; // XEP-0359 QList references; // XEP-0385 and XEP-0372 + Forwarding forwarding; // XEP-0297 Message::Reactions reactions; // XEP-0444 QString retraction; // XEP-0424 }; @@ -1134,17 +1135,44 @@ QList Message::bobDataList() const { return d ? d->bobDataList : QList< IBBData Message::ibbData() const { return d ? d->ibbData : IBBData(); } -void Message::setDisabledCarbons(bool disabled) { MessageD()->isDisabledCarbons = disabled; } +//! \brief Returns Jid of the remote contact +//! +//! Returns Jid of the remote contact for the original message +//! which may be wrapped using carbons. It is useful when a client +//! needs to know in which window it should display the message. +//! So it is not always just from(). +Jid Message::displayJid() const +{ + if (!d) + return Jid(); + + switch (d->forwarding.type()) { + case Forwarding::ForwardedCarbonsSent: + return d->forwarding.message().to(); + case Forwarding::ForwardedCarbonsReceived: + return d->forwarding.message().from(); + default: + break; + } + return from(); +} + +//! \brief Returns either the message inside the carbons or itself. +Message Message::displayMessage() const +{ + if (d && d->forwarding.isCarbons()) + return d->forwarding.message(); -bool Message::isDisabledCarbons() const { return d && d->isDisabledCarbons; } + return *this; +} -void Message::setCarbonDirection(Message::CarbonDir cd) { MessageD()->carbonDir = cd; } +void Message::setCarbonsPrivate(bool enable) { MessageD()->carbonsPrivate = enable; } -Message::CarbonDir Message::carbonDirection() const { return d ? d->carbonDir : NoCarbon; } +bool Message::carbonsPrivate() const { return (d && d->carbonsPrivate); } -void Message::setForwardedFrom(const Jid &jid) { MessageD()->forwardedFrom = jid; } +void Message::setForwarded(const Forwarding &frw) { MessageD()->forwarding = frw; } -Jid Message::forwardedFrom() const { return d ? d->forwardedFrom : Jid(); } +const Forwarding &Message::forwarded() const { return d->forwarding; } bool Message::spooled() const { return d && d->spooled; } @@ -1400,10 +1428,10 @@ Stanza Message::toStanza(Stream *stream) const } // Avoiding Carbons - if (isDisabledCarbons()) { - QDomElement e = s.createElement("urn:xmpp:carbons:2", "private"); - s.appendChild(e); + if (d->carbonsPrivate) { + s.appendChild(CarbonsManager::privateElement(stream->doc())); } + if (!d->replaceId.isEmpty()) { QDomElement e = s.createElement("urn:xmpp:message-correct:0", "replace"); e.setAttribute("id", d->replaceId); @@ -1440,6 +1468,10 @@ Stanza Message::toStanza(Stream *stream) const s.appendChild(e); } + // XEP-0297: Stanza Forwarding + if (d->forwarding.type() != Forwarding::ForwardedNone) + s.appendChild(d->forwarding.toXml(stream)); + // XEP-0372 and XEP-0385 for (auto const &r : std::as_const(d->references)) { s.appendChild(r.toXml(&s.doc())); diff --git a/iris/src/xmpp/xmpp-im/xmpp_carbons.cpp b/iris/src/xmpp/xmpp-im/xmpp_carbons.cpp new file mode 100644 index 000000000..ba4cd2114 --- /dev/null +++ b/iris/src/xmpp/xmpp-im/xmpp_carbons.cpp @@ -0,0 +1,220 @@ +/* + * xmpp_carbons.cpp - Message Carbons (XEP-0280) + * Copyright (C) 2019 Aleksey Andreev + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include + +#include "xmpp_carbons.h" +#include "xmpp_client.h" +#include "xmpp_forwarding.h" +#include "xmpp_message.h" +#include "xmpp_task.h" +#include "xmpp_tasks.h" +#include "xmpp_xmlcommon.h" + +namespace XMPP { + +static const QString xmlns_carbons(QStringLiteral("urn:xmpp:carbons:2")); + +class CarbonsSubscriber : public JT_PushMessage::Subscriber { +public: + bool xmlEvent(const QDomElement &root, QDomElement &e, Client *client, int userData, bool nested) override; + bool messageEvent(Message &msg, int userData, bool nested) override; + +private: + Forwarding frw; +}; + +class JT_MessageCarbons : public Task { + Q_OBJECT + +public: + JT_MessageCarbons(Task *parent); + + void enable(); + void disable(); + + void onGo() override; + bool take(const QDomElement &e) override; + +private: + QDomElement iq; +}; + +//---------------------------------------------------------------------------- +// JT_MessageCarbons +//---------------------------------------------------------------------------- +JT_MessageCarbons::JT_MessageCarbons(Task *parent) : Task(parent) { } + +void JT_MessageCarbons::enable() +{ + iq = createIQ(doc(), QString::fromLatin1("set"), QString(), id()); + QDomElement enable = doc()->createElement(QString::fromLatin1("enable")); + enable.setAttribute(QString::fromLatin1("xmlns"), xmlns_carbons); + iq.appendChild(enable); +} + +void JT_MessageCarbons::disable() +{ + iq = createIQ(doc(), QString::fromLatin1("set"), QString(), id()); + QDomElement disable = doc()->createElement(QString::fromLatin1("disable")); + disable.setAttribute(QString::fromLatin1("xmlns"), xmlns_carbons); + iq.appendChild(disable); +} + +void JT_MessageCarbons::onGo() +{ + if (!iq.isNull()) + send(iq); +} + +bool JT_MessageCarbons::take(const QDomElement &e) +{ + if (iqVerify(e, Jid(), id())) { + if (e.attribute(QString::fromLatin1("type")) != QString::fromLatin1("result")) + setError(e); + else + setSuccess(); + return true; + } + return false; +} + +//-------------------------------------------------- +// class CarbonsSubscriber +//-------------------------------------------------- + +bool CarbonsSubscriber::xmlEvent(const QDomElement &root, QDomElement &e, Client *client, int userData, bool nested) +{ + bool drop = false; + frw.setType(Forwarding::ForwardedNone); + if (!nested) { + Jid from(root.attribute(QStringLiteral("from"))); + Jid to(root.attribute(QStringLiteral("to"))); + if (from.resource().isEmpty() && from.compare(to, false)) { + QDomElement child = e.firstChildElement(); + while (!child.isNull()) { + if (frw.fromXml(child, client)) { + frw.setType(static_cast(userData)); + break; + } + child = child.nextSiblingElement(); + } + } else + drop = true; + e = QDomElement(); + } + return drop; +} + +bool CarbonsSubscriber::messageEvent(Message &msg, int userData, bool nested) +{ + Q_UNUSED(userData) + if (!nested && frw.type() != Forwarding::ForwardedNone) { + msg.setForwarded(frw); + frw.setType(Forwarding::ForwardedNone); + } + return false; +} + +//-------------------------------------------------- +// class CarbonsManager +//-------------------------------------------------- + +class CarbonsManager::Private { +public: + ~Private() + { + // if (sbs.get()) + // unsubscribe(); + } + + void subscribe() + { + push_m->subscribeXml(sbs.get(), QString::fromLatin1("received"), xmlns_carbons, + Forwarding::ForwardedCarbonsReceived); + push_m->subscribeXml(sbs.get(), QString::fromLatin1("sent"), xmlns_carbons, Forwarding::ForwardedCarbonsSent); + push_m->subscribeMessage(sbs.get(), 0); + } + + void unsubscribe() + { + push_m->unsubscribeXml(sbs.get(), QString::fromLatin1("received"), xmlns_carbons); + push_m->unsubscribeXml(sbs.get(), QString::fromLatin1("sent"), xmlns_carbons); + push_m->unsubscribeMessage(sbs.get()); + } + + JT_PushMessage *push_m; + std::unique_ptr sbs; + bool enable = false; +}; + +CarbonsManager::CarbonsManager(JT_PushMessage *push_m) : QObject(push_m), d(new Private) +{ + d->push_m = push_m; + d->sbs.reset(new CarbonsSubscriber()); +} + +CarbonsManager::~CarbonsManager() { } + +QDomElement CarbonsManager::privateElement(QDomDocument &doc) +{ + return doc.createElementNS(xmlns_carbons, QString::fromLatin1("private")); +} + +void CarbonsManager::setEnabled(bool enable) +{ + if (d->enable == enable) + return; + + if (enable) { + d->subscribe(); + JT_MessageCarbons *jt = new JT_MessageCarbons(d->push_m->client()->rootTask()); + connect( + jt, &JT_MessageCarbons::finished, this, + [this, jt]() { + if (jt->success()) + d->enable = true; + else + d->unsubscribe(); + emit finished(); + }, + Qt::QueuedConnection); + jt->enable(); + jt->go(true); + } else { + JT_MessageCarbons *jt = new JT_MessageCarbons(d->push_m->client()->rootTask()); + connect( + jt, &JT_MessageCarbons::finished, this, + [this]() { + d->enable = false; + d->unsubscribe(); + emit finished(); + }, + Qt::QueuedConnection); + jt->disable(); + jt->go(true); + } +} + +bool CarbonsManager::isEnabled() const { return d->enable; } + +} // namespace XMPP + +#include "xmpp_carbons.moc" diff --git a/iris/src/xmpp/xmpp-im/xmpp_carbons.h b/iris/src/xmpp/xmpp-im/xmpp_carbons.h new file mode 100644 index 000000000..20cbc671a --- /dev/null +++ b/iris/src/xmpp/xmpp-im/xmpp_carbons.h @@ -0,0 +1,60 @@ +/* + * xmpp_carbons.h - Message Carbons (XEP-0280) + * Copyright (C) 2019 Aleksey Andreev + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef XMPP_CARBONS_H +#define XMPP_CARBONS_H + +#include +#include + +class QDomDocument; +class QDomElement; + +namespace XMPP +{ + class Task; + class Client; + class JT_PushMessage; + + class CarbonsManager : public QObject + { + Q_OBJECT + + public: + CarbonsManager(JT_PushMessage *push_m); + CarbonsManager(const CarbonsManager &) = delete; + CarbonsManager & operator=(const CarbonsManager &) = delete; + ~CarbonsManager(); + + static QDomElement privateElement(QDomDocument &doc); + + void setEnabled(bool enable); + bool isEnabled() const; + + signals: + void finished(); + + private: + class Private; + std::unique_ptr d; + }; +} + +#endif diff --git a/iris/src/xmpp/xmpp-im/xmpp_client.h b/iris/src/xmpp/xmpp-im/xmpp_client.h index 58e5f9189..1cd7e39a8 100644 --- a/iris/src/xmpp/xmpp-im/xmpp_client.h +++ b/iris/src/xmpp/xmpp-im/xmpp_client.h @@ -31,17 +31,18 @@ class ByteStream; class QDomDocument; class QDomElement; class QNetworkAccessManager; -class QString; namespace XMPP { class BSConnection; class CapsManager; +class CarbonsManager; class ClientStream; class EncryptionHandler; class Features; class FileTransferManager; class HttpFileUploadManager; class IBBManager; +class JT_PushMessage; class JidLinkManager; class LiveRoster; class LiveRosterItem; @@ -70,9 +71,7 @@ namespace Jingle { class Manager; } } -} -namespace XMPP { class Client : public QObject { Q_OBJECT @@ -150,6 +149,8 @@ class Client : public QObject { BoBManager *bobManager() const; JidLinkManager *jidLinkManager() const; CapsManager *capsManager() const; + CarbonsManager *carbonsManager() const; + JT_PushMessage *pushMessage() const; ServerInfoManager *serverInfoManager() const; ExternalServiceDiscovery *externalServiceDiscovery() const; StunDiscoManager *stunDiscoManager() const; diff --git a/iris/src/xmpp/xmpp-im/xmpp_discoinfotask.h b/iris/src/xmpp/xmpp-im/xmpp_discoinfotask.h index 810232237..c651fe8c7 100644 --- a/iris/src/xmpp/xmpp-im/xmpp_discoinfotask.h +++ b/iris/src/xmpp/xmpp-im/xmpp_discoinfotask.h @@ -23,7 +23,6 @@ #include "xmpp_task.h" class QDomElement; -class QString; namespace XMPP { class Jid; diff --git a/iris/src/xmpp/xmpp-im/xmpp_forwarding.cpp b/iris/src/xmpp/xmpp-im/xmpp_forwarding.cpp new file mode 100644 index 000000000..09a935049 --- /dev/null +++ b/iris/src/xmpp/xmpp-im/xmpp_forwarding.cpp @@ -0,0 +1,204 @@ +/* + * xmpp_forwarding.cpp - Stanza Forwarding (XEP-0297) + * Copyright (C) 2019 Aleksey Andreev + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "xmpp_forwarding.h" +#include "xmpp_client.h" +#include "xmpp_message.h" +#include "xmpp_stream.h" +#include "xmpp_tasks.h" +#include "xmpp_xmlcommon.h" + +namespace XMPP { + +static const QString xmlns_forward(QStringLiteral("urn:xmpp:forward:0")); +static const QString xmlns_delay(QStringLiteral("urn:xmpp:delay")); + +//-------------------------------------------------- +// class Forwarding +//-------------------------------------------------- + +Forwarding::Forwarding() : type_(ForwardedNone) { } + +Forwarding::Forwarding(const Forwarding &other) : type_(other.type_), ts_(other.ts_), msg_(other.msg_) { } + +Forwarding::~Forwarding() { } + +Forwarding &Forwarding::operator=(const Forwarding &from) +{ + if (this != &from) { + type_ = from.type_; + ts_ = from.ts_; + msg_ = from.msg_; + } + return *this; +} + +Forwarding::Type Forwarding::type() const { return type_; } + +void Forwarding::setType(Type type) +{ + if (type_ != type) { + type_ = type; + if (type == ForwardedNone) { + ts_ = QDateTime(); + msg_ = Message(); + } + } +} + +bool Forwarding::isCarbons() const { return (type_ == ForwardedCarbonsSent || type_ == ForwardedCarbonsReceived); } + +QDateTime Forwarding::timeStamp() const +{ + if (!ts_.isNull()) + return ts_; + return msg_.timeStamp(); +} + +void Forwarding::setTimeStamp(const QDateTime &ts) { ts_ = ts; } + +Message Forwarding::message() const { return msg_; } + +void Forwarding::setMessage(const Message &msg) { msg_ = msg; } + +bool Forwarding::fromXml(const QDomElement &e, Client *client) +{ + if (e.tagName() != QString::fromLatin1("forwarded") || e.attribute(QString::fromLatin1("xmlns")) != xmlns_forward) + return false; + + bool correct = false; + type_ = Forwarding::ForwardedNone; + QDomElement child = e.firstChildElement(); + while (!child.isNull()) { + if (child.tagName() == QString::fromLatin1("message")) { + if (client->pushMessage()->processXmlSubscribers(child, client, true)) + break; + Stanza s = client->stream().createStanza(addCorrectNS(child)); + Message msg; + if (msg.fromStanza(s, client->manualTimeZoneOffset(), client->timeZoneOffset())) { + if (client->pushMessage()->processMessageSubscribers(msg, true)) + break; + msg_ = msg; + type_ = ForwardedMessage; + correct = true; + } + } else if (child.tagName() == QString::fromLatin1("delay") + && child.attribute(QString::fromLatin1("xmlns")) == xmlns_delay) { + ts_ = QDateTime::fromString(child.attribute(QString::fromLatin1("stamp")).left(19), Qt::ISODate); + } + child = child.nextSiblingElement(); + } + return correct; +} + +QDomElement Forwarding::toXml(Stream *stream) const +{ + if (type_ == ForwardedNone || msg_.isNull()) + return QDomElement(); + + QDomElement e = stream->doc().createElement(QString::fromLatin1("forwarded")); + e.setAttribute(QString::fromLatin1("xmlns"), xmlns_forward); + if (ts_.isValid()) { + QDomElement delay = stream->doc().createElement(QString::fromLatin1("delay")); + delay.setAttribute(QString::fromLatin1("xmlns"), xmlns_delay); + delay.setAttribute(QString::fromLatin1("stamp"), ts_.toUTC().toString(Qt::ISODate) + "Z"); + e.appendChild(delay); + } + e.appendChild(msg_.toStanza(stream).element()); + return e; +} + +//-------------------------------------------------- +// class ForwardingManager +//-------------------------------------------------- + +class ForwardingSubscriber : public JT_PushMessage::Subscriber { +public: + bool xmlEvent(const QDomElement &root, QDomElement &e, Client *c, int userData, bool nested) override + { + Q_UNUSED(root) + Q_UNUSED(userData) + frw.setType(Forwarding::ForwardedNone); + if (!nested) { + Stanza stanza = c->stream().createStanza(e); + if (!stanza.isNull() && stanza.kind() == Stanza::Message) { + frw.fromXml(e, c); + } + } + return false; + } + + bool messageEvent(Message &msg, int userData, bool nested) override + { + Q_UNUSED(userData) + if (!nested && frw.type() != Forwarding::ForwardedNone) { + msg.setForwarded(frw); + frw.setType(Forwarding::ForwardedNone); + } + return false; + } + +private: + Forwarding frw; +}; + +//-------------------------------------------------- +// class ForwardingManager +//-------------------------------------------------- + +class ForwardingManager::Private { +public: + ~Private() + { + // if (sbs.get()) { + // push_m->unsubscribeXml(sbs.get(), QLatin1String("forwarded"), xmlns_forward); + // push_m->unsubscribeMessage(sbs.get()); + // } + } + + JT_PushMessage *push_m; + std::unique_ptr sbs; + bool enabled = false; +}; + +ForwardingManager::ForwardingManager(JT_PushMessage *push_m) : QObject(push_m), d(new Private) { d->push_m = push_m; } + +ForwardingManager::~ForwardingManager() { } + +void ForwardingManager::setEnabled(bool enabled) +{ + if (d->enabled == enabled) + return; + + if (enabled) { + d->sbs.reset(new ForwardingSubscriber()); + d->push_m->subscribeXml(d->sbs.get(), QString::fromLatin1("forwarded"), xmlns_forward, 0); + d->push_m->subscribeMessage(d->sbs.get(), 0); + } else { + d->push_m->unsubscribeXml(d->sbs.get(), QString::fromLatin1("forwarded"), xmlns_forward); + d->push_m->unsubscribeMessage(d->sbs.get()); + d->sbs.reset(); + } + d->enabled = enabled; +} + +bool ForwardingManager::isEnabled() const { return d->enabled; } + +} // namespace XMPP diff --git a/iris/src/xmpp/xmpp-im/xmpp_forwarding.h b/iris/src/xmpp/xmpp-im/xmpp_forwarding.h new file mode 100644 index 000000000..c9f37bce9 --- /dev/null +++ b/iris/src/xmpp/xmpp-im/xmpp_forwarding.h @@ -0,0 +1,92 @@ +/* + * xmpp_forwarding.h - Stanza Forwarding (XEP-0297) + * Copyright (C) 2019 Aleksey Andreev + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef XMPP_FORWARDING_H +#define XMPP_FORWARDING_H + +#include +#include +#include +#include + +#include "xmpp_message.h" + +namespace XMPP +{ + class Client; + class Stream; + class Message; + class JT_PushMessage; + + class Forwarding + { + public: + Forwarding(); + Forwarding(const Forwarding &); + ~Forwarding(); + + Forwarding & operator=(const Forwarding &); + + enum Type { + ForwardedNone, + ForwardedMessage, // XEP-0297 + ForwardedCarbonsReceived, // XEP-0280 + ForwardedCarbonsSent, // XEP-0280 + }; + Type type() const; + void setType(Type type); + bool isCarbons() const; + + QDateTime timeStamp() const; + void setTimeStamp(const QDateTime &ts); + + Message message() const; + void setMessage(const Message &msg); + + bool fromXml(const QDomElement &e, Client *client); + QDomElement toXml(Stream *stream) const; + + private: + Type type_; + QDateTime ts_; + Message msg_; + }; + + class ForwardingManager : public QObject + { + Q_OBJECT + + public: + ForwardingManager(JT_PushMessage *push_m); + ForwardingManager(const ForwardingManager &) = delete; + ForwardingManager & operator=(const ForwardingManager &) = delete; + ~ForwardingManager(); + + void setEnabled(bool enabled); + bool isEnabled() const; + + private: + class Private; + std::unique_ptr d; + }; + +} + +#endif diff --git a/iris/src/xmpp/xmpp-im/xmpp_mammanager.cpp b/iris/src/xmpp/xmpp-im/xmpp_mammanager.cpp new file mode 100644 index 000000000..a0e686ee3 --- /dev/null +++ b/iris/src/xmpp/xmpp-im/xmpp_mammanager.cpp @@ -0,0 +1,88 @@ +/* + * xmpp_mammanager.cpp - XEP-0313 Message Archive Management + * Copyright (C) 2024 mcneb10 + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + * + */ + +#include "xmpp_mammanager.h" + +using namespace XMPP; + +class MAMManager::Private { +public: + int mamPageSize; + int mamMaxMessages; + bool flipPages; + bool backwards; + Client *client; +}; + +MAMManager::MAMManager(Client *client, int mamPageSize, int mamMaxMessages, bool flipPages, bool backwards) +{ + d = new Private; + + d->client = client; + d->mamPageSize = mamPageSize; + d->mamMaxMessages = mamMaxMessages; + d->flipPages = flipPages; + d->backwards = backwards; +} + +MAMManager::~MAMManager() { delete d; } + +// TODO: review the safety of these methods/object lifetimes +MAMTask *MAMManager::getFullArchive(const Jid &j, const bool allowMUCArchives) +{ + auto task = new MAMTask(d->client->rootTask()); + + task->get(j, QString(), QString(), allowMUCArchives, d->mamPageSize, d->mamMaxMessages, d->flipPages, d->backwards); + return task; +} + +MAMTask *MAMManager::getArchiveByIDRange(const Jid &j, const QString &fromID, const QString &toID, + const bool allowMUCArchives) +{ + auto task = new MAMTask(d->client->rootTask()); + + task->get(j, fromID, toID, allowMUCArchives, d->mamPageSize, d->mamMaxMessages, d->flipPages, d->backwards); + return task; +} + +MAMTask *MAMManager::getArchiveByTimeRange(const Jid &j, const QDateTime &from, const QDateTime &to, + const bool allowMUCArchives) +{ + auto task = new MAMTask(d->client->rootTask()); + + task->get(j, from, to, allowMUCArchives, d->mamPageSize, d->mamMaxMessages, d->flipPages, d->backwards); + return task; +} + +MAMTask *MAMManager::getLatestMessagesFromArchive(const Jid &j, const QString &fromID, const bool allowMUCArchives, + int amount) +{ + auto task = new MAMTask(d->client->rootTask()); + + task->get(j, fromID, QString(), allowMUCArchives, d->mamPageSize, amount, true, true); + return task; +} + +MAMTask *MAMManager::getMessagesBeforeID(const Jid &j, const QString &toID, const bool allowMUCArchives, int amount) +{ + auto task = new MAMTask(d->client->rootTask()); + + task->get(j, QString(), toID, allowMUCArchives, d->mamPageSize, amount, true, true); + return task; +} diff --git a/iris/src/xmpp/xmpp-im/xmpp_mammanager.h b/iris/src/xmpp/xmpp-im/xmpp_mammanager.h new file mode 100644 index 000000000..9c87e660d --- /dev/null +++ b/iris/src/xmpp/xmpp-im/xmpp_mammanager.h @@ -0,0 +1,53 @@ +/* + * xmpp_mammanager.h - XEP-0313 Message Archive Management + * Copyright (C) 2024 mcneb10 + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + * + */ + +#ifndef XMPP_MAM_MANAGER_H +#define XMPP_MAM_MANAGER_H + +#include "xmpp_client.h" +#include "xmpp_mamtask.h" + +#include +#include + +namespace XMPP { +class MAMManager : public QObject { + Q_OBJECT +public: + MAMManager(Client *client, int mamPageSize = 10, int mamMaxMessages = 0, bool flipPages = true, + bool backwards = true); + ~MAMManager(); + + MAMTask *getFullArchive(const Jid &j, const bool allowMUCArchives = true); + MAMTask *getArchiveByIDRange(const Jid &j, const QString &fromID, const QString &toID, + const bool allowMUCArchives = true); + MAMTask *getArchiveByTimeRange(const Jid &j, const QDateTime &from, const QDateTime &to, + const bool allowMUCArchives = true); + MAMTask *getLatestMessagesFromArchive(const Jid &j, const QString &fromID, const bool allowMUCArchives = true, + int amount = 100); + MAMTask *getMessagesBeforeID(const Jid &j, const QString &toID, const bool allowMUCArchives = true, + int amount = 100); + +private: + class Private; + Private *d; +}; +} + +#endif diff --git a/iris/src/xmpp/xmpp-im/xmpp_mamtask.cpp b/iris/src/xmpp/xmpp-im/xmpp_mamtask.cpp new file mode 100644 index 000000000..87eee3d3f --- /dev/null +++ b/iris/src/xmpp/xmpp-im/xmpp_mamtask.cpp @@ -0,0 +1,278 @@ +/* + * xmpp_mamtask.cpp - XEP-0313 Message Archive Management + * Copyright (C) 2024 mcneb10 + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + * + */ + +#include "xmpp_mamtask.h" + +using namespace XMLHelper; +using namespace XMPP; + +class MAMTask::Private { +public: + int mamPageSize; // TODO: this is the max page size for MAM request. Should be made into a config option in Psi+ + int mamMaxMessages; // maximum mam pages total, also should be config. zero means unlimited + int messagesFetched; + bool flipPages; + bool backwards; + bool allowMUCArchives; + bool metadataFetched; + Jid j; + MAMTask *q; + QString firstID; + QString lastID; + QString lastArchiveID; + QString fromID; + QString toID; + QString mainQueryID; + QString currentPageQueryID; + QString currentPageQueryIQID; + QDateTime from; + QDateTime to; + QList archive; + + void getPage(); + void getArchiveMetadata(); + XData makeMAMFilter(); +}; + +MAMTask::MAMTask(Task *parent) : Task(parent) { d = new Private; } +MAMTask::MAMTask(const MAMTask &x) : Task(x.parent()) { d = x.d; } +MAMTask::~MAMTask() { delete d; } + +const QList &MAMTask::archive() const { return d->archive; } + +XData MAMTask::Private::makeMAMFilter() +{ + XData::FieldList fl; + + XData::Field with; + with.setType(XData::Field::Field_JidSingle); + with.setVar(QLatin1String("with")); + with.setValue(QStringList(j.full())); + fl.append(with); + + XData::Field includeGroupchat; + includeGroupchat.setType(XData::Field::Field_Boolean); + includeGroupchat.setVar(QLatin1String("include-groupchat")); + includeGroupchat.setValue(QStringList(QLatin1String(allowMUCArchives ? "true" : "false"))); + fl.append(includeGroupchat); + + if (from.isValid()) { + XData::Field start; + start.setType(XData::Field::Field_TextSingle); + start.setVar(QLatin1String("start")); + from.setTimeSpec(Qt::UTC); + start.setValue(QStringList(from.toString())); + fl.append(start); + } + + if (to.isValid()) { + XData::Field end; + end.setType(XData::Field::Field_TextSingle); + end.setVar(QLatin1String("end")); + to.setTimeSpec(Qt::UTC); + end.setValue(QStringList(to.toString())); + fl.append(end); + } + + if (!fromID.isNull()) { + XData::Field start_id; + start_id.setType(XData::Field::Field_TextSingle); + start_id.setVar(QLatin1String("after-id")); + start_id.setValue(QStringList(fromID)); + fl.append(start_id); + } + + if (!toID.isNull()) { + XData::Field end_id; + end_id.setType(XData::Field::Field_TextSingle); + end_id.setVar(QLatin1String("before-id")); + end_id.setValue(QStringList(toID)); + fl.append(end_id); + } + + XData x; + x.setType(XData::Data_Submit); + x.setFields(fl); + x.setRegistrarType(XMPP_MAM_NAMESPACE); + + return x; +} + +void MAMTask::Private::getPage() +{ + currentPageQueryIQID = q->genUniqueID(); + QDomElement iq = createIQ(q->doc(), QLatin1String("set"), QLatin1String(), currentPageQueryIQID); + QDomElement query = q->doc()->createElementNS(XMPP_MAM_NAMESPACE, QLatin1String("query")); + currentPageQueryID = q->genUniqueID(); + query.setAttribute(QLatin1String("queryid"), currentPageQueryID); + XData x = makeMAMFilter(); + + SubsetsClientManager rsm; + rsm.setMax(mamMaxMessages); + + if (flipPages) + query.appendChild(emptyTag(q->doc(), QLatin1String("flip-page"))); + + if (lastArchiveID.isNull()) { + if (backwards) { + rsm.getLast(); + } else { + rsm.getFirst(); + } + } else { + if (backwards) { + rsm.setFirstID(lastArchiveID); + rsm.getPrevious(); + } else { + rsm.setLastID(lastArchiveID); + rsm.getNext(); + } + } + + query.appendChild(x.toXml(q->doc())); + query.appendChild(rsm.makeQueryElement(q->doc())); + iq.appendChild(query); + q->send(iq); +} + +void MAMTask::Private::getArchiveMetadata() +{ + // Craft a query to get the first and last messages in an archive + mainQueryID = q->genUniqueID(); + QDomElement iq = createIQ(q->doc(), QLatin1String("get"), QLatin1String(), mainQueryID); + QDomElement metadata = emptyTag(q->doc(), QLatin1String("metadata")); + metadata.setAttribute(QLatin1String("xmlns"), XMPP_MAM_NAMESPACE); + iq.appendChild(metadata); + + q->send(iq); +} + +// Note: Set `j` to a resource if you just want to query that resource +// if you want to query all resources, set `j` to the bare JID + +// Filter by time range +void MAMTask::get(const Jid &j, const QDateTime &from, const QDateTime &to, const bool allowMUCArchives, + int mamPageSize, int mamMaxMessages, bool flipPages, bool backwards) +{ + d->archive = {}; + d->messagesFetched = 0; + d->metadataFetched = false; + + d->j = j; + d->from = from; + d->to = to; + d->allowMUCArchives = allowMUCArchives; + d->mamPageSize = mamPageSize; + d->mamMaxMessages = mamMaxMessages; + d->flipPages = flipPages; + d->backwards = backwards; + d->q = this; +} + +// Filter by id range +void MAMTask::get(const Jid &j, const QString &fromID, const QString &toID, const bool allowMUCArchives, + int mamPageSize, int mamMaxMessages, bool flipPages, bool backwards) +{ + d->archive = {}; + d->messagesFetched = 0; + d->metadataFetched = false; + + d->j = j; + d->fromID = fromID; + d->toID = toID; + d->allowMUCArchives = allowMUCArchives; + d->mamPageSize = mamPageSize; + d->mamMaxMessages = mamMaxMessages; + d->flipPages = flipPages; + d->backwards = backwards; +} + +void MAMTask::onGo() { d->getArchiveMetadata(); } + +bool MAMTask::take(const QDomElement &x) +{ + if (d->metadataFetched) { + if (iqVerify(x, QString(), d->currentPageQueryIQID)) { + if (!x.elementsByTagNameNS(QLatin1String("urn:ietf:params:xml:ns:xmpp-stanzas"), + QLatin1String("item-not-found")) + .isEmpty()) { + setError(2, "First or last stanza UID of filter was not found in the archive"); + return true; + } else if (!x.elementsByTagNameNS(XMPP_MAM_NAMESPACE, QLatin1String("fin")).isEmpty()) { + // We are done? + //setSuccess(); + //return true; + return false; // TODO: testing + } + // Probably ignore it + return false; + } + + QDomElement result = x.firstChildElement("result"); + if (result != QDomElement() && result.namespaceURI() == XMPP_MAM_NAMESPACE + && result.attribute(QLatin1String("queryid")) == d->currentPageQueryID) { + + d->archive.append(result); + d->lastArchiveID = result.attribute(QLatin1String("id")); + d->messagesFetched = d->messagesFetched + 1; + + // Check if we are done + if (result.attribute(QLatin1String("id")) == d->lastID || d->messagesFetched >= d->mamMaxMessages) { + setSuccess(); + } else if (d->messagesFetched % d->mamPageSize == 0) { + d->getPage(); + } + } + } else { + if (!iqVerify(x, QString(), d->mainQueryID)) + return false; + + // Return if the archive is empty + QDomElement queryMetadata = x.firstChildElement(QLatin1String("metadata")); + if(queryMetadata == QDomElement()) { + setError(1, "Malformed server metadata response"); + return true; + } + if (!queryMetadata.hasChildNodes()) { + // No data in archive + setSuccess(); + return true; + } + + QDomElement start_id = queryMetadata.firstChildElement(QLatin1String("start")); + QDomElement end_id = queryMetadata.firstChildElement(QLatin1String("end")); + + if (start_id.isNull() || end_id.isNull()) { + setError(1, "Malformed server metadata response"); + return true; + } + + if (d->backwards) { + d->lastID = start_id.attribute(QLatin1String("id")); + d->firstID = end_id.attribute(QLatin1String("id")); + } else { + d->firstID = start_id.attribute(QLatin1String("id")); + d->lastID = end_id.attribute(QLatin1String("id")); + } + d->metadataFetched = true; + d->getPage(); + } + + return true; +} diff --git a/iris/src/xmpp/xmpp-im/xmpp_mamtask.h b/iris/src/xmpp/xmpp-im/xmpp_mamtask.h new file mode 100644 index 000000000..63e8535a1 --- /dev/null +++ b/iris/src/xmpp/xmpp-im/xmpp_mamtask.h @@ -0,0 +1,67 @@ +/* + * xmpp_mamtask.h - XEP-0313 Message Archive Management + * Copyright (C) 2024 mcneb10 + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + * + */ + +#ifndef XMPP_MAM_TASK_H +#define XMPP_MAM_TASK_H + +#include "xmpp/jid/jid.h" +#include "xmpp_subsets.h" +#include "xmpp_task.h" +#include "xmpp_xdata.h" +#include "xmpp_xmlcommon.h" + +#include + +#define XMPP_MAM_NAMESPACE QLatin1String("urn:xmpp:mam:2") + +class QDomElement; +class QString; + +namespace XMPP { +class Jid; + +class MAMTask : public Task { + Q_OBJECT +public: + explicit MAMTask(Task *); + MAMTask(const MAMTask &x); + ~MAMTask(); + + const QList &archive() const; + + // Time filter + void get(const Jid &j, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime(), + const bool allowMUCArchives = true, int mamPageSize = 10, int mamMaxMessages = 0, bool flipPages = true, + bool backwards = true); + + // ID Filter + void get(const Jid &j, const QString &fromID = QString(), const QString &toID = QString(), + const bool allowMUCArchives = true, int mamPageSize = 10, int mamMaxMessages = 0, bool flipPages = true, + bool backwards = true); + + void onGo(); + bool take(const QDomElement &); + +private: + class Private; + Private *d; +}; +} // namespace XMPP + +#endif diff --git a/iris/src/xmpp/xmpp-im/xmpp_message.h b/iris/src/xmpp/xmpp-im/xmpp_message.h index 45b5965db..2afef8a49 100644 --- a/iris/src/xmpp/xmpp-im/xmpp_message.h +++ b/iris/src/xmpp/xmpp-im/xmpp_message.h @@ -19,7 +19,6 @@ #ifndef XMPP_MESSAGE_H #define XMPP_MESSAGE_H -#include "iris/xmpp_stanza.h" #include "xmpp_address.h" #include "xmpp_chatstate.h" #include "xmpp_muc.h" @@ -36,6 +35,7 @@ class QString; namespace XMPP { class BoBData; +class Forwarding; class HTMLElement; class HttpAuthRequest; class IBBData; @@ -50,12 +50,6 @@ typedef enum { OfflineEvent, DeliveredEvent, DisplayedEvent, ComposingEvent, Can class Message { public: - enum CarbonDir : quint8 { - NoCarbon, - Received, // other party messages are sent to another own client - Sent // own messages are sent from other clients - }; - enum class Type { Chat, Error, Groupchat, Headline, Normal }; // XEP-0334 @@ -87,7 +81,7 @@ class Message { Type type() const; QString typeStr() const; QString lang() const; - QString subject(const QString &lang = QString()) const; + QString subject(const QString &lang = {}) const; QString subject(const QLocale &lang) const; StringMap subjectMap() const; QString body(const QString &lang = "") const; @@ -100,8 +94,8 @@ class Message { void setId(const QString &s); void setType(Type type); void setLang(const QString &s); - void setSubject(const QString &s, const QString &lang = ""); - void setBody(const QString &s, const QString &lang = ""); + void setSubject(const QString &s, const QString &lang = {}); + void setBody(const QString &s, const QString &lang = {}); void setThread(const QString &s, bool send = false); void setError(const Stanza::Error &err); @@ -115,8 +109,8 @@ class Message { void setTimeStamp(const QDateTime &ts, bool send = false); // XEP-0071 - HTMLElement html(const QString &lang = "") const; - void setHTML(const HTMLElement &s, const QString &lang = ""); + HTMLElement html(const QString &lang = {}) const; + void setHTML(const HTMLElement &s, const QString &lang = {}); bool containsHTML() const; // XEP-0066 @@ -183,14 +177,15 @@ class Message { IBBData ibbData() const; // XEP-0280 Message Carbons - void setDisabledCarbons(bool disabled); - bool isDisabledCarbons() const; - void setCarbonDirection(CarbonDir); - CarbonDir carbonDirection() const; + Jid displayJid() const; + Message displayMessage() const; + void setCarbonsPrivate(bool enable); + bool carbonsPrivate() const; // XEP-0297 - void setForwardedFrom(const Jid &jid); - Jid forwardedFrom() const; + void setForwarded(const Forwarding &frw); + // note, the next method has to be called only on not-null message + const Forwarding &forwarded() const; // XEP-0308 QString replaceId() const; diff --git a/iris/src/xmpp/xmpp-im/xmpp_subsets.cpp b/iris/src/xmpp/xmpp-im/xmpp_subsets.cpp index 5925bbabb..f1f75fdc5 100644 --- a/iris/src/xmpp/xmpp-im/xmpp_subsets.cpp +++ b/iris/src/xmpp/xmpp-im/xmpp_subsets.cpp @@ -165,6 +165,8 @@ bool SubsetsClientManager::isLast() const { return d->result.last; } int SubsetsClientManager::count() const { return d->result.count; } void SubsetsClientManager::setMax(int max) { d->query.max = max; } +void SubsetsClientManager::setFirstID(const QString& a) { d->result.firstId = a; } +void SubsetsClientManager::setLastID(const QString& a) { d->result.lastId = a; } QDomElement SubsetsClientManager::findElement(const QDomElement &el, bool child) { diff --git a/iris/src/xmpp/xmpp-im/xmpp_subsets.h b/iris/src/xmpp/xmpp-im/xmpp_subsets.h index 4e6bbd245..a940b0354 100644 --- a/iris/src/xmpp/xmpp-im/xmpp_subsets.h +++ b/iris/src/xmpp/xmpp-im/xmpp_subsets.h @@ -34,6 +34,8 @@ class SubsetsClientManager { bool isLast() const; int count() const; void setMax(int max); + void setFirstID(const QString&); + void setLastID(const QString&); void getCount(); void getFirst(); diff --git a/iris/src/xmpp/xmpp-im/xmpp_task.cpp b/iris/src/xmpp/xmpp-im/xmpp_task.cpp index 7c4c2a02b..f50842a6c 100644 --- a/iris/src/xmpp/xmpp-im/xmpp_task.cpp +++ b/iris/src/xmpp/xmpp-im/xmpp_task.cpp @@ -83,6 +83,8 @@ QDomDocument *Task::doc() const { return client()->doc(); } QString Task::id() const { return d->id; } +QString Task::genUniqueID() { return client()->genUniqueId(); } + bool Task::success() const { return d->success; } int Task::statusCode() const { return d->statusCode; } diff --git a/iris/src/xmpp/xmpp-im/xmpp_task.h b/iris/src/xmpp/xmpp-im/xmpp_task.h index 15e2d2607..a0b031de0 100644 --- a/iris/src/xmpp/xmpp-im/xmpp_task.h +++ b/iris/src/xmpp/xmpp-im/xmpp_task.h @@ -44,6 +44,7 @@ class Task : public QObject { Client *client() const; QDomDocument *doc() const; QString id() const; + QString genUniqueID(); bool success() const; int statusCode() const; diff --git a/iris/src/xmpp/xmpp-im/xmpp_tasks.cpp b/iris/src/xmpp/xmpp-im/xmpp_tasks.cpp index 2ee5f3a5e..90d8232c9 100644 --- a/iris/src/xmpp/xmpp-im/xmpp_tasks.cpp +++ b/iris/src/xmpp/xmpp-im/xmpp_tasks.cpp @@ -28,12 +28,16 @@ #include "xmpp_vcard.h" #include "xmpp_xmlcommon.h" +#include #include #include #include using namespace XMPP; +#define GET_SUBSCRIBER_ITERATOR(list, sbs) \ + std::find_if(list.begin(), list.end(), [sbs](const Private::SubsData &value) { return value.sbs == sbs; }) + static QString lineEncode(QString str) { static QRegularExpression backslash("\\\\"); @@ -821,16 +825,133 @@ void JT_Message::onGo() //---------------------------------------------------------------------------- class JT_PushMessage::Private { public: - EncryptionHandler *m_encryptionHandler; + struct SubsData { + Subscriber *sbs = nullptr; + int userData = -1; + }; + using SubsDataList = QVector; + EncryptionHandler *m_encryptionHandler; + QHash subsData; + SubsDataList subsMData; + + QString genKey(const QString &s1, const QString &s2) { return QString::fromLatin1("%1&%2").arg(s1, s2); } + + bool processChildStanzaNode(const QDomElement &root, QDomElement &e, Client *c, bool nested) + { + QString tagName = e.tagName(); + QString xmlnsStr = e.attribute(QString::fromLatin1("xmlns")); + QString key = genKey(tagName, xmlnsStr); + auto it = subsData.constFind(key); + if (it != subsData.constEnd()) { + foreach (const SubsData &sd, it.value()) { + if (sd.sbs->xmlEvent(root, e, c, sd.userData, nested)) + return true; + if (e.tagName() != tagName || e.attribute(QString::fromLatin1("xmlns")) != tagName) + return false; + } + } + return false; + } + + bool processMessage(Message &msg, bool nested) + { + foreach (const SubsData &sd, subsMData) { + if (sd.sbs->messageEvent(msg, sd.userData, nested)) + return true; + } + return false; + } }; -JT_PushMessage::JT_PushMessage(Task *parent, EncryptionHandler *encryptionHandler) : Task(parent) +JT_PushMessage::Subscriber::~Subscriber() { } + +bool JT_PushMessage::Subscriber::xmlEvent(const QDomElement &root, QDomElement &e, Client *c, int userData, bool nested) +{ + Q_UNUSED(root) + Q_UNUSED(e) + Q_UNUSED(c) + Q_UNUSED(userData) + Q_UNUSED(nested) + return false; +} + +bool JT_PushMessage::Subscriber::messageEvent(Message &msg, int userData, bool nested) +{ + Q_UNUSED(msg); + Q_UNUSED(userData); + Q_UNUSED(nested) + return false; +} + +JT_PushMessage::JT_PushMessage(Task *parent, EncryptionHandler *encryptionHandler) : Task(parent), d(new Private) { - d = new Private; d->m_encryptionHandler = encryptionHandler; } -JT_PushMessage::~JT_PushMessage() { delete d; } +JT_PushMessage::~JT_PushMessage() { } + +void JT_PushMessage::subscribeXml(Subscriber *sbs, const QString &tagName, const QString &xmlnsStr, int userData) +{ + QString key = d->genKey(tagName, xmlnsStr); + auto it = d->subsData.find(key); + if (it != d->subsData.end()) { + Private::SubsDataList &list = it.value(); + auto lit = GET_SUBSCRIBER_ITERATOR(list, sbs); + if (lit == list.end()) + list.append({ sbs, userData }); + } else { + d->subsData.insert(key, { { sbs, userData } }); + } +} + +void JT_PushMessage::unsubscribeXml(Subscriber *sbs, const QString &tagName, const QString &xmlnsStr) +{ + QString key = d->genKey(tagName, xmlnsStr); + auto it = d->subsData.find(key); + if (it != d->subsData.end()) { + Private::SubsDataList &list = it.value(); + auto lit = GET_SUBSCRIBER_ITERATOR(list, sbs); + if (lit != list.end()) { + list.erase(lit); + if (list.isEmpty()) + d->subsData.erase(it); + } + } +} + +void JT_PushMessage::subscribeMessage(Subscriber *sbs, int userData) +{ + auto &list = d->subsMData; + auto lit = GET_SUBSCRIBER_ITERATOR(list, sbs); + if (lit == list.end()) + list.append({ sbs, userData }); +} + +void JT_PushMessage::unsubscribeMessage(Subscriber *sbs) +{ + auto &list = d->subsMData; + auto lit = GET_SUBSCRIBER_ITERATOR(list, sbs); + if (lit != list.end()) + list.erase(lit); +} + +bool JT_PushMessage::processXmlSubscribers(QDomElement &el, Client *client, bool nested) +{ + bool processed = false; + QDomElement ch = el.firstChildElement(); + while (!ch.isNull()) { + QDomElement next = ch.nextSiblingElement(); + bool res = d->processChildStanzaNode(el, ch, client, nested); + if (res) + processed = true; + if (res || ch.isNull()) + el.removeChild(ch); + ch = next; + } + return (processed && el.childNodes().length() == 0); +} + +bool JT_PushMessage::processMessageSubscribers(Message &msg, bool nested) { return d->processMessage(msg, nested); } bool JT_PushMessage::take(const QDomElement &e) { @@ -848,39 +969,10 @@ bool JT_PushMessage::take(const QDomElement &e) } } - QDomElement forward; - Message::CarbonDir cd = Message::NoCarbon; - - Jid fromJid = Jid(e1.attribute(QLatin1String("from"))); - // Check for Carbon - QDomNodeList list = e1.childNodes(); - for (int i = 0; i < list.size(); ++i) { - QDomElement el = list.at(i).toElement(); - - if (el.namespaceURI() == QLatin1String("urn:xmpp:carbons:2") - && (el.tagName() == QLatin1String("received") || el.tagName() == QLatin1String("sent")) - && fromJid.compare(Jid(e1.attribute(QLatin1String("to"))), false)) { - QDomElement el1 = el.firstChildElement(); - if (el1.tagName() == QLatin1String("forwarded") - && el1.namespaceURI() == QLatin1String("urn:xmpp:forward:0")) { - QDomElement el2 = el1.firstChildElement(QLatin1String("message")); - if (!el2.isNull()) { - forward = el2; - cd = el.tagName() == QLatin1String("received") ? Message::Received : Message::Sent; - break; - } - } - } else if (el.tagName() == QLatin1String("forwarded") - && el.namespaceURI() == QLatin1String("urn:xmpp:forward:0")) { - forward = el.firstChildElement(QLatin1String("message")); // currently only messages are supportted - // TODO element support - if (!forward.isNull()) { - break; - } - } - } + if (processXmlSubscribers(e1, client(), false)) + return true; - Stanza s = client()->stream().createStanza(addCorrectNS(forward.isNull() ? e1 : forward)); + Stanza s = client()->stream().createStanza(addCorrectNS(e1)); if (s.isNull()) { // printf("take: bad stanza??\n"); return false; @@ -891,10 +983,9 @@ bool JT_PushMessage::take(const QDomElement &e) // printf("bad message\n"); return false; } - if (!forward.isNull()) { - m.setForwardedFrom(fromJid); - m.setCarbonDirection(cd); - } + + if (processMessageSubscribers(m, false)) + return true; // See: XEP-0380: Explicit Message Encryption const bool wasEncrypted = !e1.firstChildElement("encryption").isNull(); @@ -1793,41 +1884,3 @@ bool JT_CaptchaSender::take(const QDomElement &x) return true; } - -//---------------------------------------------------------------------------- -// JT_MessageCarbons -//---------------------------------------------------------------------------- -JT_MessageCarbons::JT_MessageCarbons(Task *parent) : Task(parent) { } - -void JT_MessageCarbons::enable() -{ - _iq = createIQ(doc(), "set", "", id()); - - QDomElement enable = doc()->createElementNS("urn:xmpp:carbons:2", "enable"); - - _iq.appendChild(enable); -} - -void JT_MessageCarbons::disable() -{ - _iq = createIQ(doc(), "set", "", id()); - - QDomElement disable = doc()->createElementNS("urn:xmpp:carbons:2", "disable"); - - _iq.appendChild(disable); -} - -void JT_MessageCarbons::onGo() -{ - send(_iq); - setSuccess(); -} - -bool JT_MessageCarbons::take(const QDomElement &e) -{ - if (e.tagName() != "iq" || e.attribute("type") != "result") - return false; - - bool res = iqVerify(e, Jid(), id()); - return res; -} diff --git a/iris/src/xmpp/xmpp-im/xmpp_tasks.h b/iris/src/xmpp/xmpp-im/xmpp_tasks.h index d500d3a12..da3ecdec4 100644 --- a/iris/src/xmpp/xmpp-im/xmpp_tasks.h +++ b/iris/src/xmpp/xmpp-im/xmpp_tasks.h @@ -200,9 +200,22 @@ class JT_Message : public Task { class JT_PushMessage : public Task { Q_OBJECT public: + class Subscriber { + public: + virtual ~Subscriber(); + virtual bool xmlEvent(const QDomElement &root, QDomElement &e, Client *c, int userData, bool nested); + virtual bool messageEvent(Message &msg, int userData, bool nested); + }; JT_PushMessage(Task *parent, EncryptionHandler *encryptionHandler = nullptr); ~JT_PushMessage(); + void subscribeXml(Subscriber *sbs, const QString &tagName, const QString &xmlnsStr, int userData); + void unsubscribeXml(Subscriber *sbs, const QString &tagName, const QString &xmlnsStr); + void subscribeMessage(Subscriber *sbs, int userData); + void unsubscribeMessage(Subscriber *sbs); + bool processXmlSubscribers(QDomElement &el, Client *client, bool nested); + bool processMessageSubscribers(Message &msg, bool nested); + bool take(const QDomElement &); signals: @@ -210,7 +223,7 @@ class JT_PushMessage : public Task { private: class Private; - Private *d = nullptr; + std::unique_ptr d; }; class JT_VCard : public Task { @@ -407,21 +420,6 @@ class JT_PongServer : public Task { bool take(const QDomElement &); }; -class JT_MessageCarbons : public Task { - Q_OBJECT - -public: - JT_MessageCarbons(Task *parent); - void enable(); - void disable(); - - void onGo(); - bool take(const QDomElement &e); - -private: - QDomElement _iq; -}; - class JT_CaptchaChallenger : public Task { Q_OBJECT public: diff --git a/src/chatdlg.cpp b/src/chatdlg.cpp index b1a0d1886..7420e001d 100644 --- a/src/chatdlg.cpp +++ b/src/chatdlg.cpp @@ -36,6 +36,7 @@ #include "pluginmanager.h" #endif #include "iris/xmpp_caps.h" +#include "iris/xmpp_forwarding.h" #include "iris/xmpp_htmlelement.h" #include "iris/xmpp_message.h" #include "psiaccount.h" @@ -795,20 +796,21 @@ void ChatDlg::encryptedMessageSent(int x, bool b, int e, const QString &dtext) void ChatDlg::incomingMessage(const Message &m) { + Message dm = m.displayMessage(); historyState = false; - if (m.body().isEmpty() && m.subject().isEmpty() && m.urlList().isEmpty()) { + if (dm.body().isEmpty() && dm.subject().isEmpty() && dm.urlList().isEmpty()) { // Event message - if (m.containsEvent(CancelEvent)) { + if (dm.containsEvent(CancelEvent)) { setContactChatState(XMPP::StatePaused); - } else if (m.containsEvent(ComposingEvent)) { + } else if (dm.containsEvent(ComposingEvent)) { setContactChatState(XMPP::StateComposing); } - if (m.chatState() != XMPP::StateNone) { - setContactChatState(m.chatState()); + if (dm.chatState() != XMPP::StateNone) { + setContactChatState(dm.chatState()); } - if (m.messageReceipt() == ReceiptReceived) { - chatView()->markReceived(m.messageReceiptId()); + if (dm.messageReceipt() == ReceiptReceived) { + chatView()->markReceived(dm.messageReceiptId()); } if (!m.reactions().targetId.isEmpty()) { auto mv = MessageView::reactionsMessage({}, m.reactions().targetId, m.reactions().reactions); @@ -821,16 +823,16 @@ void ChatDlg::incomingMessage(const Message &m) } else { // Normal message // Check if user requests event messages - sendComposingEvents_ = m.containsEvent(ComposingEvent); - if (!m.eventId().isEmpty()) { - eventId_ = m.eventId(); + sendComposingEvents_ = dm.containsEvent(ComposingEvent); + if (!dm.eventId().isEmpty()) { + eventId_ = dm.eventId(); } - if (m.containsEvents() || m.chatState() != XMPP::StateNone) { + if (dm.containsEvents() || dm.chatState() != XMPP::StateNone) { setContactChatState(XMPP::StateActive); } else { setContactChatState(XMPP::StateNone); } - appendMessage(m, m.carbonDirection() == Message::Sent); + appendMessage(m, m.forwarded().type() == Forwarding::ForwardedCarbonsSent); } } @@ -851,6 +853,7 @@ QString ChatDlg::whoNick(bool local) const void ChatDlg::appendMessage(const Message &m, bool local) { + Message dm = m.displayMessage(); if (trackBar_) doTrackBar(); @@ -858,10 +861,10 @@ void ChatDlg::appendMessage(const Message &m, bool local) bool encChanged = false; bool encEnabled = false; if (!historyState) { - if (lastWasEncrypted_ != m.wasEncrypted()) { + if (lastWasEncrypted_ != dm.wasEncrypted()) { encChanged = true; } - lastWasEncrypted_ = m.wasEncrypted(); + lastWasEncrypted_ = dm.wasEncrypted(); encEnabled = lastWasEncrypted_; } @@ -886,18 +889,19 @@ void ChatDlg::appendMessage(const Message &m, bool local) } } - if (!m.subject().isEmpty()) { - MessageView smv = MessageView::subjectMessage(m.subject()); + if (!dm.subject().isEmpty()) { + MessageView smv = MessageView::subjectMessage(dm.subject()); smv.setSpooled(historyState); dispatchMessage(smv); } MessageView mv(MessageView::Message); - QString body = m.body(); - HTMLElement htmlElem; - if (m.containsHTML()) - htmlElem = m.html(); + QString body = dm.body(); + HTMLElement htmlElem = dm.html(); + // if (m.forwarded().type() == Forwarding::ForwardedMessage) { + // TODO + // } #ifdef PSI_PLUGINS QDomElement html = htmlElem.body(); @@ -914,22 +918,21 @@ void ChatDlg::appendMessage(const Message &m, bool local) } else { mv.setPlainText(body); } - mv.setMessageId(m.id()); + mv.setMessageId(dm.id()); mv.setLocal(local); mv.setNick(whoNick(local)); mv.setUserId(local ? account()->jid().full() : jid().full()); // theoretically, this can be inferred from the chat dialog properties - mv.setDateTime(m.timeStamp()); + mv.setDateTime(dm.timeStamp()); mv.setSpooled(historyState); - mv.setAwaitingReceipt(local && m.messageReceipt() == ReceiptRequest); - mv.setReplaceId(m.replaceId()); - mv.setCarbonDirection(m.carbonDirection()); - account()->psi()->fileSharingManager()->fillMessageView(mv, m, account()); + mv.setAwaitingReceipt(local && dm.messageReceipt() == ReceiptRequest); + mv.setReplaceId(dm.replaceId()); + account()->psi()->fileSharingManager()->fillMessageView(mv, dm, account()); dispatchMessage(mv); - if (!m.urlList().isEmpty()) { - UrlList urls = m.urlList(); + if (!dm.urlList().isEmpty()) { + UrlList urls = dm.urlList(); QMap urlsMap; for (const Url &u : urls) { urlsMap.insert(u.url(), u.desc()); @@ -979,7 +982,7 @@ void ChatDlg::displayMessage(const MessageView &mv) // if we're not active, notify the user by changing the title MessageView::Type type = mv.type(); if (type != MessageView::System && type != MessageView::Status && !mv.isSpooled() && !isActiveTab() - && mv.carbonDirection() != Message::Sent) { + && !mv.isLocal()) { ++pending_; invalidateTab(); if (PsiOptions::instance()->getOption("options.ui.flash-windows").toBool()) { diff --git a/src/edbflatfile.cpp b/src/edbflatfile.cpp index 2e3bcb6a0..8902c2dc5 100644 --- a/src/edbflatfile.cpp +++ b/src/edbflatfile.cpp @@ -655,7 +655,7 @@ QString EDBFlatFile::File::eventToLine(const PsiEvent::Ptr &e) if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); + const Message &m = me->message().displayMessage(); const UrlList urls = m.urlList(); if (!m.subject().isEmpty()) diff --git a/src/edbsqlite.cpp b/src/edbsqlite.cpp index b34173ffa..093119eb3 100644 --- a/src/edbsqlite.cpp +++ b/src/edbsqlite.cpp @@ -412,7 +412,7 @@ bool EDBSqLite::appendEvent(const QString &accId, const XMPP::Jid &jid, const Ps if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); + const Message &m = me->message().displayMessage(); dTime = m.timeStamp(); if (m.type() == Message::Type::Chat) nType = 1; @@ -448,7 +448,7 @@ bool EDBSqLite::appendEvent(const QString &accId, const XMPP::Jid &jid, const Ps query->bindValue(":direction", nDirection); if (nType == 0 || nType == 1 || nType == 4 || nType == 5) { MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); + const Message &m = me->message().displayMessage(); QString lang = m.lang(); query->bindValue(":subject", m.subject(lang)); query->bindValue(":m_text", m.body(lang)); diff --git a/src/eventdlg.cpp b/src/eventdlg.cpp index 49f3143ed..30b7f0ae3 100644 --- a/src/eventdlg.cpp +++ b/src/eventdlg.cpp @@ -1673,7 +1673,7 @@ void EventDlg::updateEvent(const PsiEvent::Ptr &e) if (e->type() == PsiEvent::Message || e->type() == PsiEvent::HttpAuth) { MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); + const Message &dm = me->message().displayMessage(); // HTTP auth request buttons if (e->type() == PsiEvent::HttpAuth) { @@ -1687,13 +1687,13 @@ void EventDlg::updateEvent(const PsiEvent::Ptr &e) d->pb_http_deny->show(); } - bool xhtml = m.containsHTML() && PsiOptions::instance()->getOption("options.html.chat.render").toBool() - && !m.html().text().isEmpty(); - QString txt = xhtml ? m.html().toString("div") : TextUtil::plain2rich(m.body()); + bool xhtml = dm.containsHTML() && PsiOptions::instance()->getOption("options.html.chat.render").toBool() + && !dm.html().text().isEmpty(); + QString txt = xhtml ? dm.html().toString("div") : TextUtil::plain2rich(dm.body()); // show subject line if the incoming message has one - if (!m.subject().isEmpty() && !PsiOptions::instance()->getOption("options.ui.message.show-subjects").toBool()) - txt = "

" + tr("Subject:") + " " + TextUtil::plain2rich(m.subject()) + if (!dm.subject().isEmpty() && !PsiOptions::instance()->getOption("options.ui.message.show-subjects").toBool()) + txt = "

" + tr("Subject:") + " " + TextUtil::plain2rich(dm.subject()) + "

" + (xhtml ? "" : "
") + txt; if (!xhtml) { @@ -1707,13 +1707,13 @@ void EventDlg::updateEvent(const PsiEvent::Ptr &e) setHtml("" + txt + ""); - d->le_subj->setText(m.subject()); + d->le_subj->setText(dm.subject()); d->le_subj->setCursorPosition(0); - d->thread = m.thread(); + d->thread = dm.thread(); // Form buttons - if (!m.getForm().fields().empty()) { + if (!dm.getForm().fields().empty()) { d->pb_chat->hide(); d->pb_reply->hide(); d->pb_quote->hide(); @@ -1726,7 +1726,7 @@ void EventDlg::updateEvent(const PsiEvent::Ptr &e) d->pb_form_submit->setEnabled(true); d->pb_form_cancel->setEnabled(true); // set title if specified - const XData &form = m.getForm(); + const XData &form = dm.getForm(); if (!form.title().isEmpty()) setWindowTitle(form.title()); @@ -1746,13 +1746,13 @@ void EventDlg::updateEvent(const PsiEvent::Ptr &e) } d->attachView->clear(); - d->attachView->addUrlList(m.urlList()); + d->attachView->addUrlList(dm.urlList()); - if (!m.mucInvites().isEmpty()) { - MUCInvite i = m.mucInvites().constFirst(); - d->attachView->gcAdd(m.from().full(), i.from().bare(), i.reason(), m.mucPassword()); - } else if (!m.invite().isEmpty()) - d->attachView->gcAdd(m.invite()); + if (!dm.mucInvites().isEmpty()) { + MUCInvite i = dm.mucInvites().constFirst(); + d->attachView->gcAdd(dm.from().full(), i.from().bare(), i.reason(), dm.mucPassword()); + } else if (!dm.invite().isEmpty()) + d->attachView->gcAdd(dm.invite()); showHideAttachView(); } else if (e->type() == PsiEvent::Auth) { AuthEvent::Ptr ae = e.staticCast(); diff --git a/src/groupchatdlg.cpp b/src/groupchatdlg.cpp index ffb6c99e5..06c717bc4 100644 --- a/src/groupchatdlg.cpp +++ b/src/groupchatdlg.cpp @@ -2121,32 +2121,33 @@ void GCMainDlg::avatarUpdated(const Jid &jid_) void GCMainDlg::message(const Message &_m, const PsiEvent::Ptr &e) { - Message m = _m; - QString from = m.from().resource(); - d->alert = false; + Message m = _m; + const Message dm = m.displayMessage(); + QString from = dm.from().resource(); + d->alert = false; - if (m.getMUCStatuses().contains(100)) { + if (dm.getMUCStatuses().contains(100)) { d->nonAnonymous = true; } - if (m.getMUCStatuses().contains(172)) { + if (dm.getMUCStatuses().contains(172)) { d->nonAnonymous = true; } - if (m.getMUCStatuses().contains(173)) { + if (dm.getMUCStatuses().contains(173)) { d->nonAnonymous = false; } - if (m.getMUCStatuses().contains(174)) { + if (dm.getMUCStatuses().contains(174)) { d->nonAnonymous = false; } - if (m.getMUCStatuses().contains(104)) { + if (dm.getMUCStatuses().contains(104)) { updateConfiguration(); } PsiOptions *options = PsiOptions::instance(); QString topic; - if (!m.subjectMap().isEmpty() && m.isPureSubject()) { + if (!dm.subjectMap().isEmpty() && dm.isPureSubject()) { d->subjectMap.clear(); - auto sm = m.subjectMap(); + auto sm = dm.subjectMap(); for (auto l = sm.constBegin(); l != sm.constEnd(); ++l) { d->subjectMap.insert(LanguageManager::fromString(l.key()), l.value()); } @@ -2177,13 +2178,13 @@ void GCMainDlg::message(const Message &_m, const PsiEvent::Ptr &e) if (from.isEmpty()) { // The topic was set by the server // ugly trick - int btStart = m.body().indexOf(topic); - sysMsg = btStart > 0 ? m.body().left(btStart).remove(": ") : tr("The topic has been set to"); + int btStart = dm.body().indexOf(topic); + sysMsg = btStart > 0 ? dm.body().left(btStart).remove(": ") : tr("The topic has been set to"); } else { sysMsg = QString(from) + (topic.isEmpty() ? tr(" has unset the topic") : tr(" has set the topic to")); } MessageView tv = MessageView::subjectMessage(topic, sysMsg); - tv.setDateTime(m.timeStamp()); + tv.setDateTime(dm.timeStamp()); ui_.le_topic->setText(topic.replace("\n\n", " || ") .replace("\n", " | ") @@ -2196,32 +2197,32 @@ void GCMainDlg::message(const Message &_m, const PsiEvent::Ptr &e) return; } - if (!m.reactions().targetId.isEmpty()) { - auto mv = MessageView::reactionsMessage(from, m.reactions().targetId, m.reactions().reactions); + if (!dm.reactions().targetId.isEmpty()) { + auto mv = MessageView::reactionsMessage(from, dm.reactions().targetId, dm.reactions().reactions); ui_.log->dispatchMessage(mv); return; } - if (!m.retraction().isEmpty()) { - auto mv = MessageView::retractionMessage(m.retraction()); + if (!dm.retraction().isEmpty()) { + auto mv = MessageView::retractionMessage(dm.retraction()); ui_.log->dispatchMessage(mv); return; } - if (m.body().isEmpty()) + if (dm.body().isEmpty()) return; // code to determine if the speaker was addressing this client in chat - if (m.body().contains(d->self)) + if (dm.body().contains(d->self)) d->alert = true; - if (m.body().left(d->self.length()) == d->self) - d->lastReferrer = m.from().resource(); + if (dm.body().left(d->self.length()) == d->self) + d->lastReferrer = dm.from().resource(); if (options->getOption("options.ui.muc.use-highlighting").toBool()) { QStringList highlightWords = options->getOption("options.ui.muc.highlight-words").toStringList(); for (const QString &word : highlightWords) { - if (m.body().contains((word), Qt::CaseInsensitive)) { + if (dm.body().contains((word), Qt::CaseInsensitive)) { d->alert = true; } } @@ -2229,30 +2230,30 @@ void GCMainDlg::message(const Message &_m, const PsiEvent::Ptr &e) // play sound? if (from == d->self) { - if (!m.spooled()) + if (!dm.spooled()) account()->playSound(PsiAccount::eSend); } else { if (d->alert - || (options->getOption("options.ui.notifications.sounds.notify-every-muc-message").toBool() && !m.spooled() + || (options->getOption("options.ui.notifications.sounds.notify-every-muc-message").toBool() && !dm.spooled() && !from.isEmpty())) account()->playSound(PsiAccount::eGroupChat); if (d->alert || (options->getOption("options.ui.notifications.passive-popups.notify-every-muc-message").toBool() - && !m.spooled() && !from.isEmpty())) { - if (!m.spooled() && !isActiveTab() && !m.from().resource().isEmpty()) { - XMPP::Jid jid = m.from() /*.withDomain("")*/; + && !dm.spooled() && !from.isEmpty())) { + if (!dm.spooled() && !isActiveTab() && !dm.from().resource().isEmpty()) { + XMPP::Jid jid = dm.from() /*.withDomain("")*/; UserListItem i; i.setPrivate(true); account()->psi()->popupManager()->doPopup(account(), PopupManager::AlertGcHighlight, jid, - m.from().resource(), &i, e); + dm.from().resource(), &i, e); } } } if (from.isEmpty()) { - auto mv = MessageView::systemMessage(m.body()); - mv.setDateTime(m.timeStamp()); + auto mv = MessageView::systemMessage(dm.body()); + mv.setDateTime(dm.timeStamp()); dispatchMessage(mv); } else appendMessage(m, d->alert); @@ -2299,23 +2300,24 @@ void GCMainDlg::dispatchMessage(const MessageView &mv) void GCMainDlg::appendMessage(const Message &m, bool alert) { + Message dm = m.displayMessage(); // figure out the encryption state bool encChanged = false; bool encEnabled = false; { - if (lastWasEncrypted_ != m.wasEncrypted()) { + if (lastWasEncrypted_ != dm.wasEncrypted()) { encChanged = true; } - lastWasEncrypted_ = m.wasEncrypted(); + lastWasEncrypted_ = dm.wasEncrypted(); encEnabled = lastWasEncrypted_; } if (encChanged) { ui_.log->setEncryptionEnabled(encEnabled); QString msg = QString(" ") + tr("Encryption is disabled"); if (encEnabled) { - if (!m.encryptionProtocol().isEmpty()) { + if (!dm.encryptionProtocol().isEmpty()) { msg = QString(" ") - + tr("%1 encryption is enabled").arg(m.encryptionProtocol()); + + tr("%1 encryption is enabled").arg(dm.encryptionProtocol()); } else { msg = QString(" ") + tr("Encryption is enabled"); } @@ -2324,23 +2326,23 @@ void GCMainDlg::appendMessage(const Message &m, bool alert) } MessageView mv(MessageView::Message); - if (m.containsHTML() && PsiOptions::instance()->getOption("options.html.muc.render").toBool() - && !m.html().text().isEmpty()) { - mv.setHtml(m.html().toString("span")); + if (dm.containsHTML() && PsiOptions::instance()->getOption("options.html.muc.render").toBool() + && !dm.html().text().isEmpty()) { + mv.setHtml(dm.html().toString("span")); } else { - mv.setPlainText(m.body()); + mv.setPlainText(dm.body()); } if (!PsiOptions::instance()->getOption("options.ui.muc.use-highlighting").toBool()) alert = false; - mv.setMessageId(m.id()); + mv.setMessageId(dm.id()); mv.setAlert(alert); - mv.setUserId(m.from().full()); // theoretically, this can be inferred from the chat dialog properties - mv.setNick(m.from().resource()); + mv.setUserId(dm.from().full()); // theoretically, this can be inferred from the chat dialog properties + mv.setNick(dm.from().resource()); mv.setLocal(mv.nick() == d->self); - mv.setSpooled(m.spooled()); - mv.setDateTime(m.timeStamp()); - mv.setReplaceId(m.replaceId()); - account()->psi()->fileSharingManager()->fillMessageView(mv, m, account()); + mv.setSpooled(dm.spooled()); + mv.setDateTime(dm.timeStamp()); + mv.setReplaceId(dm.replaceId()); + account()->psi()->fileSharingManager()->fillMessageView(mv, dm, account()); dispatchMessage(mv); @@ -2363,7 +2365,7 @@ void GCMainDlg::appendMessage(const Message &m, bool alert) d->keepOpen = true; QTimer::singleShot(1000, this, SLOT(setKeepOpenFalse())); }*/ - emit messageAppended(m.body(), ui_.log->textWidget()); + emit messageAppended(dm.body(), ui_.log->textWidget()); } void GCMainDlg::doAlert() diff --git a/src/messageview.cpp b/src/messageview.cpp index c002a95d1..0efbf262b 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -30,10 +30,7 @@ static const QString me_cmd = "/me "; // ====================================================================== // MessageView // ====================================================================== -MessageView::MessageView(Type t) : - _type(t), _status(0), _statusPriority(0), _dateTime(QDateTime::currentDateTime()), _carbon(XMPP::Message::NoCarbon) -{ -} +MessageView::MessageView(Type t) : _type(t), _status(0), _statusPriority(0), _dateTime(QDateTime::currentDateTime()) { } MessageView MessageView::fromPlainText(const QString &text, Type type) { diff --git a/src/messageview.h b/src/messageview.h index 6a5fee6ac..a071e858f 100644 --- a/src/messageview.h +++ b/src/messageview.h @@ -126,8 +126,6 @@ class MessageView { inline const QString "eId() const { return _quoteId; } inline void setRetractionId(const QString &id) { _retractionId = id; } inline const QString &retractionId() const { return _retractionId; } - inline void setCarbonDirection(XMPP::Message::CarbonDir c) { _carbon = c; } - inline XMPP::Message::CarbonDir carbonDirection() const { return _carbon; } inline void addReference(FileSharingItem *fsi) { _references.append(fsi); } inline const QList &references() const { return _references; } inline void setReactionsId(const QString &id) { _reactionsId = id; } @@ -151,7 +149,6 @@ class MessageView { QString _quoteId; QString _retractionId; QString _reactionsId; - XMPP::Message::CarbonDir _carbon; QList _references; QSet _reactions; }; diff --git a/src/psiaccount.cpp b/src/psiaccount.cpp index 3bf4a8eea..80650f14d 100644 --- a/src/psiaccount.cpp +++ b/src/psiaccount.cpp @@ -24,7 +24,6 @@ */ #include "psiaccount.h" - #include "Certificates/CertificateDisplayDialog.h" #include "Certificates/CertificateHelpers.h" #include "accountmanagedlg.h" @@ -96,6 +95,8 @@ #include "iris/s5b.h" #include "iris/xmpp_caps.h" #include "iris/xmpp_captcha.h" +#include "iris/xmpp_carbons.h" +#include "iris/xmpp_forwarding.h" #include "iris/xmpp_serverinfomanager.h" #include "iris/xmpp_tasks.h" #include "iris/xmpp_xmlcommon.h" @@ -2382,9 +2383,7 @@ void PsiAccount::serverFeaturesChanged() } if (d->client->serverInfoManager()->canMessageCarbons()) { - JT_MessageCarbons *j = new JT_MessageCarbons(d->client->rootTask()); - j->enable(); - j->go(true); + d->client->carbonsManager()->setEnabled(true); } if (d->client->serverInfoManager()->serverFeatures().hasVCard() && !d->vcardChecked) { @@ -2774,35 +2773,44 @@ void PsiAccount::client_presenceError(const Jid &j, int, const QString &str) void PsiAccount::client_messageReceived(const Message &m) { - // check if it's a server message without a from, and set the from appropriately Message _m(m); - if (_m.from().isEmpty()) { - _m.setFrom(jid().domain()); + Message dm = _m.displayMessage(); + + // check if it's a server message without a from, and set the from appropriately + if (dm.from().isEmpty()) { + dm.setFrom(jid().domain()); + } + + // if there is no timestamp in the message, forwarded timestamp will be used + if (_m.forwarded().isCarbons() && !dm.spooled()) { + QDateTime ts = _m.forwarded().timeStamp(); + if (ts.isValid()) + dm.setTimeStamp(ts); } // if the sender is already in the queue, then queue this message also for (const Message &mi : std::as_const(d->messageQueue)) { - if (mi.from().compare(_m.from())) { + if (mi.from().compare(dm.from())) { d->messageQueue.append(_m); return; } } // check to see if message was forwarded from another resource - if (jid().compare(_m.from(), false)) { - AddressList oFrom = _m.findAddresses(Address::OriginalFrom); - AddressList oTo = _m.findAddresses(Address::OriginalTo); + if (jid().compare(dm.from(), false)) { + AddressList oFrom = dm.findAddresses(Address::OriginalFrom); + AddressList oTo = dm.findAddresses(Address::OriginalTo); if ((oFrom.count() > 0) && (oTo.count() > 0)) { // might want to store current values in MessageEvent object // replace out the from and to addresses with the original addresses - _m.setFrom(oFrom[0].jid()); - _m.setTo(oTo[0].jid()); + dm.setFrom(oFrom.at(0).jid()); + dm.setTo(oTo.at(0).jid()); } } #ifdef HAVE_PGPUTIL // encrypted message? - if (PGPUtil::instance().pgpAvailable() && !_m.xencrypted().isEmpty()) { + if (PGPUtil::instance().pgpAvailable() && !dm.xencrypted().isEmpty()) { d->messageQueue.append(_m); processMessageQueue(); return; @@ -2835,33 +2843,38 @@ void PsiAccount::wbRequest(const Jid &j, int id) */ void PsiAccount::processIncomingMessage(const Message &_m) { + bool selfMessage = _m.forwarded().type() == Forwarding::ForwardedCarbonsSent; + Message dm = _m.displayMessage(); // skip empty messages, but not if the message contains a data form - if (_m.type() != Message::Type::Error && _m.body().isEmpty() && _m.urlList().isEmpty() && _m.invite().isEmpty() - && !_m.containsEvents() && _m.chatState() == StateNone && _m.subject().isNull() - && _m.rosterExchangeItems().isEmpty() && _m.mucInvites().isEmpty() && _m.getForm().fields().empty() - && _m.messageReceipt() == ReceiptNone && _m.getMUCStatuses().isEmpty() && _m.reactions().targetId.isEmpty() - && _m.retraction().isEmpty()) + if (dm.type() != Message::Type::Error && dm.body().isEmpty() && dm.urlList().isEmpty() && dm.invite().isEmpty() + && !dm.containsEvents() && dm.chatState() == StateNone && dm.subject().isNull() + && dm.rosterExchangeItems().isEmpty() && dm.mucInvites().isEmpty() && dm.getForm().fields().empty() + && dm.messageReceipt() == ReceiptNone && dm.getMUCStatuses().isEmpty() && dm.reactions().targetId.isEmpty() + && dm.retraction().isEmpty()) return; // skip headlines? - if (_m.type() == Message::Type::Headline + if (dm.type() == Message::Type::Headline && PsiOptions::instance()->getOption("options.messages.ignore-headlines").toBool()) return; - if (_m.getForm().registrarType() == "urn:xmpp:captcha") { - CaptchaChallenge challenge(_m); + if (dm.getForm().registrarType() == "urn:xmpp:captcha") { + CaptchaChallenge challenge(dm); if (challenge.isValid()) { + if (selfMessage) { + return; + } QWidget *pw = nullptr; MUCJoinDlg *joinDlg = nullptr; - if (_m.from().resource().isEmpty()) { - pw = findDialog(_m.from()); + if (dm.from().resource().isEmpty()) { + pw = findDialog(dm.from()); if (!pw) { - joinDlg = findDialog(_m.from()); + joinDlg = findDialog(dm.from()); pw = joinDlg; } } if (!pw) { - pw = findChatDialog(_m.from()); + pw = findChatDialog(dm.from()); } // it's possible there is no any related dialog. like registration form? CaptchaDlg *dlg = new CaptchaDlg(pw, challenge, this); @@ -2875,7 +2888,7 @@ void PsiAccount::processIncomingMessage(const Message &_m) } #ifdef GROUPCHAT - if (_m.type() == Message::Type::Groupchat) { + if (dm.type() == Message::Type::Groupchat) { MessageEvent::Ptr me(new MessageEvent(_m, this)); me->setOriginLocal(false); handleEvent(me, IncomingStanza); @@ -2883,26 +2896,28 @@ void PsiAccount::processIncomingMessage(const Message &_m) } #endif - if (_m.type() != Message::Type::Error) { - UserListItem *u = findFirstRelevant(_m.from()); - if (u) { - if (_m.type() == Message::Type::Chat) - u->setLastMessageType(1); + Jid dj = _m.displayJid(); + QList ul; + if (!selfMessage) { + ul = findRelevant(dj); + if (!ul.isEmpty()) { + if (dm.type() == Message::Type::Chat) + ul.first()->setLastMessageType(1); else - u->setLastMessageType(0); + ul.first()->setLastMessageType(0); } } - Message m = _m; + Message m = _m; + Message dm2 = m.displayMessage(); // smartchat: try to match up the incoming event to an existing chat // (prior to 0.9, m.from() always contained a resource) - Jid j; - ChatDlg *c; - QList ul = findRelevant(m.from()); + ChatDlg *c; // ignore events from non-roster JIDs? - if (ul.isEmpty() && PsiOptions::instance()->getOption("options.messages.ignore-non-roster-contacts").toBool()) { + if (!selfMessage && ul.isEmpty() + && PsiOptions::instance()->getOption("options.messages.ignore-non-roster-contacts").toBool()) { if (PsiOptions::instance()->getOption("options.messages.exclude-muc-from-ignore").toBool()) { #ifdef GROUPCHAT GCMainDlg *w = findDialog(Jid(_m.from().bare())); @@ -2916,46 +2931,46 @@ void PsiAccount::processIncomingMessage(const Message &_m) } if (ul.isEmpty()) - j = m.from().bare(); + dj = dj.bare(); else - j = ul.first()->jid(); + dj = ul.first()->jid(); /*c = findChatDialog(j); if(!c) c = findChatDialog(m.from().full());*/ - if (m.type() == Message::Type::Error) { - Stanza::Error err = m.error(); + if (dm2.type() == Message::Type::Error) { + Stanza::Error err = dm2.error(); QPair desc = err.description(); QString msg = desc.first + ".\n" + desc.second; if (!err.text.isEmpty()) msg += "\n" + err.text; - m.setBody(msg + "\n------\n" + m.body()); + dm2.setBody(msg + "\n------\n" + dm2.body()); } else { // only toggle if not an invite or body is not empty if (_m.invite().isEmpty() && !_m.body().isEmpty() && _m.mucInvites().isEmpty() && _m.rosterExchangeItems().isEmpty()) - toggleSecurity(_m.from(), _m.wasEncrypted()); + toggleSecurity(dj, _m.wasEncrypted()); // Roster item exchange - if (!_m.rosterExchangeItems().isEmpty()) { - RosterExchangeEvent::Ptr ree(new RosterExchangeEvent(j, _m.rosterExchangeItems(), _m.body(), this)); + if (!_m.forwarded().isCarbons() && !_m.rosterExchangeItems().isEmpty()) { + RosterExchangeEvent::Ptr ree(new RosterExchangeEvent(dj, _m.rosterExchangeItems(), _m.body(), this)); handleEvent(ree, IncomingStanza); return; } // change the type? - if (m.type() != Message::Type::Headline && m.invite().isEmpty() && m.mucInvites().isEmpty()) { + if (dm2.type() != Message::Type::Headline && dm2.invite().isEmpty() && dm2.mucInvites().isEmpty()) { const QString type = PsiOptions::instance()->getOption("options.messages.force-incoming-message-type").toString(); if (type == "message") - m.setType(Message::Type::Normal); + dm2.setType(Message::Type::Normal); else if (type == "chat") - m.setType(Message::Type::Chat); + dm2.setType(Message::Type::Chat); else if (type == "current-open") { c = nullptr; - const auto &dlgs = findChatDialogs(m.from(), false); + const auto &dlgs = findChatDialogs(dj, false); for (ChatDlg *cl : dlgs) { if (cl->autoSelectContact() || cl->jid().resource().isEmpty() || m.from().resource() == cl->jid().resource()) { @@ -2964,9 +2979,9 @@ void PsiAccount::processIncomingMessage(const Message &_m) } } if (c != nullptr && !c->isHidden()) - m.setType(Message::Type::Chat); + dm2.setType(Message::Type::Chat); else - m.setType(Message::Type::Normal); + dm2.setType(Message::Type::Normal); } } @@ -2974,17 +2989,28 @@ void PsiAccount::processIncomingMessage(const Message &_m) // if(m.type() == "chat" && (!m.urlList().isEmpty() || !m.subject().isEmpty())) // m.setType(""); - if (m.messageReceipt() == ReceiptRequest && !m.id().isEmpty() - && PsiOptions::instance()->getOption("options.ui.notifications.send-receipts").toBool()) { - UserListItem *u; - if (j.compare(d->self.jid(), false) || groupchats().contains(j.bare()) - || (!d->loginStatus.isInvisible() && (u = d->userList.find(j)) - && (u->subscription().type() == Subscription::To - || u->subscription().type() == Subscription::Both))) { - Message tm(m.from()); - tm.setMessageReceiptId(m.id()); - tm.setMessageReceipt(ReceiptReceived); - dj_sendMessage(tm, false); + if (dm2.messageReceipt() == ReceiptRequest) { + QString id; + switch (m.forwarded().type()) { + case Forwarding::ForwardedNone: + case Forwarding::ForwardedMessage: + case Forwarding::ForwardedCarbonsReceived: + id = dm2.id(); + break; + default: + break; + } + if (!id.isEmpty() && PsiOptions::instance()->getOption("options.ui.notifications.send-receipts").toBool()) { + UserListItem *u; + if (dj.compare(d->self.jid(), false) || groupchats().contains(dj.bare()) + || (!d->loginStatus.isInvisible() && (u = d->userList.find(dj)) + && (u->subscription().type() == Subscription::To + || u->subscription().type() == Subscription::Both))) { + Message tm(dm2.from()); + tm.setMessageReceiptId(id); + tm.setMessageReceipt(ReceiptReceived); + dj_sendMessage(tm, false); + } } } } @@ -3120,9 +3146,8 @@ void PsiAccount::setStatus(const Status &_s, bool withPriority, bool isManualSta // Block all transports' contacts' status change popups from popping { for (const auto &i : std::as_const(d->acc.roster)) { - if (i.jid() - .node() - .isEmpty() /*&& i.jid().resource() == "registered"*/) // it is very likely then, that it's transport + if (i.jid().node().isEmpty() /*&& i.jid().resource() == "registered"*/) // it is very likely then, + // that it's transport new BlockTransportPopup(d->blockTransportPopupList, i.jid()); // FIXME this code looks like a source for memory leak } @@ -3160,8 +3185,8 @@ void PsiAccount::setStatus(const Status &_s, bool withPriority, bool isManualSta } if (s.isInvisible()) { //&&Pass invis to transports KEVIN - // this is a nasty hack to let the transports know we're invisible, since they get an offline packet - // when we go invisible + // this is a nasty hack to let the transports know we're invisible, since they get an offline + // packet when we go invisible for (UserListItem *u : std::as_const(d->userList)) { if (u->isTransport()) { JT_Presence *j = new JT_Presence(d->client->rootTask()); @@ -4737,10 +4762,11 @@ void PsiAccount::openUri(const QUrl &uriToOpen) // TODO: default case - be more smart!! ;-) // if (QMessageBox::question(0, tr("Hmm.."), QString(tr("So, you'd like to open %1 URI, right?\n" - // "Unfortunately, this URI only identifies an entity, but it doesn't say what action to perform (or at least - // Psi cannot understand it). " "So it's pretty much like if I said \"John\" to you - you'd immediately ask - // \"But what about John?\".\n" "So... What about %1??\n" "At worst, you may send a message to %2 to ask what - // to do (and maybe complain about this URI ;)) " "Would you like to do this + // "Unfortunately, this URI only identifies an entity, but it doesn't say what action to perform (or + // at least Psi cannot understand it). " "So it's pretty much like if I said \"John\" to you - you'd + // immediately ask + // \"But what about John?\".\n" "So... What about %1??\n" "At worst, you may send a message to %2 to + // ask what to do (and maybe complain about this URI ;)) " "Would you like to do this // now?")).arg(uri).arg(entity.full()), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == // QMessageBox::Yes) actionSendMessage(entity); @@ -5084,53 +5110,45 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy return; } + MessageEvent::Ptr me = nullptr; + if (e->type() == PsiEvent::Message) + me = e.staticCast(); + if (activationType != FromXml) { if (e->type() == PsiEvent::Message || e->type() == PsiEvent::Auth) { - bool found = false; - - // don't log private messages - if (!found - && !(e->type() == PsiEvent::Message && e.staticCast()->message().body().isEmpty())) { + if (!me || !me->message().displayMessage().body().isEmpty()) { bool isMuc = false; #ifdef GROUPCHAT - if (e->type() == PsiEvent::Message) { - MessageEvent::Ptr me = e.staticCast(); - if (me->message().type() == Message::Type::Groupchat) - isMuc = true; - } + if (me && me->message().displayMessage().type() == Message::Type::Groupchat) + isMuc = true; #endif if (!isMuc) { Jid chatJid = e->from(); - if (e->type() == PsiEvent::Message) { - MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); - if (m.carbonDirection() == Message::Sent) { - chatJid = m.to(); - } - } - - int type = findGCContact(chatJid) ? EDB::GroupChatContact : EDB::Contact; + int type = findGCContact(chatJid) ? EDB::GroupChatContact : EDB::Contact; logEvent(chatJid, e, type); } } } } - if (e->type() == PsiEvent::Message) { - MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); + if (me) { + const Message &m = me->message(); + const Message dm = m.displayMessage(); + bool selfMessage = m.forwarded().type() == Forwarding::ForwardedCarbonsSent; #ifdef PSI_PLUGINS // TODO(mck): clean up // UserListItem *ulItem=nullptr; // if ( !ul.isEmpty() ) // ulItem=ul.first(); - if (PluginManager::instance()->processMessage(this, e->from().full(), m.body(), m.subject())) { - return; + if (!selfMessage) { + if (PluginManager::instance()->processMessage(this, e->from().full(), dm.body(), dm.subject())) { + return; + } + // PluginManager::instance()->message(this,e->from(),ulItem,((MessageEvent*)e)->message().body()); } - // PluginManager::instance()->message(this,e->from(),ulItem,((MessageEvent*)e)->message().body()); #endif - if (m.messageReceipt() == ReceiptReceived) { + if (dm.messageReceipt() == ReceiptReceived) { if (o->getOption("options.ui.notifications.request-receipts").toBool()) { const auto &dialogs = findChatDialogs(e->from(), false); for (ChatDlg *c : dialogs) { @@ -5144,9 +5162,9 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy } // Pass message events to chat window - if ((m.containsEvents() || m.chatState() != StateNone) && m.body().isEmpty() - && m.type() != Message::Type::Groupchat) { - if (m.carbonDirection() == Message::Sent) { + if ((dm.containsEvents() || dm.chatState() != StateNone) && dm.body().isEmpty() + && dm.type() != Message::Type::Groupchat) { + if (selfMessage) { return; // ignore own composing for carbon. TODO should we? } if (o->getOption("options.messages.send-composing-events").toBool()) { @@ -5156,7 +5174,7 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy c->incomingMessage(m); } } - if (m.chatState() == StateComposing) { + if (dm.chatState() == StateComposing) { doPopup = true; putToQueue = false; popupType = PopupManager::AlertComposing; @@ -5166,19 +5184,18 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy } // pass chat messages directly to a chat window if possible (and deal with sound) - else if (m.type() == Message::Type::Chat) { - Jid chatJid = m.carbonDirection() == Message::Sent ? m.to() : m.from(); - - if (m.carbonDirection() == Message::Sent) { + else if (dm.type() == Message::Type::Chat) { + Jid chatJid = m.displayJid(); + if (selfMessage) e->setOriginLocal(true); - doPopup = false; - } + // throw away carbons sent for MUC private messages - // server sends them to all resources so we have to explicitly skip them or we'll have a lot of duplicates - if (m.carbonDirection() == Message::Received && m.hasMUCUser()) { + // server sends them to all resources so we have to explicitly skip them or we'll have a lot of + // duplicates + if (m.forwarded().type() == Forwarding::ForwardedCarbonsReceived && m.hasMUCUser()) { return; } - ChatDlg *c = findChatDialogEx(chatJid, m.carbonDirection() == Message::Sent); + ChatDlg *c = findChatDialogEx(chatJid, selfMessage); if (c && c->jid().resource().isEmpty()) c->setJid(chatJid); @@ -5187,7 +5204,7 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy if (c && (d->tabManager->isChatTabbed(c) || !c->isHidden())) { c->incomingMessage(m); soundType = eChat2; - if (m.carbonDirection() != Message::Sent + if (selfMessage && ((o->getOption("options.ui.chat.alert-for-already-open-chats").toBool() && !c->isActiveTab()) || (c->isTabbed() && c->getManagingTabDlg()->isHidden()))) { @@ -5201,32 +5218,32 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy soundType = firstChat ? eChat1 : eChat2; } - if (putToQueue && m.carbonDirection() != Message::Sent) { + if (putToQueue && !selfMessage) { doPopup = true; popupType = PopupManager::AlertChat; } } // /chat - else if (m.type() == Message::Type::Headline) { + else if (dm.type() == Message::Type::Headline) { soundType = eHeadline; doPopup = true; popupType = PopupManager::AlertHeadline; } // /headline #ifdef GROUPCHAT - else if (m.type() == Message::Type::Groupchat) { + else if (dm.type() == Message::Type::Groupchat) { putToQueue = false; bool allowMucEvents = o->getOption("options.ui.muc.allow-highlight-events").toBool(); if (activationType != FromXml) { GCMainDlg *c = findDialog(e->from()); if (c) { c->message(m, e); - if (!c->isActiveTab() && c->isLastMessageAlert() && !m.spooled() && allowMucEvents) + if (!c->isActiveTab() && c->isLastMessageAlert() && !dm.spooled() && allowMucEvents) putToQueue = true; } } else if (allowMucEvents) putToQueue = true; } // /groupchat #endif - else if (m.type() == Message::Type::Normal) { + else if (dm.type() == Message::Type::Normal) { soundType = eMessage; doPopup = true; popupType = PopupManager::AlertMessage; @@ -5235,12 +5252,12 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy soundType = eSystem; } - if (m.carbonDirection() == Message::Sent) { + if (selfMessage) { doPopup = false; soundType = eNone; putToQueue = false; } - if (m.type() == Message::Type::Error) { + if (dm.type() == Message::Type::Error) { // FIXME: handle message errors // msg.text = QString(tr("[Error Message]
%1").arg(plain2rich(msg.text))); } @@ -5431,7 +5448,7 @@ void PsiAccount::queueEvent(const PsiEvent::Ptr &e, ActivationType activationTyp nick = ae->nick(); } else if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - if (me->message().type() != Message::Type::Error) + if (me->message().displayMessage().type() != Message::Type::Error) nick = me->nick(); } @@ -5460,10 +5477,10 @@ void PsiAccount::queueEvent(const PsiEvent::Ptr &e, ActivationType activationTyp // Check to see if we need to popup if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); - if (m.type() == Message::Type::Chat) + const Message dm = me->message().displayMessage(); + if (dm.type() == Message::Type::Chat) doPopup = PsiOptions::instance()->getOption("options.ui.chat.auto-popup").toBool(); - else if (m.type() == Message::Type::Headline) + else if (dm.type() == Message::Type::Headline) doPopup = PsiOptions::instance()->getOption("options.ui.message.auto-popup-headlines").toBool(); else doPopup = PsiOptions::instance()->getOption("options.ui.message.auto-popup").toBool(); @@ -5541,6 +5558,8 @@ int PsiAccount::forwardPendingEvents(const Jid &jid) for (const PsiEvent::Ptr &e : std::as_const(chatList)) { MessageEvent::Ptr me = e.staticCast(); Message m = me->message(); + if (m.forwarded().isCarbons()) + continue; AddressList oFrom = m.findAddresses(Address::OriginalFrom); AddressList oTo = m.findAddresses(Address::OriginalTo); @@ -5610,11 +5629,11 @@ void PsiAccount::processReadNext(const UserListItem &u) #endif if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); - if (m.type() == Message::Type::Chat && m.getForm().fields().empty()) + const Message &dm = me->message().displayMessage(); + if (dm.type() == Message::Type::Chat && dm.getForm().fields().empty()) isChat = true; #ifdef GROUPCHAT - else if (m.type() == Message::Type::Groupchat) + else if (dm.type() == Message::Type::Groupchat) isMuc = true; #endif } @@ -5674,11 +5693,9 @@ void PsiAccount::processChatsHelper(const Jid &j, bool removeEvents) for (const PsiEvent::Ptr &e : std::as_const(chatList)) { if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); - // process the message if (!me->sentToChatWindow()) { - c->incomingMessage(m); + c->incomingMessage(me->message()); me->setSentToChatWindow(true); } } @@ -5811,7 +5828,8 @@ void PsiAccount::shareImage(const Jid &target, const QImage &image, const QStrin Q_UNUSED(target) Q_UNUSED(description) - // this method is intended to use xep-0385. But let's do something simple first. xep-0385 will be implemented later + // this method is intended to use xep-0385. But let's do something simple first. xep-0385 will be + // implemented later auto buffer = new QBuffer(); buffer->open(QIODevice::ReadWrite); image.save(buffer, "PNG"); @@ -6145,7 +6163,7 @@ void PsiAccount::processPgpEncryptedMessage(const Message &m) connect(transaction, &GpgTransaction::transactionFinished, this, &PsiAccount::pgp_decryptFinished); transaction->setOrigMessage(m); - transaction->setStdInString(PGPUtil::instance().addHeaderFooter(m.xencrypted(), 0)); + transaction->setStdInString(PGPUtil::instance().addHeaderFooter(m.displayMessage().xencrypted(), 0)); transaction->start(); #endif } @@ -6156,22 +6174,22 @@ void PsiAccount::pgp_decryptFinished() GpgTransaction *transaction = dynamic_cast(sender()); if (!transaction) return; - + Message m = transaction->origMessage(); + Message dm = m.displayMessage(); if (transaction->success()) { - Message m = transaction->origMessage(); - m.setBody(transaction->stdOutString()); - m.setXEncrypted(""); - m.setWasEncrypted(true); - m.setEncryptionProtocol("Legacy OpenPGP"); + dm.setBody(transaction->stdOutString()); + dm.setXEncrypted({}); + dm.setWasEncrypted(true); + dm.setEncryptionProtocol("Legacy OpenPGP"); processIncomingMessage(m); } else { - if (loggedIn()) { + if (loggedIn() && m.forwarded().type() != Forwarding::ForwardedCarbonsSent) { Message m; - m.setTo(transaction->origMessage().from()); + m.setTo(dm.displayJid()); m.setType(Message::Type::Error); - if (!transaction->origMessage().id().isEmpty()) - m.setId(transaction->origMessage().id()); - m.setBody(transaction->origMessage().body()); + if (!dm.id().isEmpty()) + m.setId(dm.id()); + m.setBody(dm.body()); m.setError(Stanza::Error(Stanza::Error::ErrorType::Modify, Stanza::Error::ErrorCond::NotAcceptable, "Unable to decrypt")); @@ -6191,7 +6209,7 @@ void PsiAccount::processMessageQueue() #ifdef HAVE_PGPUTIL // encrypted? - if (PGPUtil::instance().pgpAvailable() && !mp.xencrypted().isEmpty()) { + if (PGPUtil::instance().pgpAvailable() && !mp.displayMessage().xencrypted().isEmpty()) { processPgpEncryptedMessageNext(); break; } diff --git a/src/psicon.cpp b/src/psicon.cpp index 5f3493f73..42ff17e75 100644 --- a/src/psicon.cpp +++ b/src/psicon.cpp @@ -1795,15 +1795,15 @@ void PsiCon::processEvent(const PsiEvent::Ptr &e, ActivationType activationType) bool sentToChatWindow = false; if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); + const Message dm = me->message().displayMessage(); #ifdef GROUPCHAT - if (m.type() == Message::Type::Groupchat) { + if (dm.type() == Message::Type::Groupchat) { isMuc = true; } else { #endif - bool emptyForm = m.getForm().fields().empty(); + bool emptyForm = dm.getForm().fields().empty(); // FIXME: Refactor this, PsiAccount and PsiEvent out - if (m.type() == Message::Type::Chat && emptyForm) { + if (dm.type() == Message::Type::Chat && emptyForm) { isChat = true; sentToChatWindow = me->sentToChatWindow(); } diff --git a/src/psievent.cpp b/src/psievent.cpp index 0e265157d..953b27179 100644 --- a/src/psievent.cpp +++ b/src/psievent.cpp @@ -187,17 +187,17 @@ int MessageEvent::type() const { return Message; } Jid MessageEvent::from() const { #ifdef GROUPCHAT - if (v_m.type() == Message::Type::Groupchat) - return v_m.from().bare(); + if (v_m.displayMessage().type() == Message::Type::Groupchat) + return v_m.displayJid().bare(); #endif - return v_m.carbonDirection() == XMPP::Message::Sent ? v_m.to() : v_m.from(); + return v_m.displayJid(); } void MessageEvent::setFrom(const Jid &j) { v_m.setFrom(j); } -QString MessageEvent::nick() const { return v_m.nick(); } +QString MessageEvent::nick() const { return v_m.displayMessage().nick(); } -void MessageEvent::setNick(const QString &nick) { v_m.setNick(nick); } +void MessageEvent::setNick(const QString &nick) { v_m.displayMessage().setNick(nick); } bool MessageEvent::sentToChatWindow() const { return v_sentToChatWindow; } @@ -248,9 +248,9 @@ bool MessageEvent::fromXml(PsiCon *psi, PsiAccount *account, const QDomElement * int MessageEvent::priority() const { - if (v_m.type() == Message::Type::Headline) + if (v_m.displayMessage().type() == Message::Type::Headline) return eventPriorityHeadline; - else if (v_m.type() == Message::Type::Chat) + else if (v_m.displayMessage().type() == Message::Type::Chat) return eventPriorityChat; return eventPriorityMessage; @@ -258,12 +258,13 @@ int MessageEvent::priority() const QString MessageEvent::description() const { - QStringList result; - if (!v_m.subject().isEmpty()) - result << v_m.subject(); - if (!v_m.body().isEmpty()) - result << v_m.body(); - const auto &urls = v_m.urlList(); + QStringList result; + const XMPP::Message d_m = v_m.displayMessage(); + if (!d_m.subject().isEmpty()) + result << d_m.subject(); + if (!d_m.body().isEmpty()) + result << d_m.body(); + const auto &urls = d_m.urlList(); for (const Url &url : urls) { QString text = url.url(); if (!url.desc().isEmpty()) @@ -767,7 +768,7 @@ PsiEvent::Ptr EventQueue::peekFirstChat(const Jid &j, bool compareRes) const PsiEvent::Ptr e = i->event(); if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - if (j.compare(me->from(), compareRes) && me->message().type() == Message::Type::Chat) + if (j.compare(me->from(), compareRes) && me->message().displayMessage().type() == Message::Type::Chat) return e; } } @@ -788,7 +789,7 @@ void EventQueue::extractChats(QList *el, const Jid &j, bool compa if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); if (j.compare(me->from(), compareRes) - && me->message().type() == Message::Type::Chat) { // FIXME: refactor-refactor-refactor + && me->message().displayMessage().type() == Message::Type::Chat) { // FIXME: refactor-refactor-refactor extract = true; } } diff --git a/src/psiiconset.cpp b/src/psiiconset.cpp index fc0314374..b938f3717 100644 --- a/src/psiiconset.cpp +++ b/src/psiiconset.cpp @@ -836,12 +836,12 @@ PsiIcon *PsiIconset::event2icon(const PsiEvent::Ptr &e) QString icon; if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - const Message &m = me->message(); - if (m.type() == Message::Type::Headline) + const Message dm = me->message().displayMessage(); + if (dm.type() == Message::Type::Headline) icon = "psi/headline"; - else if (m.type() == Message::Type::Chat || m.type() == Message::Type::Groupchat) + else if (dm.type() == Message::Type::Chat || dm.type() == Message::Type::Groupchat) icon = "psi/chat"; - else if (m.type() == Message::Type::Error) + else if (dm.type() == Message::Type::Error) icon = "psi/system"; else icon = "psi/message"; diff --git a/src/psipopup.cpp b/src/psipopup.cpp index 804c2cbe0..acf9ac9ac 100644 --- a/src/psipopup.cpp +++ b/src/psipopup.cpp @@ -326,12 +326,12 @@ void PsiPopup::setData(const Jid &j, const Resource &r, const UserListItem *u, c || !PsiOptions::instance()->getOption("options.ui.file-transfer.auto-popup").toBool())) { if ((event && event->type() == PsiEvent::Message) && (PsiOptions::instance()->getOption("options.ui.notifications.passive-popups.showMessage").toBool())) { - const Message *jmessage = &event.staticCast()->message(); - QString message; + const Message jmessage = event.staticCast()->message().displayMessage(); + QString message; - if (!jmessage->subject().isEmpty()) - message += "" + tr("Subject:") + " " + jmessage->subject() + "
"; - message += TextUtil::plain2rich(jmessage->body()); + if (!jmessage.subject().isEmpty()) + message += "" + tr("Subject:") + " " + jmessage.subject() + "
"; + message += TextUtil::plain2rich(jmessage.body()); if (!message.isEmpty()) { contactText += "
" + message + ""; diff --git a/version b/version index d44af2ff5..cc9ee7d93 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.5.2041 (2024-09-24, 4927ae62) +1.5.2051 (2024-09-29, 56f6c039)