diff options
| author | Melvin Keskin <melvo@olomono.de> | 2022-10-16 19:59:49 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-16 19:59:49 +0200 |
| commit | 66e5f060abe831fa08a758b9de44b29bfec8b3ba (patch) | |
| tree | 0fab0a2b20d6563c3522172129f0c5520c6028b7 | |
| parent | ecce762e109bc9d88f3f6b7925e8b33ffcc0f57d (diff) | |
| download | qxmpp-66e5f060abe831fa08a758b9de44b29bfec8b3ba.tar.gz | |
Implement XEP-0444: Message Reactions (#492)
https://xmpp.org/extensions/xep-0444.html
| -rw-r--r-- | doc/doap.xml | 8 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/base/QXmppConstants.cpp | 2 | ||||
| -rw-r--r-- | src/base/QXmppConstants_p.h | 2 | ||||
| -rw-r--r-- | src/base/QXmppMessage.cpp | 42 | ||||
| -rw-r--r-- | src/base/QXmppMessage.h | 5 | ||||
| -rw-r--r-- | src/base/QXmppMessageReaction.cpp | 132 | ||||
| -rw-r--r-- | src/base/QXmppMessageReaction.h | 40 | ||||
| -rw-r--r-- | src/client/QXmppClient.cpp | 2 | ||||
| -rw-r--r-- | tests/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | tests/qxmppmessage/tst_qxmppmessage.cpp | 26 | ||||
| -rw-r--r-- | tests/qxmppmessagereaction/tst_qxmppmessagereaction.cpp | 110 |
12 files changed, 372 insertions, 0 deletions
diff --git a/doc/doap.xml b/doc/doap.xml index a21293d4..c9b9e3e1 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -592,6 +592,14 @@ SPDX-License-Identifier: CC0-1.0 </implements> <implements> <xmpp:SupportedXep> + <xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0444.html'/> + <xmpp:status>complete</xmpp:status> + <xmpp:version>0.1</xmpp:version> + <xmpp:since>1.5</xmpp:since> + </xmpp:SupportedXep> + </implements> + <implements> + <xmpp:SupportedXep> <xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0446.html'/> <xmpp:status>complete</xmpp:status> <xmpp:version>0.2</xmpp:version> diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 089039b7..1b4595f5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -54,6 +54,7 @@ set(INSTALL_HEADER_FILES base/QXmppLogger.h base/QXmppMamIq.h base/QXmppMessage.h + base/QXmppMessageReaction.h base/QXmppMixInfoItem.h base/QXmppMixInvitation.h base/QXmppMixIq.h @@ -183,6 +184,7 @@ set(SOURCE_FILES base/QXmppLogger.cpp base/QXmppMamIq.cpp base/QXmppMessage.cpp + base/QXmppMessageReaction.cpp base/QXmppMixInvitation.cpp base/QXmppMixIq.cpp base/QXmppMixItems.cpp diff --git a/src/base/QXmppConstants.cpp b/src/base/QXmppConstants.cpp index 0a8152f9..3d1afe88 100644 --- a/src/base/QXmppConstants.cpp +++ b/src/base/QXmppConstants.cpp @@ -199,6 +199,8 @@ const char *ns_mix_misc = "urn:xmpp:mix:misc:0"; const char *ns_fallback_indication = "urn:xmpp:fallback:0"; // XEP-0434: Trust Messages (TM) const char *ns_tm = "urn:xmpp:tm:1"; +// XEP-0444: Message Reactions +const char *ns_reactions = "urn:xmpp:reactions:0"; // XEP-0446: File metadata element const char *ns_file_metadata = "urn:xmpp:file:metadata:0"; // XEP-0447: Stateless file sharing diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h index f5086cc9..88489e0c 100644 --- a/src/base/QXmppConstants_p.h +++ b/src/base/QXmppConstants_p.h @@ -211,6 +211,8 @@ extern const char *ns_mix_misc; extern const char *ns_fallback_indication; // XEP-0434: Trust Messages (TM) extern const char *ns_tm; +// XEP-0444: Message Reactions +extern const char *ns_reactions; // XEP-0446: File metadata element extern const char *ns_file_metadata; // XEP-0447: Stateless file sharing diff --git a/src/base/QXmppMessage.cpp b/src/base/QXmppMessage.cpp index 7998598c..c0923d22 100644 --- a/src/base/QXmppMessage.cpp +++ b/src/base/QXmppMessage.cpp @@ -11,6 +11,7 @@ #include "QXmppConstants_p.h" #include "QXmppFileShare.h" #include "QXmppGlobal_p.h" +#include "QXmppMessageReaction.h" #include "QXmppMixInvitation.h" #ifdef BUILD_OMEMO #include "QXmppOmemoElement_p.h" @@ -154,6 +155,9 @@ public: // XEP-0434: Trust Messages (TM) std::optional<QXmppTrustMessageElement> trustMessageElement; + // XEP-0444: Message Reactions + std::optional<QXmppMessageReaction> reaction; + // XEP-0448: Encryption for stateless file sharing QVector<QXmppFileShare> sharedFiles; }; @@ -1239,6 +1243,32 @@ void QXmppMessage::setTrustMessageElement(const std::optional<QXmppTrustMessageE } /// +/// Returns a reaction to a message as defined by \xep{0444, Message Reactions}. +/// +/// \since QXmpp 1.5 +/// +std::optional<QXmppMessageReaction> QXmppMessage::reaction() const +{ + return d->reaction; +} + +/// +/// Sets a reaction to a message as defined by \xep{0444, Message Reactions}. +/// +/// The corresponding hint must be set manually: +/// \code +/// QXmppMessage message; +/// message.addHint(QXmppMessage::Store); +/// \endcode +/// +/// \since QXmpp 1.5 +/// +void QXmppMessage::setReaction(const std::optional<QXmppMessageReaction> &reaction) +{ + d->reaction = reaction; +} + +/// /// Returns the via \xep{0447, Stateless file sharing} shared files attached to this message. /// /// \since QXmpp 1.5 @@ -1544,6 +1574,13 @@ bool QXmppMessage::parseExtension(const QDomElement &element, QXmpp::SceMode sce d->trustMessageElement = trustMessageElement; return true; } + // XEP-0444: Message Reactions + if (QXmppMessageReaction::isMessageReaction(element)) { + QXmppMessageReaction reaction; + reaction.parse(element); + d->reaction = std::move(reaction); + return true; + } // XEP-0448: Stateless file sharing if (checkElement(element, QStringLiteral("file-sharing"), ns_sfs)) { QXmppFileShare share; @@ -1800,6 +1837,11 @@ void QXmppMessage::serializeExtensions(QXmlStreamWriter *writer, QXmpp::SceMode d->trustMessageElement->toXml(writer); } + // XEP-0444: Message Reactions + if (d->reaction) { + d->reaction->toXml(writer); + } + // XEP-0448: Stateless file sharing for (const auto &fileShare : d->sharedFiles) { fileShare.toXml(writer); diff --git a/src/base/QXmppMessage.h b/src/base/QXmppMessage.h index 899f477e..be9e9b96 100644 --- a/src/base/QXmppMessage.h +++ b/src/base/QXmppMessage.h @@ -18,6 +18,7 @@ class QXmppMessagePrivate; class QXmppBitsOfBinaryDataList; +class QXmppMessageReaction; class QXmppMixInvitation; #ifdef BUILD_OMEMO class QXmppOmemoElement; @@ -253,6 +254,10 @@ public: std::optional<QXmppTrustMessageElement> trustMessageElement() const; void setTrustMessageElement(const std::optional<QXmppTrustMessageElement> &trustMessageElement); + // XEP-0444: Message Reactions + std::optional<QXmppMessageReaction> reaction() const; + void setReaction(const std::optional<QXmppMessageReaction> &reaction); + // XEP-0447: Stateless file sharing const QVector<QXmppFileShare> &sharedFiles() const; void setSharedFiles(const QVector<QXmppFileShare> &sharedFiles); diff --git a/src/base/QXmppMessageReaction.cpp b/src/base/QXmppMessageReaction.cpp new file mode 100644 index 00000000..4bf9dcb3 --- /dev/null +++ b/src/base/QXmppMessageReaction.cpp @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppMessageReaction.h" + +#include "QXmppConstants_p.h" +#include "QXmppUtils.h" + +#include <QDomElement> + +class QXmppMessageReactionPrivate : public QSharedData +{ +public: + QString messageId; + QVector<QString> emojis; +}; + +/// +/// \class QXmppMessageReaction +/// +/// \brief The QXmppMessageReaction class represents a reaction to a message in the form of emojis +/// as specified by \xep{0444, Message Reactions}. +/// +/// \since QXmpp 1.5 +/// + +/// +/// Constructs a message reaction. +/// +QXmppMessageReaction::QXmppMessageReaction() + : d(new QXmppMessageReactionPrivate) +{ +} + +QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppMessageReaction) + +/// +/// Returns the ID of the message for that the reaction is sent. +/// +/// For a group chat message, \code QXmppMessage::stanzaId() \endcode is used. +/// +/// For other message types, \code QXmppMessage::originId() \endcode is used. +/// If that is not available, \code QXmppMessage::id() \endcode is used. +/// +/// \return the message's ID +/// +QString QXmppMessageReaction::messageId() const +{ + return d->messageId; +} + +/// +/// Sets the ID of the message for that the reaction is sent. +/// +/// For a group chat message, \code QXmppMessage::stanzaId() \endcode must be used. +/// If there is no such ID, a message reaction must not be sent. +/// +/// For other message types, \code QXmppMessage::originId() \endcode should be used. +/// If that is not available, \code QXmppMessage::id() \endcode should be used. +/// +/// \param messageId message's ID +/// +void QXmppMessageReaction::setMessageId(const QString &messageId) +{ + d->messageId = messageId; +} + +/// +/// Returns the emojis in reaction to a message. +/// +/// \return the emoji reactions +/// +QVector<QString> QXmppMessageReaction::emojis() const +{ + return d->emojis; +} + +/// +/// Sets the emojis in reaction to a message. +/// +/// Each reaction should only consist of unicode codepoints that can be displayed as a single emoji. +/// Duplicates are not allowed. +/// +/// \param emojis emoji reactions +/// +void QXmppMessageReaction::setEmojis(const QVector<QString> &emojis) +{ + d->emojis = emojis; +} + +/// \cond +void QXmppMessageReaction::parse(const QDomElement &element) +{ + d->messageId = element.attribute(QStringLiteral("id")); + + for (auto childElement = element.firstChildElement(); + !childElement.isNull(); + childElement = childElement.nextSiblingElement()) { + d->emojis.append(childElement.text()); + } + + // Remove duplicate emojis. + std::sort(d->emojis.begin(), d->emojis.end()); + d->emojis.erase(std::unique(d->emojis.begin(), d->emojis.end()), d->emojis.end()); +} + +void QXmppMessageReaction::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("reactions")); + writer->writeDefaultNamespace(ns_reactions); + writer->writeAttribute(QStringLiteral("id"), d->messageId); + + for (const auto &reaction : d->emojis) { + helperToXmlAddTextElement(writer, QStringLiteral("reaction"), reaction); + } + writer->writeEndElement(); +} +/// \endcond + +/// +/// Determines whether the given DOM element is a message reaction element. +/// +/// \param element DOM element being checked +/// +/// \return true if element is a message reaction element, otherwise false +/// +bool QXmppMessageReaction::isMessageReaction(const QDomElement &element) +{ + return element.tagName() == QStringLiteral("reactions") && + element.namespaceURI() == ns_reactions; +} diff --git a/src/base/QXmppMessageReaction.h b/src/base/QXmppMessageReaction.h new file mode 100644 index 00000000..ce8fdfc1 --- /dev/null +++ b/src/base/QXmppMessageReaction.h @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPMESSAGEREACTION_H +#define QXMPPMESSAGEREACTION_H + +#include "QXmppGlobal.h" + +#include <QSharedDataPointer> + +class QDomElement; +class QXmlStreamWriter; +class QXmppMessageReactionPrivate; + +class QXMPP_EXPORT QXmppMessageReaction +{ +public: + QXmppMessageReaction(); + + QXMPP_PRIVATE_DECLARE_RULE_OF_SIX(QXmppMessageReaction) + + QString messageId() const; + void setMessageId(const QString &messageId); + + QVector<QString> emojis() const; + void setEmojis(const QVector<QString> &emojis); + + /// \cond + void parse(const QDomElement &element); + void toXml(QXmlStreamWriter *writer) const; + /// \endcond + + static bool isMessageReaction(const QDomElement &element); + +private: + QSharedDataPointer<QXmppMessageReactionPrivate> d; +}; + +#endif // QXMPPMESSAGEREACTION_H diff --git a/src/client/QXmppClient.cpp b/src/client/QXmppClient.cpp index f8be81b0..f69bc29b 100644 --- a/src/client/QXmppClient.cpp +++ b/src/client/QXmppClient.cpp @@ -112,6 +112,8 @@ QStringList QXmppClientPrivate::discoveryFeatures() ns_spoiler, // XEP-0428: Fallback Indication ns_fallback_indication, + // XEP-0444: Message Reactions + ns_reactions, }; } /// \endcond diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e43b50c6..3e55e4d9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -43,6 +43,7 @@ add_simple_test(qxmppmammanager) add_simple_test(qxmppmixinvitation) add_simple_test(qxmppmixitems) add_simple_test(qxmppmessage) +add_simple_test(qxmppmessagereaction) add_simple_test(qxmppmessagereceiptmanager) add_simple_test(qxmppmixiq) add_simple_test(qxmppnonsaslauthiq) diff --git a/tests/qxmppmessage/tst_qxmppmessage.cpp b/tests/qxmppmessage/tst_qxmppmessage.cpp index 1aa2c673..ee964a4c 100644 --- a/tests/qxmppmessage/tst_qxmppmessage.cpp +++ b/tests/qxmppmessage/tst_qxmppmessage.cpp @@ -8,6 +8,7 @@ #include "QXmppBitsOfBinaryDataList.h" #include "QXmppEncryptedFileSource.h" #include "QXmppMessage.h" +#include "QXmppMessageReaction.h" #include "QXmppMixInvitation.h" #include "QXmppOutOfBandUrl.h" #include "QXmppTrustMessageElement.h" @@ -53,6 +54,7 @@ private slots: void testSlashMe(); void testMixInvitation(); void testTrustMessageElement(); + void testReaction(); void testE2eeFallbackBody(); void testFileSharing(); void testEncryptedFileSource(); @@ -1129,6 +1131,30 @@ void tst_QXmppMessage::testTrustMessageElement() QVERIFY(message2.trustMessageElement()); } +void tst_QXmppMessage::testReaction() +{ + const QByteArray xml( + "<message id=\"96d73204-a57a-11e9-88b8-4889e7820c76\" to=\"romeo@capulet.net/orchard\" type=\"chat\">" + "<store xmlns=\"urn:xmpp:hints\"/>" + "<reactions xmlns=\"urn:xmpp:reactions:0\" id=\"744f6e18-a57a-11e9-a656-4889e7820c76\">" + "<reaction>🐢</reaction>" + "<reaction>👋</reaction>" + "</reactions>" + "</message>"); + + QXmppMessage message1; + QVERIFY(!message1.reaction()); + + parsePacket(message1, xml); + QVERIFY(message1.reaction()); + serializePacket(message1, xml); + + QXmppMessage message2; + message2.addHint(QXmppMessage::Store); + message2.setReaction(QXmppMessageReaction()); + QVERIFY(message2.reaction()); +} + void tst_QXmppMessage::testE2eeFallbackBody() { const QByteArray xml( diff --git a/tests/qxmppmessagereaction/tst_qxmppmessagereaction.cpp b/tests/qxmppmessagereaction/tst_qxmppmessagereaction.cpp new file mode 100644 index 00000000..8acbe6c9 --- /dev/null +++ b/tests/qxmppmessagereaction/tst_qxmppmessagereaction.cpp @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppMessageReaction.h" + +#include "util.h" +#include <QObject> + +class tst_QXmppMessageReaction : public QObject +{ + Q_OBJECT + +private slots: + void testIsMessageReaction_data(); + void testIsMessageReaction(); + void testMessageReaction(); + void testMessageReactionWithDuplicateEmojis(); + void testMessageReactionRemoval(); +}; + +void tst_QXmppMessageReaction::testIsMessageReaction_data() +{ + QTest::addColumn<QByteArray>("xml"); + QTest::addColumn<bool>("isValid"); + + QTest::newRow("valid") + << QByteArrayLiteral("<reactions xmlns=\"urn:xmpp:reactions:0\"/>") + << true; + QTest::newRow("invalidTag") + << QByteArrayLiteral("<invalid xmlns=\"urn:xmpp:reactions:0\"/>") + << false; + QTest::newRow("invalidNamespace") + << QByteArrayLiteral("<reactions xmlns=\"invalid\"/>") + << false; +} + +void tst_QXmppMessageReaction::testIsMessageReaction() +{ + QFETCH(QByteArray, xml); + QFETCH(bool, isValid); + + QCOMPARE(QXmppMessageReaction::isMessageReaction(xmlToDom(xml)), isValid); +} + +void tst_QXmppMessageReaction::testMessageReaction() +{ + const QByteArray xml( + "<reactions xmlns=\"urn:xmpp:reactions:0\" id=\"744f6e18-a57a-11e9-a656-4889e7820c76\">" + "<reaction>🐢</reaction>" + "<reaction>👋</reaction>" + "</reactions>"); + + QXmppMessageReaction reaction1; + QVERIFY(reaction1.messageId().isEmpty()); + QVERIFY(reaction1.emojis().isEmpty()); + + parsePacket(reaction1, xml); + QCOMPARE(reaction1.messageId(), QStringLiteral("744f6e18-a57a-11e9-a656-4889e7820c76")); + QCOMPARE(reaction1.emojis().at(0), QStringLiteral("🐢")); + QCOMPARE(reaction1.emojis().at(1), QStringLiteral("👋")); + + serializePacket(reaction1, xml); + + QXmppMessageReaction reaction2; + reaction2.setMessageId(QStringLiteral("744f6e18-a57a-11e9-a656-4889e7820c76")); + reaction2.setEmojis({ QStringLiteral("🐢"), QStringLiteral("👋") }); + + QCOMPARE(reaction1.messageId(), QStringLiteral("744f6e18-a57a-11e9-a656-4889e7820c76")); + QCOMPARE(reaction1.emojis().at(0), QStringLiteral("🐢")); + QCOMPARE(reaction1.emojis().at(1), QStringLiteral("👋")); + + serializePacket(reaction2, xml); +} + +void tst_QXmppMessageReaction::testMessageReactionWithDuplicateEmojis() +{ + const QByteArray xml( + "<reactions xmlns=\"urn:xmpp:reactions:0\" id=\"744f6e18-a57a-11e9-a656-4889e7820c76\">" + "<reaction>🐢</reaction>" + "<reaction>👋</reaction>" + "<reaction>🐢</reaction>" + "<reaction>👋</reaction>" + "</reactions>"); + + QXmppMessageReaction reaction; + + parsePacket(reaction, xml); + QCOMPARE(reaction.messageId(), QStringLiteral("744f6e18-a57a-11e9-a656-4889e7820c76")); + QCOMPARE(reaction.emojis().size(), 2); + QCOMPARE(reaction.emojis().at(0), QStringLiteral("🐢")); + QCOMPARE(reaction.emojis().at(1), QStringLiteral("👋")); +} + +void tst_QXmppMessageReaction::testMessageReactionRemoval() +{ + const QByteArray xml( + "<reactions xmlns=\"urn:xmpp:reactions:0\" id=\"744f6e18-a57a-11e9-a656-4889e7820c76\"/>"); + + QXmppMessageReaction reaction; + + parsePacket(reaction, xml); + QCOMPARE(reaction.messageId(), QStringLiteral("744f6e18-a57a-11e9-a656-4889e7820c76")); + QCOMPARE(reaction.emojis().size(), 0); + + serializePacket(reaction, xml); +} + +QTEST_MAIN(tst_QXmppMessageReaction) +#include "tst_qxmppmessagereaction.moc" |
