aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTibor Csötönyi <work@taibsu.de>2023-05-03 14:46:38 +0200
committerLinus Jahn <lnj@kaidan.im>2023-05-14 23:58:01 +0200
commita4dcd906850b5ebecfdf7331059a13192f094f07 (patch)
treefa9a77ad38d1f702ec15672d907e4851e6c677c1
parent2fde987d39dc66f028ea3ff44929ebd6e2b37f90 (diff)
Add XEP-0353: Jingle Message Initiation manager
-rw-r--r--doc/doap.xml8
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/client/QXmppJingleMessageInitiationManager.cpp584
-rw-r--r--src/client/QXmppJingleMessageInitiationManager.h126
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/qxmppjinglemessageinitiationmanager/tst_qxmppjinglemessageinitiationmanager.cpp924
6 files changed, 1645 insertions, 0 deletions
diff --git a/doc/doap.xml b/doc/doap.xml
index e32eff05..0bbb1cc9 100644
--- a/doc/doap.xml
+++ b/doc/doap.xml
@@ -491,6 +491,14 @@ SPDX-License-Identifier: CC0-1.0
</implements>
<implements>
<xmpp:SupportedXep>
+ <xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0353.html'/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.6</xmpp:version>
+ <xmpp:since>1.6</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
<xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0357.html'/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.4</xmpp:version>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 929554c4..7014ae60 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -108,6 +108,7 @@ set(INSTALL_HEADER_FILES
client/QXmppHttpUploadManager.h
client/QXmppInvokable.h
client/QXmppIqHandling.h
+ client/QXmppJingleMessageInitiationManager.h
client/QXmppMamManager.h
client/QXmppMessageHandler.h
client/QXmppMessageReceiptManager.h
@@ -243,6 +244,7 @@ set(SOURCE_FILES
client/QXmppInternalClientExtension.cpp
client/QXmppInvokable.cpp
client/QXmppIqHandling.cpp
+ client/QXmppJingleMessageInitiationManager.cpp
client/QXmppMamManager.cpp
client/QXmppMessageReceiptManager.cpp
client/QXmppMucManager.cpp
diff --git a/src/client/QXmppJingleMessageInitiationManager.cpp b/src/client/QXmppJingleMessageInitiationManager.cpp
new file mode 100644
index 00000000..d7a4cea1
--- /dev/null
+++ b/src/client/QXmppJingleMessageInitiationManager.cpp
@@ -0,0 +1,584 @@
+// SPDX-FileCopyrightText: 2023 Tibor Csötönyi <work@taibsu.de>
+// SPDX-FileCopyrightText: 2023 Melvin Keskin <melvo@olomono.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppJingleMessageInitiationManager.h"
+
+#include "QXmppClient.h"
+#include "QXmppConstants_p.h"
+#include "QXmppMessage.h"
+#include "QXmppPromise.h"
+#include "QXmppUtils.h"
+
+#include <QStringBuilder>
+#include <QUuid>
+
+using namespace QXmpp;
+using Jmi = QXmppJingleMessageInitiation;
+using JmiManager = QXmppJingleMessageInitiationManager;
+using JmiElement = QXmppJingleMessageInitiationElement;
+using JmiType = JmiElement::Type;
+
+class QXmppJingleMessageInitiationPrivate
+{
+public:
+ QXmppJingleMessageInitiationPrivate(JmiManager *manager)
+ : manager(manager)
+ {
+ }
+
+ QXmppTask<SendResult> request(JmiElement &&jmiElement);
+
+ QXmppJingleMessageInitiationManager *manager;
+ QString id;
+ QString callPartnerJid;
+ bool isProceeded { false };
+};
+
+///
+/// \brief Creates a Jingle Message Initiation request based on given type.
+/// \param type The request type (proceed, accept, reject, retract, finish).
+///
+QXmppTask<SendResult> QXmppJingleMessageInitiationPrivate::request(JmiElement &&jmiElement)
+{
+ jmiElement.setId(id);
+ return manager->sendMessage(jmiElement, callPartnerJid);
+}
+
+///
+/// \class QXmppJingleMessageInitiation
+///
+/// \brief The QXmppJingleMessageInitiation class holds information about the JMI element in the
+/// current context.
+///
+/// \since QXmpp 1.6
+///
+
+///
+/// \brief Constructs a Jingle Message Initiation object.
+///
+QXmppJingleMessageInitiation::QXmppJingleMessageInitiation(QXmppJingleMessageInitiationManager *manager)
+ : d(new QXmppJingleMessageInitiationPrivate(manager))
+{
+}
+
+QXmppJingleMessageInitiation::~QXmppJingleMessageInitiation() = default;
+
+///
+/// Creates a JMI element of type "ringing" and sends a request containing the element.
+///
+QXmppTask<SendResult> QXmppJingleMessageInitiation::ring()
+{
+ QXmppJingleMessageInitiationElement jmiElement;
+ jmiElement.setType(JmiType::Ringing);
+
+ return d->request(std::move(jmiElement));
+}
+
+///
+/// Creates a JMI element of type "proceed" and sends a request containing the element.
+///
+QXmppTask<SendResult> QXmppJingleMessageInitiation::proceed()
+{
+ QXmppJingleMessageInitiationElement jmiElement;
+ jmiElement.setType(JmiType::Proceed);
+
+ return d->request(std::move(jmiElement));
+}
+
+///
+/// Creates a JMI element of type "reject" and sends a request containing the element.
+/// The default reason tag/type will be "busy" with text "Busy".
+///
+/// \param reason Reason object for reject element
+/// \param containsTieBreak Whether the reject element contains a tie-break tag or not
+///
+QXmppTask<SendResult> QXmppJingleMessageInitiation::reject(std::optional<QXmppJingleReason> reason, bool containsTieBreak)
+{
+ JmiElement jmiElement;
+ jmiElement.setType(JmiType::Reject);
+
+ if (!reason) {
+ reason = QXmppJingleReason();
+ reason->setType(QXmppJingleReason::Busy);
+ reason->setText(QStringLiteral("Busy"));
+ }
+
+ jmiElement.setReason(reason);
+ jmiElement.setContainsTieBreak(containsTieBreak);
+
+ return d->request(std::move(jmiElement));
+}
+
+///
+/// Creates a JMI element of type "retract" and sends a request containing the element.
+/// The default reason tag/type will be "cancel" with text "Retracted".
+///
+/// \param reason Reason object for retract element
+/// \param containsTieBreak Whether the retract element contains a tie-break tag or not
+///
+QXmppTask<SendResult> QXmppJingleMessageInitiation::retract(std::optional<QXmppJingleReason> reason, bool containsTieBreak)
+{
+ JmiElement jmiElement;
+ jmiElement.setType(JmiType::Retract);
+
+ if (!reason) {
+ reason = QXmppJingleReason();
+ reason->setType(QXmppJingleReason::Cancel);
+ reason->setText(QStringLiteral("Retracted"));
+ }
+
+ jmiElement.setReason(reason);
+ jmiElement.setContainsTieBreak(containsTieBreak);
+
+ return d->request(std::move(jmiElement));
+}
+
+///
+/// Creates a JMI element of type "finish" and sends a request containing the element.
+/// The default reason type/tag will be "success" with text "Success".
+///
+/// \param reason Reason object for finish element
+/// \param migratedTo JMI id the session has been migrated to
+///
+QXmppTask<SendResult> QXmppJingleMessageInitiation::finish(std::optional<QXmppJingleReason> reason, const QString &migratedTo)
+{
+ JmiElement jmiElement;
+ jmiElement.setType(JmiType::Finish);
+
+ if (!reason) {
+ reason = QXmppJingleReason();
+ reason->setType(QXmppJingleReason::Success);
+ reason->setText(QStringLiteral("Success"));
+ }
+
+ jmiElement.setReason(reason);
+ jmiElement.setMigratedTo(migratedTo);
+
+ return d->request(std::move(jmiElement));
+}
+
+///
+/// Returns the JMI ID.
+///
+QString QXmppJingleMessageInitiation::id() const
+{
+ return d->id;
+}
+
+///
+/// Sets the JMI ID.
+///
+void QXmppJingleMessageInitiation::setId(const QString &id)
+{
+ d->id = id;
+}
+
+///
+/// Sets the call partner's bare JID.
+///
+/// Normally, the JMI ID would be sufficient in order to differentiate the JMIs.
+/// However, attackers pretending to be the call partner can be mitigated by caching the call
+/// partner's JID.
+///
+/// \param callPartnerJid bare JID of the call partner
+///
+void QXmppJingleMessageInitiation::setCallPartnerJid(const QString &callPartnerJid)
+{
+ d->callPartnerJid = callPartnerJid;
+}
+
+///
+/// Returns the call partner's bare JID.
+///
+/// \return the call partner's bare JID.
+///
+QString QXmppJingleMessageInitiation::callPartnerJid() const
+{
+ return d->callPartnerJid;
+}
+
+///
+/// Returns the "isProceeded" flag, e.g., if the Jingle Message Initiation has already been
+/// proceeded.
+///
+bool QXmppJingleMessageInitiation::isProceeded() const
+{
+ return d->isProceeded;
+}
+
+///
+/// Sets the "isProceeded" flag, e.g., if the Jingle Message Initiation has already been
+/// proceeded.
+///
+void QXmppJingleMessageInitiation::setIsProceeded(bool isProceeded)
+{
+ d->isProceeded = isProceeded;
+}
+
+///
+/// \fn QXmppJingleMessageInitiation::ringing()
+///
+/// Emitted when a propose request was accepted and the device starts ringing.
+///
+
+///
+/// \fn QXmppJingleMessageInitiation::proceeded(const QString &, const QString &)
+///
+/// Emitted when a propose request was successfully processed and accepted.
+///
+/// \param id belonging JMI id
+/// \param callPartnerResource resource of the call partner about to be called
+///
+
+///
+/// \fn QXmppJingleMessageInitiation::closed(const Result &)
+///
+/// Emitted when a call was ended either through rejection, retraction, finish or an error.
+///
+/// \param result close reason
+///
+
+class QXmppJingleMessageInitiationManagerPrivate
+{
+public:
+ QVector<std::shared_ptr<Jmi>> jmis;
+};
+
+///
+/// \typedef QXmppJingleMessageInitiationManager::ProposeResult
+///
+/// Contains JMI object or an error if sending the propose message failed.
+///
+
+///
+/// \class QXmppJingleMessageInitiationManager
+///
+/// \brief The QXmppJingleMessageInitiationManager class makes it possible to retrieve
+/// Jingle Message Initiation elements as defined by \xep{0353, Jingle Message Initiation}.
+///
+/// \since QXmpp 1.6
+///
+QXmppJingleMessageInitiationManager::QXmppJingleMessageInitiationManager()
+ : d(std::make_unique<QXmppJingleMessageInitiationManagerPrivate>())
+{
+}
+
+QXmppJingleMessageInitiationManager::~QXmppJingleMessageInitiationManager() = default;
+
+/// \cond
+QStringList QXmppJingleMessageInitiationManager::discoveryFeatures() const
+{
+ return { ns_jingle_message_initiation };
+}
+/// \endcond
+
+///
+/// Creates a proposal JMI element and passes it as a message.
+///
+QXmppTask<QXmppJingleMessageInitiationManager::ProposeResult> QXmppJingleMessageInitiationManager::propose(const QString &callPartnerJid, const QXmppJingleDescription &description)
+{
+ QXmppPromise<ProposeResult> promise;
+
+ QXmppJingleMessageInitiationElement jmiElement;
+ jmiElement.setType(JmiType::Propose);
+ jmiElement.setId(QXmppUtils::generateStanzaUuid());
+ jmiElement.setDescription(description);
+
+ sendMessage(jmiElement, callPartnerJid).then(this, [this, promise, callPartnerJid](SendResult result) mutable {
+ if (auto error = std::get_if<QXmppError>(&result)) {
+ warning(u"Error sending Jingle Message Initiation proposal: " % error->description);
+ promise.finish(*error);
+ } else {
+ promise.finish(addJmi(callPartnerJid));
+ }
+ });
+
+ return promise.task();
+}
+
+/// \cond
+bool QXmppJingleMessageInitiationManager::handleMessage(const QXmppMessage &message)
+{
+ // JMI messages must be of type "chat" and contain a <store/> hint.
+ if (message.type() != QXmppMessage::Chat || !message.hasHint(QXmppMessage::Store)) {
+ return false;
+ }
+
+ // Only continue if the message contains a JMI element.
+ if (auto jmiElement = message.jingleMessageInitiationElement()) {
+ return handleJmiElement(std::move(*jmiElement), message.from());
+ }
+
+ return false;
+}
+
+void QXmppJingleMessageInitiationManager::setClient(QXmppClient *client)
+{
+ QXmppClientExtension::setClient(client);
+}
+/// \endcond
+
+///
+/// Lets the client send a message to user with given callPartnerJid containing the JMI element.
+///
+/// \param jmiElement the JMI element to be passed
+/// \param callPartnerJid bare JID of the call partner
+///
+QXmppTask<SendResult> QXmppJingleMessageInitiationManager::sendMessage(const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerJid)
+{
+ QXmppMessage message;
+ message.setTo(callPartnerJid);
+ message.addHint(QXmppMessage::Store);
+ message.setJingleMessageInitiationElement(jmiElement);
+
+ return client()->send(std::move(message));
+}
+
+///
+/// Removes a JMI object from the JMIs vector.
+///
+/// \param jmi object to be removed
+///
+void QXmppJingleMessageInitiationManager::clear(const std::shared_ptr<QXmppJingleMessageInitiation> &jmi)
+{
+ d->jmis.erase(
+ std::remove_if(
+ d->jmis.begin(),
+ d->jmis.end(),
+ [&jmi](const auto &storedJmi) {
+ return jmi->id() == storedJmi->id() && jmi->callPartnerJid() == storedJmi->callPartnerJid();
+ }),
+ d->jmis.end());
+}
+
+///
+/// Removes all JMI objects from the JMI vector.
+///
+void QXmppJingleMessageInitiationManager::clearAll()
+{
+ d->jmis.clear();
+}
+
+bool QXmppJingleMessageInitiationManager::handleJmiElement(QXmppJingleMessageInitiationElement &&jmiElement, const QString &senderJid)
+{
+ auto jmiElementId = jmiElement.id();
+ auto callPartnerJid = QXmppUtils::jidToBareJid(senderJid);
+
+ // Check if there's already a JMI object with jmiElementId and callPartnerJid in JMIs vector.
+ // That means that a JMI has already been created with given (J)IDs.
+ auto itr = std::find_if(d->jmis.begin(), d->jmis.end(), [&jmiElementId, &callPartnerJid](const auto &jmi) {
+ return jmi->id() == jmiElementId && jmi->callPartnerJid() == callPartnerJid;
+ });
+
+ if (itr != d->jmis.end()) {
+ return handleExistingJmi(*itr, jmiElement, QXmppUtils::jidToResource(senderJid));
+ }
+
+ if (jmiElement.type() == JmiType::Propose) {
+ return handleProposeJmiElement(jmiElement, callPartnerJid);
+ }
+
+ return false;
+}
+
+///
+/// Handles a JMI object which already exists in the JMIs vector.
+///
+/// \param existingJmi JMI object to be handled
+/// \param jmiElement JMI element to be processed with the JMI object
+/// \param callPartnerResource resource of the call partner (i.e., phone, tablet etc.)
+/// \return success (true) or failure
+///
+bool QXmppJingleMessageInitiationManager::handleExistingJmi(const std::shared_ptr<Jmi> &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource)
+{
+ switch (jmiElement.type()) {
+ case JmiType::Ringing:
+ Q_EMIT existingJmi->ringing();
+ return true;
+ case JmiType::Proceed:
+ Q_EMIT existingJmi->proceeded(jmiElement.id(), callPartnerResource);
+ existingJmi->setIsProceeded(true);
+ return true;
+ case JmiType::Reject:
+ Q_EMIT existingJmi->closed(
+ Jmi::Rejected { jmiElement.reason(), jmiElement.containsTieBreak() });
+ return true;
+ case JmiType::Retract:
+ Q_EMIT existingJmi->closed(
+ Jmi::Retracted { jmiElement.reason(), jmiElement.containsTieBreak() });
+ return true;
+ case JmiType::Finish:
+ existingJmi->finish(jmiElement.reason(), jmiElement.migratedTo());
+ Q_EMIT existingJmi->closed(
+ Jmi::Finished { jmiElement.reason(), jmiElement.migratedTo() });
+ return true;
+ default:
+ return false;
+ }
+}
+
+///
+/// Handles a propose JMI element.
+///
+/// \param jmiElement to be handled
+/// \param callPartnerJid bare JID of the call partner
+/// \return success (true) or failure
+///
+bool QXmppJingleMessageInitiationManager::handleProposeJmiElement(const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerJid)
+{
+ // Check if there's already a JMI object with provided callPartnerJid in JMIs vector.
+ // That means that a propose has already been sent.
+ auto itr = std::find_if(
+ d->jmis.cbegin(),
+ d->jmis.cend(),
+ [&callPartnerJid](const auto &jmi) {
+ return jmi->callPartnerJid() == callPartnerJid;
+ });
+
+ // Tie break case or usual JMI proposal?
+ if (itr != d->jmis.end()) {
+ return handleTieBreak(*itr, jmiElement, callPartnerJid);
+ }
+
+ Q_EMIT proposed(addJmi(callPartnerJid), jmiElement.id(), jmiElement.description());
+ return true;
+}
+
+///
+/// Handles a tie break case as defined in https://xmpp.org/extensions/xep-0353.html#tie-breaking.
+/// \param existingJmi existing JMI object to be handled
+/// \param jmiElement JMI element to be processed with existing JMI object
+/// \param callPartnerResource resource of the call partner (i.e., phone, tablet etc.)
+/// \return success (true) or failure
+///
+bool QXmppJingleMessageInitiationManager::handleTieBreak(const std::shared_ptr<Jmi> &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource)
+{
+ // Tie break -> session is set to be expired
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Expired);
+
+ // Existing (proceeded) or non-existing session?
+ if (existingJmi->isProceeded()) {
+ return handleExistingSession(existingJmi, jmiElement.id());
+ }
+
+ // Tie break in propose state (no existing session) - two parties try calling each other
+ // at the same time, the proposal with the lower ID overrules the other one.
+ return handleNonExistingSession(existingJmi, jmiElement.id(), callPartnerResource);
+}
+
+///
+/// Device switch: session already exists and will be migrated to new device with id jmiElementId.
+///
+/// \param existingJmi Current JMI object
+/// \param jmiElementId New (counterpart's) session JMI element ID
+///
+bool QXmppJingleMessageInitiationManager::handleExistingSession(const std::shared_ptr<Jmi> &existingJmi, const QString &jmiElementId)
+{
+ // Old session will be finished with reason "Expired".
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Expired);
+ reason.setText(QStringLiteral("Session migrated"));
+
+ // Tell the old session to be finished.
+ Q_EMIT existingJmi->closed(Jmi::Finished { reason, jmiElementId });
+
+ existingJmi->finish(reason, jmiElementId).then(this, [this, existingJmi, jmiElementId](SendResult result) {
+ if (auto *error = std::get_if<QXmppError>(&result)) {
+ Q_EMIT existingJmi->closed(*error);
+ } else {
+ // Then, proceed (accept) the new proposal and set the JMI ID
+ // to the ID of the received JMI element.
+ existingJmi->setId(jmiElementId);
+ existingJmi->proceed().then(this, [existingJmi](SendResult result) {
+ if (auto *error = std::get_if<QXmppError>(&result)) {
+ Q_EMIT existingJmi->closed(*error);
+ } else {
+ // The session is now closed as it is finished.
+ existingJmi->setIsProceeded(true);
+ }
+ });
+ }
+ });
+
+ return true;
+}
+
+///
+/// \brief Tie break in propose state (no existing session) - two parties try calling each other
+/// at the same time, the proposal with the lower ID overrules the other one.
+///
+/// \param existingJmi Current JMI object
+/// \param jmiElementId Counterpart's JMI element ID
+///
+bool QXmppJingleMessageInitiationManager::handleNonExistingSession(const std::shared_ptr<Jmi> &existingJmi, const QString &jmiElementId, const QString &callPartnerResource)
+{
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Expired);
+ reason.setText(QStringLiteral("Tie-Break"));
+
+ if (QUuid::fromString(existingJmi->id()) < QUuid::fromString(jmiElementId)) {
+ // Jingle message initiator with lower ID rejects the other proposal.
+ existingJmi->setId(jmiElementId);
+ existingJmi->reject(std::move(reason), true).then(this, [existingJmi](auto result) {
+ if (auto *error = std::get_if<QXmppError>(&result)) {
+ Q_EMIT existingJmi->closed(*error);
+ }
+ });
+ } else {
+ // Jingle message initiator with higher ID retracts its proposal.
+ existingJmi->retract(std::move(reason), true).then(this, [this, existingJmi, jmiElementId, callPartnerResource](auto result) {
+ if (auto error = std::get_if<QXmppError>(&result)) {
+ Q_EMIT existingJmi->closed(*error);
+ } else {
+ // Afterwards, JMI ID is changed to lower ID.
+ existingJmi->setId(jmiElementId);
+
+ // Finally, the call is being accepted.
+ existingJmi->proceed().then(this, [existingJmi, jmiElementId, callPartnerResource](SendResult result) {
+ if (auto *error = std::get_if<QXmppError>(&result)) {
+ Q_EMIT existingJmi->closed(*error);
+ } else {
+ existingJmi->setIsProceeded(true);
+ Q_EMIT existingJmi->proceeded(jmiElementId, callPartnerResource);
+ }
+ });
+ }
+ });
+ }
+
+ return true;
+}
+
+///
+/// Adds a JMI object to the JMIs vector and sets the bare JID of the call partner in the JMI object.
+/// \param callPartnerJid bare JID of the call partner
+/// \return The newly created JMI
+///
+std::shared_ptr<QXmppJingleMessageInitiation> QXmppJingleMessageInitiationManager::addJmi(const QString &callPartnerJid)
+{
+ auto jmi { std::make_shared<QXmppJingleMessageInitiation>(this) };
+ jmi->setCallPartnerJid(callPartnerJid);
+ d->jmis.append(jmi);
+ return jmi;
+}
+
+///
+/// Returns the JMIs vector.
+///
+const QVector<std::shared_ptr<QXmppJingleMessageInitiation>> &QXmppJingleMessageInitiationManager::jmis() const
+{
+ return d->jmis;
+}
+
+///
+/// \fn QXmppJingleMessageInitiationManager::proposed(const std::shared_ptr<QXmppJingleMessageInitiation> &, const QString &, const QXmppJingleDescription &)
+///
+/// Emitted when a call has been proposed.
+///
+/// \param jmi Jingle Message Initiation object of proposed session
+/// \param id JMI element id
+/// \param description JMI element's description containing media type (i.e., audio, video)
+///
diff --git a/src/client/QXmppJingleMessageInitiationManager.h b/src/client/QXmppJingleMessageInitiationManager.h
new file mode 100644
index 00000000..cd5bb494
--- /dev/null
+++ b/src/client/QXmppJingleMessageInitiationManager.h
@@ -0,0 +1,126 @@
+// SPDX-FileCopyrightText: 2023 Tibor Csötönyi <work@taibsu.de>
+// SPDX-FileCopyrightText: 2023 Melvin Keskin <melvo@olomono.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef QXMPPJINGLEMESSAGEINITIATIONMANAGER_H
+#define QXMPPJINGLEMESSAGEINITIATIONMANAGER_H
+
+#include "QXmppClientExtension.h"
+#include "QXmppError.h"
+#include "QXmppJingleIq.h"
+#include "QXmppMessageHandler.h"
+#include "QXmppSendResult.h"
+#include "QXmppTask.h"
+
+class QXmppJingleMessageInitiationManager;
+class QXmppJingleMessageInitiationPrivate;
+class QXmppJingleMessageInitiationManagerPrivate;
+
+class QXMPP_EXPORT QXmppJingleMessageInitiation : public QObject
+{
+ Q_OBJECT
+public:
+ struct Rejected
+ {
+ std::optional<QXmppJingleReason> reason;
+ bool containsTieBreak;
+ };
+
+ struct Retracted
+ {
+ std::optional<QXmppJingleReason> reason;
+ bool containsTieBreak;
+ };
+
+ struct Finished
+ {
+ std::optional<QXmppJingleReason> reason;
+ QString migratedTo;
+ };
+
+ /// Variant of Rejected, Retracted, Finished or Error result types
+ using Result = std::variant<Rejected, Retracted, Finished, QXmppError>;
+
+ QXmppJingleMessageInitiation(QXmppJingleMessageInitiationManager *manager);
+ ~QXmppJingleMessageInitiation();
+
+ QXmppTask<QXmpp::SendResult> ring();
+ QXmppTask<QXmpp::SendResult> proceed();
+ QXmppTask<QXmpp::SendResult> reject(std::optional<QXmppJingleReason> reason, bool containsTieBreak = false);
+ QXmppTask<QXmpp::SendResult> retract(std::optional<QXmppJingleReason> reason, bool containsTieBreak = false);
+ QXmppTask<QXmpp::SendResult> finish(std::optional<QXmppJingleReason> reason, const QString &migratedTo = {});
+
+ Q_SIGNAL void ringing();
+ Q_SIGNAL void proceeded(const QString &id, const QString &callPartnerResource);
+ Q_SIGNAL void closed(const Result &result);
+
+private:
+ QString id() const;
+ void setId(const QString &id);
+ void setCallPartnerJid(const QString &callPartnerJid);
+ QString callPartnerJid() const;
+ bool isProceeded() const;
+ void setIsProceeded(bool isProceeded);
+
+ std::unique_ptr<QXmppJingleMessageInitiationPrivate> d;
+
+ friend class QXmppJingleMessageInitiationManager;
+ friend class tst_QXmppJingleMessageInitiationManager;
+};
+
+class QXMPP_EXPORT QXmppJingleMessageInitiationManager : public QXmppClientExtension, public QXmppMessageHandler
+{
+ Q_OBJECT
+public:
+ using ProposeResult = std::variant<std::shared_ptr<QXmppJingleMessageInitiation>, QXmppError>;
+
+ QXmppJingleMessageInitiationManager();
+ ~QXmppJingleMessageInitiationManager();
+
+ /// \cond
+ QStringList discoveryFeatures() const override;
+ /// \endcond
+
+ QXmppTask<ProposeResult> propose(
+ const QString &callPartnerJid,
+ const QXmppJingleDescription &description);
+
+ Q_SIGNAL void proposed(
+ const std::shared_ptr<QXmppJingleMessageInitiation> &jmi,
+ const QString &id,
+ const std::optional<QXmppJingleDescription> &description);
+
+protected:
+ /// \cond
+ bool handleMessage(const QXmppMessage &) override;
+ void setClient(QXmppClient *client) override;
+ /// \endcond
+
+private:
+ QXmppTask<QXmpp::SendResult> sendMessage(
+ const QXmppJingleMessageInitiationElement &jmiElement,
+ const QString &callPartnerJid);
+
+ void clear(const std::shared_ptr<QXmppJingleMessageInitiation> &jmi);
+ void clearAll();
+
+ bool handleJmiElement(QXmppJingleMessageInitiationElement &&jmiElement, const QString &senderJid);
+ bool handleExistingJmi(const std::shared_ptr<QXmppJingleMessageInitiation> &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource);
+ bool handleProposeJmiElement(const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerJid);
+ bool handleTieBreak(const std::shared_ptr<QXmppJingleMessageInitiation> &existingJmi, const QXmppJingleMessageInitiationElement &jmiElement, const QString &callPartnerResource);
+ bool handleExistingSession(const std::shared_ptr<QXmppJingleMessageInitiation> &existingJmi, const QString &jmiElementId);
+ bool handleNonExistingSession(const std::shared_ptr<QXmppJingleMessageInitiation> &existingJmi, const QString &jmiElementId, const QString &callPartnerResource);
+ std::shared_ptr<QXmppJingleMessageInitiation> addJmi(const QString &callPartnerJid);
+ const QVector<std::shared_ptr<QXmppJingleMessageInitiation>> &jmis() const;
+
+private:
+ std::unique_ptr<QXmppJingleMessageInitiationManagerPrivate> d;
+
+ friend class QXmppJingleMessageInitiationPrivate;
+ friend class tst_QXmppJingleMessageInitiationManager;
+};
+
+Q_DECLARE_METATYPE(QXmppJingleMessageInitiation::Result)
+
+#endif // QXMPPJINGLEMESSAGEINITIATIONMANAGER_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index ce2ca155..ee5064be 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -42,6 +42,7 @@ add_simple_test(qxmpphttpuploadiq)
add_simple_test(qxmppiceconnection)
add_simple_test(qxmppiq)
add_simple_test(qxmppjingleiq)
+add_simple_test(qxmppjinglemessageinitiationmanager)
add_simple_test(qxmppmammanager)
add_simple_test(qxmppmixinvitation)
add_simple_test(qxmppmixitems)
diff --git a/tests/qxmppjinglemessageinitiationmanager/tst_qxmppjinglemessageinitiationmanager.cpp b/tests/qxmppjinglemessageinitiationmanager/tst_qxmppjinglemessageinitiationmanager.cpp
new file mode 100644
index 00000000..00b599aa
--- /dev/null
+++ b/tests/qxmppjinglemessageinitiationmanager/tst_qxmppjinglemessageinitiationmanager.cpp
@@ -0,0 +1,924 @@
+// SPDX-FileCopyrightText: 2023 Tibor Csötönyi <work@taibsu.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppClient.h"
+#include "QXmppConstants_p.h"
+#include "QXmppJingleMessageInitiationManager.h"
+#include "QXmppMessage.h"
+#include "QXmppUtils.h"
+
+#include "IntegrationTesting.h"
+#include "util.h"
+#include <QTest>
+
+using Jmi = QXmppJingleMessageInitiation;
+using JmiType = QXmppJingleMessageInitiationElement::Type;
+using Result = QXmppJingleMessageInitiation::Result;
+
+class tst_QXmppJingleMessageInitiationManager : public QObject
+{
+ Q_OBJECT
+
+private:
+ Q_SLOT void initTestCase();
+
+ Q_SLOT void testClear();
+ Q_SLOT void testClearAll();
+
+ Q_SLOT void testRing();
+ Q_SLOT void testProceed();
+ Q_SLOT void testReject();
+ Q_SLOT void testRetract();
+ Q_SLOT void testFinish();
+
+ Q_SLOT void testPropose();
+ Q_SLOT void testSendMessage();
+ Q_SLOT void testHandleNonExistingSessionLowerId();
+ Q_SLOT void testHandleNonExistingSessionHigherId();
+ Q_SLOT void testHandleExistingSession();
+ Q_SLOT void testHandleTieBreak();
+ Q_SLOT void testHandleProposeJmiElement();
+ Q_SLOT void testHandleExistingJmi();
+ Q_SLOT void testHandleJmiElement();
+ Q_SLOT void testHandleMessage_data();
+ Q_SLOT void testHandleMessage();
+ Q_SLOT void testHandleMessageRinging();
+ Q_SLOT void testHandleMessageProceeded();
+ Q_SLOT void testHandleMessageClosedRejected();
+ Q_SLOT void testHandleMessageClosedRetracted();
+ Q_SLOT void testHandleMessageClosedFinished();
+
+ QXmppClient m_client;
+ QXmppLogger m_logger;
+ QXmppJingleMessageInitiationManager m_manager;
+};
+
+void tst_QXmppJingleMessageInitiationManager::initTestCase()
+{
+ m_client.addExtension(&m_manager);
+
+ m_logger.setLoggingType(QXmppLogger::SignalLogging);
+ m_client.setLogger(&m_logger);
+
+ m_client.connectToServer(IntegrationTests::clientConfiguration());
+
+ qRegisterMetaType<QXmppJingleMessageInitiation::Result>();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testClear()
+{
+ QCOMPARE(m_manager.jmis().size(), 0);
+ auto jmi1 { m_manager.addJmi("test1") };
+ auto jmi2 { m_manager.addJmi("test2") };
+ QCOMPARE(m_manager.jmis().size(), 2);
+
+ m_manager.clear(jmi1);
+ m_manager.clear(jmi2);
+ QCOMPARE(m_manager.jmis().size(), 0);
+}
+
+void tst_QXmppJingleMessageInitiationManager::testClearAll()
+{
+ QCOMPARE(m_manager.jmis().size(), 0);
+ m_manager.addJmi("test1");
+ m_manager.addJmi("test2");
+ m_manager.addJmi("test3");
+ m_manager.addJmi("test4");
+ m_manager.addJmi("test5");
+ QCOMPARE(m_manager.jmis().size(), 5);
+
+ m_manager.clearAll();
+ QCOMPARE(m_manager.jmis().size(), 0);
+}
+
+void tst_QXmppJingleMessageInitiationManager::testRing()
+{
+ auto jmi { m_manager.addJmi("julietRing@capulet.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ connect(&m_logger, &QXmppLogger::message, this, [jmicallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jmicallPartnerJid) {
+ QVERIFY(message.jingleMessageInitiationElement());
+ QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Ringing);
+ }
+ }
+ });
+
+ auto future = jmi->ring();
+
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+
+ QVERIFY(future.isFinished());
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testProceed()
+{
+ auto jmi { m_manager.addJmi("julietProceed@capulet.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ connect(&m_logger, &QXmppLogger::message, this, [jmiCallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jmiCallPartnerJid) {
+ QVERIFY(message.jingleMessageInitiationElement());
+ QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Proceed);
+ }
+ }
+ });
+
+ auto future = jmi->proceed();
+
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+
+ QVERIFY(future.isFinished());
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testReject()
+{
+ auto jmi { m_manager.addJmi("julietReject@capulet.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Decline);
+ reason.setText("Declined");
+ reason.setNamespaceUri(ns_jingle);
+
+ connect(&m_logger, &QXmppLogger::message, this, [jmiCallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jmiCallPartnerJid) {
+ QVERIFY(message.jingleMessageInitiationElement());
+ QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Reject);
+ QCOMPARE(message.jingleMessageInitiationElement()->reason()->type(), QXmppJingleReason::Decline);
+ QCOMPARE(message.jingleMessageInitiationElement()->reason()->text(), "Declined");
+ QCOMPARE(message.jingleMessageInitiationElement()->reason()->namespaceUri(), ns_jingle);
+ QCOMPARE(message.jingleMessageInitiationElement()->containsTieBreak(), true);
+ }
+ }
+ });
+
+ auto future = jmi->reject(reason, true);
+
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+
+ QVERIFY(future.isFinished());
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testRetract()
+{
+ auto jmi { m_manager.addJmi("julietRetract@capulet.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Gone);
+ reason.setText("Gone");
+ reason.setNamespaceUri(ns_jingle);
+
+ connect(&m_logger, &QXmppLogger::message, this, [jmicallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jmicallPartnerJid) {
+ QVERIFY(message.jingleMessageInitiationElement());
+ QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Retract);
+ QCOMPARE(message.jingleMessageInitiationElement()->reason()->type(), QXmppJingleReason::Gone);
+ QCOMPARE(message.jingleMessageInitiationElement()->reason()->text(), "Gone");
+ QCOMPARE(message.jingleMessageInitiationElement()->reason()->namespaceUri(), ns_jingle);
+ QCOMPARE(message.jingleMessageInitiationElement()->containsTieBreak(), true);
+ }
+ }
+ });
+
+ auto future = jmi->retract(reason, true);
+
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+
+ QVERIFY(future.isFinished());
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testFinish()
+{
+ auto jmi { m_manager.addJmi("julietFinish@capulet.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Success);
+ reason.setText("Finished");
+ reason.setNamespaceUri(ns_jingle);
+
+ connect(&m_logger, &QXmppLogger::message, this, [jmicallPartnerJid = jmi->callPartnerJid()](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jmicallPartnerJid) {
+ QVERIFY(message.jingleMessageInitiationElement());
+ QCOMPARE(message.jingleMessageInitiationElement()->type(), JmiType::Finish);
+ QCOMPARE(message.jingleMessageInitiationElement()->reason()->type(), QXmppJingleReason::Success);
+ QCOMPARE(message.jingleMessageInitiationElement()->reason()->text(), "Finished");
+ QCOMPARE(message.jingleMessageInitiationElement()->reason()->namespaceUri(), ns_jingle);
+ QCOMPARE(message.jingleMessageInitiationElement()->migratedTo(), "fecbea35-08d3-404f-9ec7-2b57c566fa74");
+ }
+ }
+ });
+
+ auto future = jmi->finish(reason, "fecbea35-08d3-404f-9ec7-2b57c566fa74");
+
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+
+ QVERIFY(future.isFinished());
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testPropose()
+{
+ QString jid { "julietPropose@capulet.example" };
+
+ QXmppJingleDescription description;
+ description.setMedia(QStringLiteral("audio"));
+ description.setSsrc(123);
+ description.setType(ns_jingle_rtp);
+
+ connect(&m_logger, &QXmppLogger::message, this, [&, jid, description](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jid) {
+ const auto &jmiElement { message.jingleMessageInitiationElement() };
+ QVERIFY(jmiElement);
+
+ QCOMPARE(jmiElement->type(), JmiType::Propose);
+ QVERIFY(!jmiElement->id().isEmpty());
+ QVERIFY(jmiElement->description());
+ QCOMPARE(jmiElement->description()->media(), description.media());
+ QCOMPARE(jmiElement->description()->ssrc(), description.ssrc());
+ QCOMPARE(jmiElement->description()->type(), description.type());
+
+ SKIP_IF_INTEGRATION_TESTS_DISABLED()
+
+ // verify that the JMI ID has been changed and the JMI was processed
+ QCOMPARE(m_manager.jmis().size(), 1);
+ }
+ }
+ });
+
+ auto future = m_manager.propose(jid, description);
+
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+
+ QVERIFY(future.isFinished());
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testSendMessage()
+{
+ QString jid { "julietSendMessage@capulet.example" };
+
+ QXmppJingleMessageInitiationElement jmiElement;
+ jmiElement.setType(JmiType::Propose);
+ jmiElement.setId(QStringLiteral("fecbea35-08d3-404f-9ec7-2b57c566fa74"));
+
+ connect(&m_logger, &QXmppLogger::message, this, [jid, jmiElement](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jid) {
+ QVERIFY(message.hasHint(QXmppMessage::Store));
+ QVERIFY(message.jingleMessageInitiationElement());
+ QCOMPARE(message.jingleMessageInitiationElement()->type(), jmiElement.type());
+ }
+ }
+ });
+
+ auto future = m_manager.sendMessage(jmiElement, jid);
+
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+
+ QVERIFY(future.isFinished());
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleNonExistingSessionLowerId()
+{
+ // --- request with lower id sends propose to request with higher id ---
+
+ QByteArray xmlProposeLowId {
+ "<message from='romeoNonExistingSession@montague.example/low' to='juliet@capulet.example' type='chat'>"
+ "<propose xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>"
+ "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>"
+ "</propose>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "</message>"
+ };
+
+ auto jmiWithHigherId { m_manager.addJmi("romeoNonExistingSession@montague.example") };
+ jmiWithHigherId->setId("fecbea35-08d3-404f-9ec7-2b57c566fa74");
+
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Expired);
+ reason.setText("Tie-Break");
+ reason.setNamespaceUri(ns_jingle);
+
+ // make sure that request with higher ID is being retracted
+ connect(&m_logger, &QXmppLogger::message, this, [jmiWithHigherId, reason](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jmiWithHigherId->callPartnerJid()) {
+ const auto &jmiElement { message.jingleMessageInitiationElement() };
+ QVERIFY(jmiElement);
+
+ QCOMPARE(jmiElement->type(), JmiType::Retract);
+ QCOMPARE(jmiElement->id(), "fecbea35-08d3-404f-9ec7-2b57c566fa74");
+ QVERIFY(jmiElement->reason());
+ QCOMPARE(jmiElement->reason()->type(), reason.type());
+ QCOMPARE(jmiElement->reason()->text(), reason.text());
+ QCOMPARE(jmiElement->reason()->namespaceUri(), reason.namespaceUri());
+
+ SKIP_IF_INTEGRATION_TESTS_DISABLED()
+
+ // verify that the JMI ID has been changed and the JMI was processed
+ QCOMPARE(jmiWithHigherId->id(), "ca3cf894-5325-482f-a412-a6e9f832298d");
+ QVERIFY(jmiWithHigherId->isProceeded());
+ }
+ }
+ });
+
+ QXmppMessage message;
+ message.parse(xmlToDom(xmlProposeLowId));
+
+ QVERIFY(m_manager.handleMessage(message));
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleNonExistingSessionHigherId()
+{
+ // --- request with higher id sends propose to request with lower id ---
+ QByteArray xmlProposeHighId {
+ "<message from='julietNonExistingSession@capulet.example/high' to='romeo@montague.example' type='chat'>"
+ "<propose xmlns='urn:xmpp:jingle-message:0' id='fecbea35-08d3-404f-9ec7-2b57c566fa74'>"
+ "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>"
+ "</propose>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "</message>"
+ };
+
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Expired);
+ reason.setText("Tie-Break");
+ reason.setNamespaceUri(ns_jingle);
+
+ auto jmiWithLowerId { m_manager.addJmi("julietNonExistingSession@capulet.example") };
+ jmiWithLowerId->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ // make sure that request with lower id rejects request with higher id
+ connect(&m_logger, &QXmppLogger::message, this, [jid = jmiWithLowerId->callPartnerJid(), reason](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jid) {
+ const auto &jmiElement { message.jingleMessageInitiationElement() };
+ QVERIFY(jmiElement);
+
+ QCOMPARE(jmiElement->type(), JmiType::Reject);
+ QCOMPARE(jmiElement->id(), "fecbea35-08d3-404f-9ec7-2b57c566fa74");
+ QVERIFY(jmiElement->reason());
+ QCOMPARE(jmiElement->reason()->type(), reason.type());
+ QCOMPARE(jmiElement->reason()->text(), reason.text());
+ QCOMPARE(jmiElement->reason()->namespaceUri(), reason.namespaceUri());
+ }
+ }
+ });
+
+ QXmppMessage message;
+ message.parse(xmlToDom(xmlProposeHighId));
+
+ QVERIFY(m_manager.handleMessage(message));
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleExistingSession()
+{
+ QXmppMessage message;
+
+ QByteArray xmlPropose {
+ "<message from='julietExistingSession@capulet.example/tablet' to='romeo@montague.example' type='chat'>"
+ "<propose xmlns='urn:xmpp:jingle-message:0' id='989a46a6-f202-4910-a7c3-83c6ba3f3947'>"
+ "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>"
+ "</propose>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "</message>"
+ };
+
+ auto jmi { m_manager.addJmi("julietExistingSession@capulet.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+ jmi->setIsProceeded(true);
+
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Expired);
+ reason.setText("Session migrated");
+ reason.setNamespaceUri(ns_jingle);
+
+ connect(&m_logger, &QXmppLogger::message, this, [jmi, reason](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (message.to() == jmi->callPartnerJid()) {
+ const auto &jmiElement { message.jingleMessageInitiationElement() };
+ QVERIFY(jmiElement);
+
+ QCOMPARE(jmiElement->type(), JmiType::Finish);
+ QCOMPARE(jmiElement->id(), jmi->id());
+ QCOMPARE(jmiElement->migratedTo(), "989a46a6-f202-4910-a7c3-83c6ba3f3947");
+ QVERIFY(jmiElement->reason());
+ QCOMPARE(jmiElement->reason()->type(), reason.type());
+ QCOMPARE(jmiElement->reason()->text(), reason.text());
+ QCOMPARE(jmiElement->reason()->namespaceUri(), reason.namespaceUri());
+ }
+ }
+ });
+
+ message.parse(xmlToDom(xmlPropose));
+
+ QVERIFY(m_manager.handleMessage(message));
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleTieBreak()
+{
+ QString callPartnerJid { "romeoHandleTieBreakExistingSession@montague.example/orchard" };
+ QString jmiId { "ca3cf894-5325-482f-a412-a6e9f832298d" };
+ auto jmi { m_manager.addJmi(QXmppUtils::jidToBareJid(callPartnerJid)) };
+ jmi->setId(jmiId);
+
+ QXmppJingleMessageInitiationElement jmiElement;
+ QString newJmiId("989a46a6-f202-4910-a7c3-83c6ba3f3947");
+ jmiElement.setId(newJmiId);
+
+ // Cannot use macro SKIP_IF_INTEGRATION_TESTS_DISABLED() here since
+ // this would also skip the manager cleanup.
+ if (IntegrationTests::enabled()) {
+ // --- ensure handleExistingSession ---
+ jmi->setIsProceeded(true);
+ QSignalSpy closedSpy(jmi.get(), &QXmppJingleMessageInitiation::closed);
+ QVERIFY(m_manager.handleTieBreak(jmi, jmiElement, QXmppUtils::jidToResource(callPartnerJid)));
+ QCOMPARE(closedSpy.count(), 1);
+
+ // --- ensure handleNonExistingSession ---
+ jmi->setIsProceeded(false);
+ QSignalSpy proceededSpy(jmi.get(), &QXmppJingleMessageInitiation::proceeded);
+ QVERIFY(m_manager.handleTieBreak(jmi, jmiElement, QXmppUtils::jidToResource(callPartnerJid)));
+ QCOMPARE(proceededSpy.count(), 1);
+ }
+
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleProposeJmiElement()
+{
+ QXmppJingleMessageInitiationElement jmiElement;
+
+ QXmppJingleDescription description;
+ description.setMedia("audio");
+ description.setSsrc(321);
+ description.setType("abcd");
+
+ jmiElement.setId("ca3cf123-5325-482f-a412-a6e9f832298d");
+ jmiElement.setDescription(description);
+
+ QString callPartnerJid { "juliet@capulet.example" };
+
+ // --- Tie break ---
+
+ auto jmi { m_manager.addJmi(callPartnerJid) };
+ jmi->setId("989a4123-f202-4910-a7c3-83c6ba3f3947");
+
+ QVERIFY(m_manager.handleProposeJmiElement(jmiElement, callPartnerJid));
+ QCOMPARE(m_manager.jmis().size(), 1);
+ m_manager.clearAll();
+
+ // --- usual JMI proposal ---
+
+ connect(&m_manager, &QXmppJingleMessageInitiationManager::proposed, this, [&, jmiElement](const std::shared_ptr<Jmi> &, const QString &jmiElementId, const std::optional<QXmppJingleDescription> &description) {
+ if (jmiElement.id() == jmiElementId) {
+ QCOMPARE(m_manager.jmis().size(), 1);
+ QVERIFY(description.has_value());
+ QCOMPARE(description->media(), jmiElement.description()->media());
+ QCOMPARE(description->ssrc(), jmiElement.description()->ssrc());
+ QCOMPARE(description->type(), jmiElement.description()->type());
+ }
+ });
+
+ callPartnerJid = "romeoHandleProposeJmiElement@montague.example";
+
+ QVERIFY(m_manager.handleProposeJmiElement(jmiElement, callPartnerJid));
+ QCOMPARE(m_manager.jmis().size(), 1);
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleExistingJmi()
+{
+ QString callPartnerJid { "juliet@capulet.example" };
+ QString jmiId { "989a46a6-f202-4910-a7c3-83c6ba3f3947" };
+
+ auto jmi { m_manager.addJmi(callPartnerJid) };
+ jmi->setId(jmiId);
+
+ QXmppJingleMessageInitiationElement jmiElement;
+ jmiElement.setId(jmiId);
+
+ // --- ringing ---
+
+ QSignalSpy ringingSpy(jmi.get(), &QXmppJingleMessageInitiation::ringing);
+
+ jmiElement.setType(JmiType::Ringing);
+
+ QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid));
+ QCOMPARE(ringingSpy.count(), 1);
+ m_manager.clearAll();
+
+ // --- proceeded ---
+
+ jmi = m_manager.addJmi(callPartnerJid);
+ jmi->setId(jmiId);
+
+ jmiElement.setType(JmiType::Proceed);
+ connect(jmi.get(), &QXmppJingleMessageInitiation::proceeded, this, [jmiElement](const QString &jmiElementId) {
+ if (jmiElementId == jmiElement.id()) {
+ QVERIFY(true);
+ }
+ });
+
+ QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid));
+ m_manager.clearAll();
+
+ // --- closed: rejected ---
+
+ jmi = m_manager.addJmi(callPartnerJid);
+ jmi->setId(jmiId);
+
+ QXmppJingleReason reason;
+ reason.setType(QXmppJingleReason::Expired);
+ reason.setText("Rejected because expired.");
+ reason.setNamespaceUri(ns_jingle);
+
+ jmiElement.setType(JmiType::Reject);
+ jmiElement.setReason(reason);
+
+ connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [jmiElement](const Result &result) {
+ using ResultType = QXmppJingleMessageInitiation::Rejected;
+
+ QVERIFY(std::holds_alternative<ResultType>(result));
+ const ResultType &rejectedJmiElement { std::get<ResultType>(result) };
+
+ QVERIFY(rejectedJmiElement.reason);
+ QCOMPARE(rejectedJmiElement.reason->type(), jmiElement.reason()->type());
+ QCOMPARE(rejectedJmiElement.reason->text(), jmiElement.reason()->text());
+ QCOMPARE(rejectedJmiElement.reason->namespaceUri(), jmiElement.reason()->namespaceUri());
+ QCOMPARE(rejectedJmiElement.containsTieBreak, jmiElement.containsTieBreak());
+ });
+
+ QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid));
+ m_manager.clearAll();
+
+ // --- closed: retracted ---
+
+ jmi = m_manager.addJmi(callPartnerJid);
+ jmi->setId(jmiId);
+
+ reason.setType(QXmppJingleReason::ConnectivityError);
+ reason.setText("Retracted due to connectivity error.");
+ reason.setNamespaceUri(ns_jingle);
+
+ jmiElement.setType(JmiType::Retract);
+ jmiElement.setReason(reason);
+
+ connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [jmiElement](const Result &result) {
+ using ResultType = QXmppJingleMessageInitiation::Retracted;
+
+ QVERIFY(std::holds_alternative<ResultType>(result));
+ const ResultType &rejectedJmiElement { std::get<ResultType>(result) };
+
+ QVERIFY(rejectedJmiElement.reason);
+ QCOMPARE(rejectedJmiElement.reason->type(), jmiElement.reason()->type());
+ QCOMPARE(rejectedJmiElement.reason->text(), jmiElement.reason()->text());
+ QCOMPARE(rejectedJmiElement.reason->namespaceUri(), jmiElement.reason()->namespaceUri());
+ QCOMPARE(rejectedJmiElement.containsTieBreak, jmiElement.containsTieBreak());
+ });
+
+ QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid));
+ m_manager.clearAll();
+
+ // --- closed: finished ---
+
+ jmi = m_manager.addJmi(callPartnerJid);
+ jmi->setId(jmiId);
+
+ reason.setType(QXmppJingleReason::Success);
+ reason.setText("Finished.");
+ reason.setNamespaceUri(ns_jingle);
+
+ jmiElement.setType(JmiType::Finish);
+ jmiElement.setReason(reason);
+ jmiElement.setMigratedTo("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [jmiElement](const Result &result) {
+ using ResultType = QXmppJingleMessageInitiation::Finished;
+
+ QVERIFY(std::holds_alternative<ResultType>(result));
+ const ResultType &rejectedJmiElement { std::get<ResultType>(result) };
+
+ QVERIFY(rejectedJmiElement.reason);
+ QCOMPARE(rejectedJmiElement.reason->type(), jmiElement.reason()->type());
+ QCOMPARE(rejectedJmiElement.reason->text(), jmiElement.reason()->text());
+ QCOMPARE(rejectedJmiElement.reason->namespaceUri(), jmiElement.reason()->namespaceUri());
+ QCOMPARE(rejectedJmiElement.migratedTo, jmiElement.migratedTo());
+ });
+
+ QVERIFY(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid));
+ m_manager.clearAll();
+
+ // --- none ---
+
+ jmi = m_manager.addJmi(callPartnerJid);
+ jmi->setId(jmiId);
+
+ jmiElement.setType(JmiType::None);
+
+ QCOMPARE(m_manager.handleExistingJmi(jmi, jmiElement, callPartnerJid), false);
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleJmiElement()
+{
+ QString callPartnerJid { "romeoHandleJmiElement@montague.example/orchard" };
+ QString jmiId { "ca3cf894-5325-482f-a412-a6e9f832298d" };
+
+ // case 1: no JMI found in JMIs vector and jmiElement is not a propose element
+ QXmppJingleMessageInitiationElement jmiElement;
+ jmiElement.setType(JmiType::None);
+
+ QCOMPARE(m_manager.handleJmiElement(std::move(jmiElement), {}), false);
+
+ // case 2: no JMI found in JMIs vector and jmiElement is a propose element
+ jmiElement = {};
+ jmiElement.setType(JmiType::Propose);
+ jmiElement.setId(jmiId);
+
+ QSignalSpy proposedSpy(&m_manager, &QXmppJingleMessageInitiationManager::proposed);
+ QVERIFY(m_manager.handleJmiElement(std::move(jmiElement), callPartnerJid));
+ QCOMPARE(proposedSpy.count(), 1);
+ m_manager.clearAll();
+
+ // case 3: JMI found in JMIs vector, existing session
+ jmiElement = {};
+ jmiElement.setType(JmiType::Ringing);
+ jmiElement.setId(jmiId);
+ auto jmi { m_manager.addJmi(QXmppUtils::jidToBareJid(callPartnerJid)) };
+ jmi->setId(jmiId);
+
+ QSignalSpy ringingSpy(jmi.get(), &QXmppJingleMessageInitiation::ringing);
+ QVERIFY(m_manager.handleJmiElement(std::move(jmiElement), callPartnerJid));
+ QCOMPARE(ringingSpy.count(), 1);
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleMessage_data()
+{
+ QTest::addColumn<QByteArray>("xml");
+ QTest::addColumn<bool>("isValid");
+
+ QTest::newRow("xmlValid")
+ << QByteArray(
+ "<message to='julietHandleMessageValid@capulet.example' from='romeoHandleMessageValid@montague.example/orchard' type='chat'>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "<propose xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>"
+ "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>"
+ "</propose>"
+ "</message>")
+ << true;
+
+ QTest::newRow("xmlInvalidTypeNotChat")
+ << QByteArray(
+ "<message to='julietHandleMessageNoChat@capulet.example' from='romeoHandleMessageNoChat@montague.example/orchard' type='normal'>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "<propose xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>"
+ "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>"
+ "</propose>"
+ "</message>")
+ << false;
+
+ QTest::newRow("xmlInvalidNoStore")
+ << QByteArray(
+ "<message to='julietHandleMessageNoStore@capulet.example' from='romeoHandleMessageNoStore@montague.example/orchard' type='chat'>"
+ "<propose xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>"
+ "<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'/>"
+ "</propose>"
+ "</message>")
+ << false;
+
+ QTest::newRow("xmlInvalidNoJmiElement")
+ << QByteArray("<message to='julietHandleMessageNoJmi@capulet.example' from='romeoHandleMessageNoJmi@montague.example/orchard' type='chat'/>")
+ << false;
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleMessage()
+{
+ QFETCH(QByteArray, xml);
+ QFETCH(bool, isValid);
+
+ QXmppMessage message;
+
+ parsePacket(message, xml);
+ QCOMPARE(m_manager.handleMessage(message), isValid);
+ serializePacket(message, xml);
+
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleMessageRinging()
+{
+ QXmppMessage message;
+ QByteArray xmlRinging {
+ "<message from='juliet@capulet.example/phone' to='romeo@montague.example' type='chat'>"
+ "<ringing xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'/>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "</message>"
+ };
+
+ auto jmi { m_manager.addJmi("juliet@capulet.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ QSignalSpy ringingSpy(jmi.get(), &QXmppJingleMessageInitiation::ringing);
+
+ message.parse(xmlToDom(xmlRinging));
+
+ QVERIFY(m_manager.handleMessage(message));
+ QCOMPARE(ringingSpy.count(), 1);
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleMessageProceeded()
+{
+ QXmppMessage message;
+ QByteArray xmlProceed {
+ "<message from='juliet@capulet.example/phone' to='romeo@montague.example' type='chat'>"
+ "<proceed xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'/>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "</message>"
+ };
+
+ auto jmi { m_manager.addJmi("juliet@capulet.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ QSignalSpy proceededSpy(jmi.get(), &QXmppJingleMessageInitiation::proceeded);
+
+ message.parse(xmlToDom(xmlProceed));
+
+ QVERIFY(m_manager.handleMessage(message));
+ QCOMPARE(proceededSpy.count(), 1);
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleMessageClosedRejected()
+{
+ QXmppMessage message;
+ QByteArray xmlReject {
+ "<message from='juliet@capulet.example/phone' to='romeo@montague.example' type='chat'>"
+ "<reject xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>"
+ "<reason xmlns=\"urn:xmpp:jingle:1\">"
+ "<busy/>"
+ "<text>Busy</text>"
+ "</reason>"
+ "</reject>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "</message>"
+ };
+
+ auto jmi { m_manager.addJmi("juliet@capulet.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [](const Result &result) {
+ using ResultType = QXmppJingleMessageInitiation::Rejected;
+
+ QVERIFY(std::holds_alternative<ResultType>(result));
+ const ResultType &rejectedJmiElement { std::get<ResultType>(result) };
+
+ QCOMPARE(rejectedJmiElement.reason->type(), QXmppJingleReason::Busy);
+ QCOMPARE(rejectedJmiElement.reason->text(), "Busy");
+ QCOMPARE(rejectedJmiElement.reason->namespaceUri(), ns_jingle);
+ });
+
+ message.parse(xmlToDom(xmlReject));
+
+ QVERIFY(m_manager.handleMessage(message));
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleMessageClosedRetracted()
+{
+ QXmppMessage message;
+ QByteArray xmlRetract {
+ "<message from='romeo@montague.example/orchard' to='juliet@capulet.example' type='chat'>"
+ "<retract xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>"
+ "<reason xmlns=\"urn:xmpp:jingle:1\">"
+ "<cancel/>"
+ "<text>Retracted</text>"
+ "</reason>"
+ "</retract>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "</message>"
+ };
+
+ auto jmi { m_manager.addJmi("romeo@montague.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [](const Result &result) {
+ using ResultType = QXmppJingleMessageInitiation::Retracted;
+
+ QVERIFY(std::holds_alternative<ResultType>(result));
+ const ResultType &retractedJmiElement { std::get<ResultType>(result) };
+
+ QCOMPARE(retractedJmiElement.reason->type(), QXmppJingleReason::Cancel);
+ QCOMPARE(retractedJmiElement.reason->text(), "Retracted");
+ QCOMPARE(retractedJmiElement.reason->namespaceUri(), ns_jingle);
+ });
+
+ message.parse(xmlToDom(xmlRetract));
+
+ QVERIFY(m_manager.handleMessage(message));
+ m_manager.clearAll();
+}
+
+void tst_QXmppJingleMessageInitiationManager::testHandleMessageClosedFinished()
+{
+ QXmppMessage message;
+ QByteArray xmlFinish {
+ "<message from='romeo@montague.example/orchard' to='juliet@capulet.example' type='chat'>"
+ "<finish xmlns='urn:xmpp:jingle-message:0' id='ca3cf894-5325-482f-a412-a6e9f832298d'>"
+ "<reason xmlns=\"urn:xmpp:jingle:1\">"
+ "<success/>"
+ "<text>Success</text>"
+ "</reason>"
+ "<migrated to='989a46a6-f202-4910-a7c3-83c6ba3f3947'/>"
+ "</finish>"
+ "<store xmlns=\"urn:xmpp:hints\"/>"
+ "</message>"
+ };
+
+ auto jmi { m_manager.addJmi("romeo@montague.example") };
+ jmi->setId("ca3cf894-5325-482f-a412-a6e9f832298d");
+
+ connect(jmi.get(), &QXmppJingleMessageInitiation::closed, this, [](const Result &result) {
+ using ResultType = QXmppJingleMessageInitiation::Finished;
+
+ QVERIFY(std::holds_alternative<ResultType>(result));
+ const ResultType &finishedJmiElement { std::get<ResultType>(result) };
+
+ QCOMPARE(finishedJmiElement.reason->type(), QXmppJingleReason::Success);
+ QCOMPARE(finishedJmiElement.reason->text(), "Success");
+ QCOMPARE(finishedJmiElement.reason->namespaceUri(), ns_jingle);
+ QCOMPARE(finishedJmiElement.migratedTo, "989a46a6-f202-4910-a7c3-83c6ba3f3947");
+ });
+
+ message.parse(xmlToDom(xmlFinish));
+
+ QVERIFY(m_manager.handleMessage(message));
+ m_manager.clearAll();
+}
+
+QTEST_MAIN(tst_QXmppJingleMessageInitiationManager)
+#include "tst_qxmppjinglemessageinitiationmanager.moc"