From 8c174dad300b2645ec32a0f58e537883d515543b Mon Sep 17 00:00:00 2001 From: Melvin Keskin Date: Mon, 11 Sep 2023 21:11:43 +0200 Subject: [PATCH] WIP: Implement MIX TODO: Create PR that includes https://github.com/xsf/xeps/pull/919 and a version block --- doc/doap.xml | 19 +- src/CMakeLists.txt | 2 + src/base/QXmppConstants.cpp | 5 +- src/base/QXmppConstants_p.h | 3 + src/base/QXmppMixInfoItem.h | 4 + src/base/QXmppMixIq.cpp | 339 +++- src/base/QXmppMixIq.h | 63 +- src/base/QXmppMixItems.cpp | 23 +- src/base/QXmppMixParticipantItem.h | 2 + src/client/QXmppMixManager.cpp | 1451 ++++++++++++++ src/client/QXmppMixManager.h | 161 ++ tests/CMakeLists.txt | 1 + tests/TestClient.h | 2 + tests/qxmppmixiq/tst_qxmppmixiq.cpp | 228 ++- tests/qxmppmixitems/tst_qxmppmixitems.cpp | 3 + tests/qxmppmixmanager/tst_qxmppmixmanager.cpp | 1785 +++++++++++++++++ 16 files changed, 3977 insertions(+), 114 deletions(-) create mode 100644 src/client/QXmppMixManager.cpp create mode 100644 src/client/QXmppMixManager.h create mode 100644 tests/qxmppmixmanager/tst_qxmppmixmanager.cpp diff --git a/doc/doap.xml b/doc/doap.xml index 9660268ce..0a98c4776 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -540,10 +540,10 @@ SPDX-License-Identifier: CC0-1.0 - partial + complete 0.14 1.1 - Only IQ queries implemented + IQ stanzas for participants and channel information since 1.5; Manager since 1.6 @@ -573,10 +573,19 @@ SPDX-License-Identifier: CC0-1.0 - partial + complete 0.5 1.3 - Only IQ queries implemented + Manager since 1.6 + + + + + + partial + 0.3 + 1.6 + Channel configuration not implemented @@ -585,7 +594,7 @@ SPDX-License-Identifier: CC0-1.0 partial 0.1 1.4 - Only invitations implemented + Only invitations implemented; Manager since 1.6 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b1ab54e06..c3c7559dd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -115,6 +115,7 @@ set(INSTALL_HEADER_FILES client/QXmppMamManager.h client/QXmppMessageHandler.h client/QXmppMessageReceiptManager.h + client/QXmppMixManager.h client/QXmppMucManager.h client/QXmppOutgoingClient.h client/QXmppRegistrationManager.h @@ -252,6 +253,7 @@ set(SOURCE_FILES client/QXmppJingleMessageInitiationManager.cpp client/QXmppMamManager.cpp client/QXmppMessageReceiptManager.cpp + client/QXmppMixManager.cpp client/QXmppMucManager.cpp client/QXmppOutgoingClient.cpp client/QXmppRosterManager.cpp diff --git a/src/base/QXmppConstants.cpp b/src/base/QXmppConstants.cpp index 4e21df3c1..15b986c02 100644 --- a/src/base/QXmppConstants.cpp +++ b/src/base/QXmppConstants.cpp @@ -181,6 +181,8 @@ const char *ns_mix_node_participants = "urn:xmpp:mix:nodes:participants"; const char *ns_mix_node_presence = "urn:xmpp:mix:nodes:presence"; const char *ns_mix_node_config = "urn:xmpp:mix:nodes:config"; const char *ns_mix_node_info = "urn:xmpp:mix:nodes:info"; +const char *ns_mix_node_allowed = "urn:xmpp:mix:nodes:allowed"; +const char *ns_mix_node_banned = "urn:xmpp:mix:nodes:banned"; // XEP-0373: OpenPGP for XMPP const char *ns_ox = "urn:xmpp:openpgp:0"; // XEP-0380: Explicit Message Encryption @@ -194,7 +196,8 @@ const char *ns_omemo_2 = "urn:xmpp:omemo:2"; const char *ns_omemo_2_bundles = "urn:xmpp:omemo:2:bundles"; const char *ns_omemo_2_devices = "urn:xmpp:omemo:2:devices"; // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements -const char *ns_mix_pam = "urn:xmpp:mix:pam:1"; +const char *ns_mix_pam = "urn:xmpp:mix:pam:2"; +const char *ns_mix_pam_archiving = "urn:xmpp:mix:pam:2#archive"; const char *ns_mix_roster = "urn:xmpp:mix:roster:0"; const char *ns_mix_presence = "urn:xmpp:presence:0"; // XEP-0407: Mediated Information eXchange (MIX): Miscellaneous Capabilities diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h index 1b5ddf313..13a009e3d 100644 --- a/src/base/QXmppConstants_p.h +++ b/src/base/QXmppConstants_p.h @@ -193,6 +193,8 @@ extern const char *ns_mix_node_participants; extern const char *ns_mix_node_presence; extern const char *ns_mix_node_config; extern const char *ns_mix_node_info; +extern const char *ns_mix_node_allowed; +extern const char *ns_mix_node_banned; // XEP-0373: OpenPGP for XMPP extern const char *ns_ox; // XEP-0380: Explicit Message Encryption @@ -207,6 +209,7 @@ extern const char *ns_omemo_2_bundles; extern const char *ns_omemo_2_devices; // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements extern const char *ns_mix_pam; +extern const char *ns_mix_pam_archiving; extern const char *ns_mix_roster; extern const char *ns_mix_presence; // XEP-0407: Mediated Information eXchange (MIX): Miscellaneous Capabilities diff --git a/src/base/QXmppMixInfoItem.h b/src/base/QXmppMixInfoItem.h index 313c3e145..9e05c32e0 100644 --- a/src/base/QXmppMixInfoItem.h +++ b/src/base/QXmppMixInfoItem.h @@ -5,6 +5,7 @@ #ifndef QXMPPMIXINFOITEM_H #define QXMPPMIXINFOITEM_H +#include "QXmppDataForm.h" #include "QXmppPubSubBaseItem.h" class QXmppMixInfoItemPrivate; @@ -20,6 +21,9 @@ class QXMPP_EXPORT QXmppMixInfoItem : public QXmppPubSubBaseItem QXmppMixInfoItem &operator=(const QXmppMixInfoItem &); QXmppMixInfoItem &operator=(QXmppMixInfoItem &&); + const QXmppDataForm::Type formType() const; + void setFormType(QXmppDataForm::Type formType); + const QString &name() const; void setName(QString); diff --git a/src/base/QXmppMixIq.cpp b/src/base/QXmppMixIq.cpp index 6af9bd924..7ae1e66f7 100644 --- a/src/base/QXmppMixIq.cpp +++ b/src/base/QXmppMixIq.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2019 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin // // SPDX-License-Identifier: LGPL-2.1-or-later @@ -25,12 +26,24 @@ static const QStringList MIX_ACTION_TYPES = { QStringLiteral("destroy") }; +static const QMap NODES = { + { QXmppMixIq::Node::AllowedJids, ns_mix_node_allowed }, + { QXmppMixIq::Node::BannedJids, ns_mix_node_banned }, + { QXmppMixIq::Node::Configuration, ns_mix_node_config }, + { QXmppMixIq::Node::Information, ns_mix_node_info }, + { QXmppMixIq::Node::Messages, ns_mix_node_messages }, + { QXmppMixIq::Node::Participants, ns_mix_node_participants }, + { QXmppMixIq::Node::Presence, ns_mix_node_presence }, +}; + class QXmppMixIqPrivate : public QSharedData { public: - QString jid; - QString channelName; - QStringList nodes; + QString participantId; + QString channelId; + QString channelJid; + QXmppMixIq::Nodes nodesBeingSubscribedTo; + QXmppMixIq::Nodes nodesBeingUnsubscribedFrom; QString nick; QString inviteeJid; std::optional invitation; @@ -38,6 +51,7 @@ class QXmppMixIqPrivate : public QSharedData }; QXmppMixIq::QXmppMixIq() + // : QXmppIq(), d(new QXmppMixIqPrivate) : d(new QXmppMixIqPrivate) { } @@ -46,69 +60,253 @@ QXmppMixIq::QXmppMixIq() QXmppMixIq::QXmppMixIq(const QXmppMixIq &) = default; /// Default move-constructor QXmppMixIq::QXmppMixIq(QXmppMixIq &&) = default; +// QXmppMixIq::QXmppMixIq(const QXmppMixIq &other) = default; + QXmppMixIq::~QXmppMixIq() = default; /// Default assignment operator QXmppMixIq &QXmppMixIq::operator=(const QXmppMixIq &) = default; /// Default move-assignment operator QXmppMixIq &QXmppMixIq::operator=(QXmppMixIq &&) = default; +// QXmppMixIq& QXmppMixIq::operator=(const QXmppMixIq &other) = default; -/// Returns the channel JID. It also contains a participant id for Join/ -/// ClientJoin results. - +/// +/// Returns the channel JID, in case of a Join/ClientJoin query result, containing the participant +/// ID. +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use \c QXmppMixIq::channelJid() and +/// \c QXmppMixIq::participantId() instead. +/// QString QXmppMixIq::jid() const { - return d->jid; -} + if (d->participantId.isEmpty()) { + return d->channelJid; + } + + if (d->channelJid.isEmpty()) { + return {}; + } -/// Sets the channel JID. For results of Join/ClientJoin queries this also -/// needs to contain a participant id. + return d->participantId + "#" + d->channelJid; +} +/// +/// Sets the channel JID, in case of a Join/ClientJoin query result, containing the participant ID. +/// +/// \param jid channel JID including a possible participant ID +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use \c QXmppMixIq::setChannelJid() and +/// \c QXmppMixIq::setParticipantId() instead. +/// void QXmppMixIq::setJid(const QString &jid) { - d->jid = jid; + const auto jidParts = jid.split("#"); + + if (jidParts.size() == 1) { + d->channelJid = jid; + } else if (jidParts.size() == 2) { + d->participantId = jidParts.at(0); + d->channelJid = jidParts.at(1); + } +} + +/// +/// Returns the participant ID for a Join/ClientJoin result. +/// +/// \return the participant ID +/// +/// \since QXmpp 1.6 +/// +QString QXmppMixIq::participantId() const +{ + return d->participantId; } -/// Returns the channel name (the name part of the channel JID). This may still -/// be empty, if a JID was set. +/// +/// Sets the participant ID for a Join/ClientJoin result. +/// +/// @param participantId ID of the user in the channel +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setParticipantId(const QString &participantId) +{ + d->participantId = participantId; +} +/// +/// Returns the channel's ID (the local part of the channel JID). +/// +/// It can be empty if a JID was set. +/// +/// \return the ID of the channel +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use \c QXmppMixIq::channelId() instead. +/// QString QXmppMixIq::channelName() const { - return d->channelName; + return d->channelId; } -/// Sets the channel name for creating/destroying specific channels. When you -/// create a new channel, this can also be left empty to let the server -/// generate a name. - +/// +/// Sets the channel's ID (the local part of the channel JID) for creating or destroying a channel. +/// +/// If you create a new channel, the channel ID can be left empty to let the server generate an ID. +/// +/// \param channelName ID of the channel +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use \c QXmppMixIq::setChannelId() +/// instead. +/// void QXmppMixIq::setChannelName(const QString &channelName) { - d->channelName = channelName; + d->channelId = channelName; } -/// Returns the list of nodes to subscribe to. +/// +/// Returns the channel's ID (the local part of the channel JID). +/// +/// It can be empty if a JID was set. +/// +/// \return the ID of the channel +/// +/// \since QXmpp 1.6 +/// +QString QXmppMixIq::channelId() const +{ + return d->channelId; +} -QStringList QXmppMixIq::nodes() const +/// +/// Sets the channel's ID (the local part of the channel JID) for creating or destroying a channel. +/// +/// If you create a new channel, the channel ID can be left empty to let the server generate an ID. +/// +/// @param channelId channel ID to be set +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setChannelId(const QString &channelId) +{ + d->channelId = channelId; +} + +/// +/// Returns the channel's JID. +/// +/// \return the JID of the channel +/// +/// \since QXmpp 1.6 +/// +QString QXmppMixIq::channelJid() const +{ + return d->channelJid; +} + +/// +/// Sets the channel's JID. +/// +/// @param channelJid JID to be set +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setChannelJid(const QString &channelJid) { - return d->nodes; + d->channelJid = channelJid; } -/// Sets the nodes to subscribe to. Note that for UpdateSubscription queries -/// you only need to include the new subscriptions. +/// +/// Returns the nodes being subscribed to. +/// +/// \return the nodes being subscribed to +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use +/// \c QXmppMixIq::nodesBeingSubscribedTo() instead. +/// +QStringList QXmppMixIq::nodes() const +{ + return nodesToList(d->nodesBeingSubscribedTo).toList(); +} +/// +/// Sets the nodes being subscribe to. +/// +/// In case of an UpdateSubscription query, you only need to set new subscriptions. +/// +/// \param nodes nodes being subscribed to +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use +/// \c QXmppMixIq::setNodesBeingSubscribedTo() instead. +/// void QXmppMixIq::setNodes(const QStringList &nodes) { - d->nodes = nodes; + d->nodesBeingSubscribedTo = listToNodes(nodes.toVector()); } -/// Returns the user's nickname in the channel. +/// +/// Returns the nodes to subscribe to. +/// +/// \return the nodes being subscribed to +/// +/// \since QXmpp 1.6 +/// +QXmppMixIq::Nodes QXmppMixIq::nodesBeingSubscribedTo() const +{ + return d->nodesBeingSubscribedTo; +} + +/// +/// Sets the nodes to subscribe to. +/// +/// In case of an UpdateSubscription query, you only need to set new subscriptions. +/// +/// \param nodes nodes being subscribed to +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setNodesBeingSubscribedTo(Nodes nodes) +{ + d->nodesBeingSubscribedTo = nodes; +} + +/// +/// Returns the nodes to unsubscribe from. +/// +/// \return the nodes being unsubscribed from +/// +/// \since QXmpp 1.6 +/// +QXmppMixIq::Nodes QXmppMixIq::nodesBeingUnsubscribedFrom() const +{ + return d->nodesBeingUnsubscribedFrom; +} + +/// +/// Sets the nodes to unsubscribe from. +/// +/// \param nodes nodes being unsubscribed from +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setNodesBeingUnsubscribedFrom(Nodes nodes) +{ + d->nodesBeingUnsubscribedFrom = nodes; +} +/// +/// Returns the user's nickname in the channel. +/// +/// \return the nickname of the user +/// QString QXmppMixIq::nick() const { return d->nick; } -/// Sets the nickname for the channel. - +/// +/// Sets the user's nickname used for the channel. +/// +/// \param nick nick of the user to be set +/// void QXmppMixIq::setNick(const QString &nick) { d->nick = nick; @@ -174,15 +372,20 @@ void QXmppMixIq::setInvitation(const std::optional &invitati d->invitation = invitation; } -/// Returns the MIX channel action type. - +/// Returns the MIX channel's action type. +/// +/// \return the action type of the channel +/// QXmppMixIq::Type QXmppMixIq::actionType() const { return d->actionType; } -/// Sets the channel action. - +/// +/// Sets the MIX channel's action type. +/// +/// \param type action type of the channel +/// void QXmppMixIq::setActionType(QXmppMixIq::Type type) { d->actionType = type; @@ -214,30 +417,38 @@ void QXmppMixIq::parseElementFromChild(const QDomElement &element) return; } - if (auto index = MIX_ACTION_TYPES.indexOf(child.tagName()); index >= 0) { - d->actionType = Type(index); - } + const auto actionTypeIndex = MIX_ACTION_TYPES.indexOf(child.tagName()); + d->actionType = actionTypeIndex == -1 ? None : (QXmppMixIq::Type)actionTypeIndex; if (child.namespaceURI() == ns_mix_pam) { if (child.hasAttribute(QStringLiteral("channel"))) { - d->jid = child.attribute(QStringLiteral("channel")); + d->channelJid = child.attribute(QStringLiteral("channel")); } child = child.firstChildElement(); } if (!child.isNull() && child.namespaceURI() == ns_mix) { - if (child.hasAttribute(QStringLiteral("jid"))) { - d->jid = child.attribute(QStringLiteral("jid")); + // TODO: Will those attributes finally be adapted by the XEP? + if (child.hasAttribute(QStringLiteral("id"))) { + d->participantId = child.attribute(QStringLiteral("id")); + } + if (child.hasAttribute(QStringLiteral("jid")) && d->actionType != QXmppMixIq::UpdateSubscription) { + d->channelJid = (child.attribute(QStringLiteral("jid"))).split("#").last(); } if (child.hasAttribute(QStringLiteral("channel"))) { - d->channelName = child.attribute(QStringLiteral("channel")); + d->channelId = child.attribute(QStringLiteral("channel")); } QDomElement subChild = child.firstChildElement(); + QVector nodesBeingSubscribedTo; + QVector nodesBeingUnsubscribedFrom; + while (!subChild.isNull()) { if (subChild.tagName() == QStringLiteral("subscribe")) { - d->nodes << subChild.attribute(QStringLiteral("node")); + nodesBeingSubscribedTo << subChild.attribute(QStringLiteral("node")); + } else if (subChild.tagName() == QStringLiteral("unsubscribe")) { + nodesBeingUnsubscribedFrom << subChild.attribute(QStringLiteral("node")); } else if (subChild.tagName() == QStringLiteral("nick")) { d->nick = subChild.text(); } else if (subChild.tagName() == QStringLiteral("invitation")) { @@ -247,6 +458,9 @@ void QXmppMixIq::parseElementFromChild(const QDomElement &element) subChild = subChild.nextSiblingElement(); } + + d->nodesBeingSubscribedTo = listToNodes(nodesBeingSubscribedTo); + d->nodesBeingUnsubscribedFrom = listToNodes(nodesBeingUnsubscribedFrom); } } @@ -274,9 +488,8 @@ void QXmppMixIq::toXmlElementFromChild(QXmlStreamWriter *writer) const if (d->actionType == ClientJoin || d->actionType == ClientLeave) { writer->writeDefaultNamespace(ns_mix_pam); if (type() == Set) { - helperToXmlAddAttribute(writer, QStringLiteral("channel"), d->jid); + helperToXmlAddAttribute(writer, QStringLiteral("channel"), d->channelJid); } - if (d->actionType == ClientJoin) { writer->writeStartElement(QStringLiteral("join")); } else if (d->actionType == ClientLeave) { @@ -285,16 +498,25 @@ void QXmppMixIq::toXmlElementFromChild(QXmlStreamWriter *writer) const } writer->writeDefaultNamespace(ns_mix); - helperToXmlAddAttribute(writer, QStringLiteral("channel"), d->channelName); + helperToXmlAddAttribute(writer, QStringLiteral("channel"), d->channelId); if (type() == Result) { - helperToXmlAddAttribute(writer, QStringLiteral("jid"), d->jid); + helperToXmlAddAttribute(writer, QStringLiteral("id"), d->participantId); } - for (const auto &node : d->nodes) { + const auto nodesBeingSubscribedTo = nodesToList(d->nodesBeingSubscribedTo); + for (const auto &node : nodesBeingSubscribedTo) { writer->writeStartElement(QStringLiteral("subscribe")); writer->writeAttribute(QStringLiteral("node"), node); writer->writeEndElement(); } + + const auto nodesBeingUnsubscribedFrom = nodesToList(d->nodesBeingUnsubscribedFrom); + for (const auto &node : nodesBeingUnsubscribedFrom) { + writer->writeStartElement(QStringLiteral("unsubscribe")); + writer->writeAttribute(QStringLiteral("node"), node); + writer->writeEndElement(); + } + if (!d->nick.isEmpty()) { writer->writeTextElement(QStringLiteral("nick"), d->nick); } @@ -304,8 +526,35 @@ void QXmppMixIq::toXmlElementFromChild(QXmlStreamWriter *writer) const } writer->writeEndElement(); + if (d->actionType == ClientJoin || d->actionType == ClientLeave) { writer->writeEndElement(); } } /// \endcond + +QVector QXmppMixIq::nodesToList(Nodes nodes) +{ + QVector nodeList; + + for (auto itr = NODES.constBegin(); itr != NODES.constEnd(); ++itr) { + if (nodes.testFlag(itr.key())) { + nodeList.append(itr.value()); + } + } + + return nodeList; +} + +QXmppMixIq::Nodes QXmppMixIq::listToNodes(const QVector &nodeList) +{ + Nodes nodes; + + for (auto itr = NODES.constBegin(); itr != NODES.constEnd(); ++itr) { + if (nodeList.contains(itr.value())) { + nodes |= itr.key(); + } + } + + return nodes; +} diff --git a/src/base/QXmppMixIq.h b/src/base/QXmppMixIq.h index 340c41f8f..0e37691f4 100644 --- a/src/base/QXmppMixIq.h +++ b/src/base/QXmppMixIq.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2019 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin // // SPDX-License-Identifier: LGPL-2.1-or-later @@ -12,7 +13,6 @@ class QXmppMixInvitation; class QXmppMixIqPrivate; -/// /// \brief The QXmppMixIq class represents an IQ used to do actions on a MIX /// channel as defined by \xep{0369, Mediated Information eXchange (MIX)}, /// \xep{0405, Mediated Information eXchange (MIX): Participant Server @@ -21,11 +21,13 @@ class QXmppMixIqPrivate; /// \since QXmpp 1.1 /// /// \ingroup Stanzas -/// + class QXMPP_EXPORT QXmppMixIq : public QXmppIq { public: - /// The action type of the MIX query IQ. + /// + /// Action type of the MIX IQ stanza. + /// enum Type { None, ClientJoin, @@ -40,6 +42,23 @@ class QXMPP_EXPORT QXmppMixIq : public QXmppIq Destroy }; + /// + /// PubSub node belonging to a MIX channel. + /// + /// \since QXmpp 1.6 + /// + enum class Node { + None = 0, //< Do not receive updates for anything related to the channel. + AllowedJids = 1, //< Stay informed about JIDs allowed to participate in the channel. + BannedJids = 2, //< Stay informed about JIDs banned from participating in the channel. + Configuration = 4, //< Stay informed about configuration changes. + Information = 8, //< Stay informed about information changes. + Messages = 16, //< Receive messages sent over the channel. + Participants = 32, //< Stay informed about joined users and left participants. + Presence = 64, //< Stay informed about the participants' presence. + }; + Q_DECLARE_FLAGS(Nodes, Node) + QXmppMixIq(); QXmppMixIq(const QXmppMixIq &); QXmppMixIq(QXmppMixIq &&); @@ -52,16 +71,31 @@ class QXMPP_EXPORT QXmppMixIq : public QXmppIq void setActionType(QXmppMixIq::Type); QString jid() const; - void setJid(const QString &); + void setJid(const QString &jid); QString channelName() const; - void setChannelName(const QString &); + void setChannelName(const QString &channelName); + + QString participantId() const; + void setParticipantId(const QString &participantId); + + QString channelId() const; + void setChannelId(const QString &channelId); + + QString channelJid() const; + void setChannelJid(const QString &channelJid); QStringList nodes() const; - void setNodes(const QStringList &); + void setNodes(const QStringList &nodes); + + Nodes nodesBeingSubscribedTo() const; + void setNodesBeingSubscribedTo(Nodes nodes); + + Nodes nodesBeingUnsubscribedFrom() const; + void setNodesBeingUnsubscribedFrom(Nodes nodes); QString nick() const; - void setNick(const QString &); + void setNick(const QString &nick); QString inviteeJid() const; void setInviteeJid(const QString &inviteeJid); @@ -70,17 +104,26 @@ class QXMPP_EXPORT QXmppMixIq : public QXmppIq void setInvitation(const std::optional &invitation); /// \cond - static bool isMixIq(const QDomElement &); + static bool isMixIq(const QDomElement &element); /// \endcond protected: /// \cond - void parseElementFromChild(const QDomElement &) override; - void toXmlElementFromChild(QXmlStreamWriter *) const override; + void parseElementFromChild(const QDomElement &element) override; + void toXmlElementFromChild(QXmlStreamWriter *writer) const override; /// \endcond private: + static QVector nodesToList(Nodes nodes); + static Nodes listToNodes(const QVector &nodeList); + QSharedDataPointer d; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(QXmppMixIq::Nodes) +/// \cond +// Scoped enums (enum class) are not implicitly converted to int. +inline uint qHash(QXmppMixIq::Node key, uint seed) noexcept { return qHash(std::underlying_type_t(key), seed); } +/// \endcond + #endif // QXMPPMIXIQ_H diff --git a/src/base/QXmppMixItems.cpp b/src/base/QXmppMixItems.cpp index a19edbb0d..42262bdfc 100644 --- a/src/base/QXmppMixItems.cpp +++ b/src/base/QXmppMixItems.cpp @@ -14,6 +14,7 @@ static const auto CONTACT_JIDS = QStringLiteral("Contact"); class QXmppMixInfoItemPrivate : public QSharedData, public QXmppDataFormBase { public: + QXmppDataForm::Type dataFormType = QXmppDataForm::Result; QString name; QString description; QStringList contactJids; @@ -84,6 +85,26 @@ QXmppMixInfoItem &QXmppMixInfoItem::operator=(const QXmppMixInfoItem &) = defaul QXmppMixInfoItem &QXmppMixInfoItem::operator=(QXmppMixInfoItem &&) = default; QXmppMixInfoItem::~QXmppMixInfoItem() = default; +/// +/// Returns the type of the data form that contains the channel information. +/// +/// \return the data form's type +/// +const QXmppDataForm::Type QXmppMixInfoItem::formType() const +{ + return d->dataFormType; +} + +/// +/// Sets the type of the data form that contains the channel information. +/// +/// \param formType data form's type +/// +void QXmppMixInfoItem::setFormType(QXmppDataForm::Type formType) +{ + d->dataFormType = formType; +} + /// /// Returns the user-specified name of the MIX channel. This is not the name /// part of the channel's JID. @@ -168,7 +189,7 @@ void QXmppMixInfoItem::parsePayload(const QDomElement &payload) void QXmppMixInfoItem::serializePayload(QXmlStreamWriter *writer) const { auto form = d->toDataForm(); - form.setType(QXmppDataForm::Result); + form.setType(d->dataFormType); form.toXml(writer); } /// \endcond diff --git a/src/base/QXmppMixParticipantItem.h b/src/base/QXmppMixParticipantItem.h index ce179fbe4..431c83515 100644 --- a/src/base/QXmppMixParticipantItem.h +++ b/src/base/QXmppMixParticipantItem.h @@ -38,4 +38,6 @@ class QXMPP_EXPORT QXmppMixParticipantItem : public QXmppPubSubBaseItem QSharedDataPointer d; }; +Q_DECLARE_METATYPE(QXmppMixParticipantItem) + #endif // QXMPPMIXPARTICIPANTITEM_H diff --git a/src/client/QXmppMixManager.cpp b/src/client/QXmppMixManager.cpp new file mode 100644 index 000000000..2b21560d6 --- /dev/null +++ b/src/client/QXmppMixManager.cpp @@ -0,0 +1,1451 @@ +// SPDX-FileCopyrightText: 2023 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppMixManager.h" + +#include "QXmppClient.h" +#include "QXmppConstants_p.h" +#include "QXmppDiscoveryIq.h" +#include "QXmppDiscoveryManager.h" +#include "QXmppMessage.h" +#include "QXmppMixInfoItem.h" +#include "QXmppMixInvitation.h" +#include "QXmppMixIq.h" +#include "QXmppPubSubEvent.h" +#include "QXmppPubSubManager.h" +#include "QXmppRosterManager.h" +#include "QXmppUtils.h" + +#include +#include + +using namespace QXmpp::Private; + +/// +/// \class QXmppMixManager +/// +/// This class manages group chat communication as specified in the following XEPs: +/// * \xep{0369, Mediated Information eXchange (MIX)} +/// * \xep{0405, Mediated Information eXchange (MIX): Participant Server Requirements} +/// * \xep{0406, Mediated Information eXchange (MIX): MIX Administration} +/// * \xep{0407, Mediated Information eXchange (MIX): Miscellaneous Capabilities} +/// +/// In order to use this manager, you need to add it to the client: +/// \code +/// auto *manager = client->addNewExtension(); +/// \endcode +/// +/// If you want to be informed about results of the methods in this class by the corresponding +/// signals, please make sure to subscribe by the relevant subsciption methods before. +/// That way is chosen to keep the structure simple and not notify two times by one signal for the +/// same result. +/// +/// In order to send a message to a MIX channel, you have to set the type QXmppMessage::GroupChat. +/// +/// Example for an unencrypted message (mostly for public group chats or when the recipients do not +/// support encryption): +/// \code +/// message->setType(QXmppMessage::GroupChat); +/// message->setTo("group@mix.example.org") +/// client->send(std::move(message)); +/// \endcode +/// +/// Example for an encrypted message to be decrypted by Alice and Bob (mostly for private group +/// chats): +/// \code +/// message->setType(QXmppMessage::GroupChat); +/// message->setTo("group@mix.example.org") +/// +/// QXmppSendStanzaParams params; +/// params.setEncryptionJids({ "alice@example.org", "bob@example.com" }) +/// +/// client->sendSensitive(std::move(message), params); +/// \endcode +/// +/// \ingroup Managers +/// +/// \since QXmpp 1.6 +/// + +/// +/// \property QXmppMixManager::supportedByServer +/// +/// \see QXmppMixManager::supportedByServer() +/// + +/// +/// \property QXmppMixManager::archivingSupportedByServer +/// +/// \see QXmppMixManager::archivingSupportedByServer() +/// + +/// +/// \property QXmppMixManager::services +/// +/// \see QXmppMixManager::services() +/// + +/// +/// \struct QXmppMixManager::Service +/// +/// Service providing MIX channels and corresponding nodes. +/// +/// \var QXmppMixManager::Service::jid +/// +/// JID of the service. +/// +/// \var QXmppMixManager::Service::channelsSearchable +/// +/// Whether the service can be searched for channels. +/// +/// \var QXmppMixManager::Service::channelCreationAllowed +/// +/// Whether channels can be created on the service. +/// + +/// \cond +bool QXmppMixManager::Service::operator==(const Service &other) const +{ + return jid == other.jid && + channelsSearchable == other.channelsSearchable && + channelCreationAllowed == other.channelCreationAllowed; +} +/// \endcond + +/// +/// \struct QXmppMixManager::Subscription +/// +/// Subscription to nodes of a MIX channel. +/// +/// \var QXmppMixManager::Subscription::nodesBeingSubscribedTo +/// +/// Nodes belonging to the channel that are subscribed to. +/// +/// If not all desired nodes could be subscribed, this contains only the subscribed nodes. +/// +/// \var QXmppMixManager::Subscription::nodesBeingUnsubscribedFrom +/// +/// Nodes belonging to the channel that are unsubscribed from. +/// + +/// +/// \struct QXmppMixManager::Participation +/// +/// Participation in a channel. +/// +/// \var QXmppMixManager::Participation::participantId +/// +/// ID of the user within the channel. +/// +/// \var QXmppMixManager::Participation::nickname +/// +/// Nickname of the user within the channel. +/// +/// If the server modified the desired nickname, this is the modified one. +/// +/// \var QXmppMixManager::Participation::nodesBeingSubscribedTo +/// +/// Nodes belonging to the joined channel that are subscribed to. +/// +/// If not all desired nodes could be subscribed, this contains only the subscribed nodes. +/// + +/// +/// \typedef QXmppMixManager::Jid +/// +/// JID of a user or domain. +/// + +/// +/// \typedef QXmppMixManager::ChannelJid +/// +/// JID of a MIX channel. +/// + +/// +/// \typedef QXmppMixManager::Nickname +/// +/// Nickname of the user within a MIX channel. +/// +/// If the server modified the desired nickname, this is the modified one. +/// + +/// +/// \typedef QXmppMixManager::ChannelJidResult +/// +/// Contains the JIDs of all discoverable MIX channels of a MIX service or a QXmppError if it +/// failed. +/// + +/// +/// \typedef QXmppMixManager::InformationResult +/// +/// Contains the information of the MIX channel or a QXmppError if it failed. +/// + +/// +/// \typedef QXmppMixManager::CreationResult +/// +/// Contains the JID of the created MIX channel a QXmppError if it failed. +/// + +/// +/// \typedef QXmppMixManager::IsChannelPublicResult +/// +/// Contains whether the requested MIX channel is public or a QXmppError if it failed. +/// + +/// +/// \typedef QXmppMixManager::JoiningResult +/// +/// Contains the result of the joined MIX channel or a QXmppError if it failed. +/// + +/// +/// \typedef QXmppMixManager::NicknameResult +/// +/// Contains the new nickname within a joined MIX channel or a QXmppError if it failed. +/// + +/// +/// \typedef QXmppMixManager::SubscriptionResult +/// +/// Contains the result of the subscribed/unsubscribed nodes belonging to a MIX channel or a +/// QXmppError if it failed. +/// + +/// +/// \typedef QXmppMixManager::JidResult +/// +/// Contains the JIDs of users or domains that are allowed to participate resp. banned from +/// participating in a MIX channel or a QXmppError if it failed. +/// + +/// +/// \typedef QXmppMixManager::ParticipantResult +/// +/// Contains the participants of a MIX channel or a QXmppError if it failed. +/// + +constexpr auto MIX_SERVICE_DISCOVERY_NODE = "mix"; + +/// +/// Constructs a MIX manager. +/// +QXmppMixManager::QXmppMixManager() = default; + +QStringList QXmppMixManager::discoveryFeatures() const +{ + return QStringList() << ns_mix; +} + +/// +/// Returns whether the own server supports MIX clients. +/// +/// In that case, the server interacts between a client and a MIX service. +/// E.g., the server adds a MIX service to the client's roster after joining it and archives the +/// messages sent through the channel while the client is offline. +/// +/// \return whether MIX clients are supported +/// +bool QXmppMixManager::supportedByServer() const +{ + return m_supportedByServer; +} + +/// +/// \fn QXmppMixManager::supportedByServerChanged() +/// +/// Emitted when the server enabled or disabled supporting MIX clients. +/// + +/// +/// Returns whether the own server supports archiving messages via +/// \xep{0313, Message Archive Management} of MIX channels the user participates in. +/// +/// \return whether MIX messages are archived +/// +bool QXmppMixManager::archivingSupportedByServer() const +{ + return m_archivingSupportedByServer; +} + +/// +/// \fn QXmppMixManager::archivingSupportedByServerChanged() +/// +/// Emitted when the server enabled or disabled supporting archiving for MIX. +/// + +/// +/// Returns the services providing MIX on the own server. +/// +/// Such services provide MIX channels and their nodes. +/// It interacts directly with clients or with their servers. +/// +/// \return the provided MIX services +/// +QList QXmppMixManager::services() const +{ + return m_services; +} + +/// +/// \fn QXmppMixManager::servicesChanged() +/// +/// Emitted when the services providing MIX on the own server changed. +/// + +/// +/// Requests the JIDs of all discoverable MIX channels of a MIX service. +/// +/// \param serviceJid JID of the service that provides the channels +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestChannelJids(const QString &serviceJid) +{ + QXmppPromise promise; + + auto task = m_discoveryManager->requestDiscoItems(serviceJid); + + task.then(this, [promise](QXmppDiscoveryManager::ItemsResult result) mutable { + if (const auto items = std::get_if>(&result)) { + QVector channelJids; + + std::for_each(items->cbegin(), items->cend(), [&channelJids](const QXmppDiscoveryIq::Item &item) { + channelJids.append(item.jid()); + }); + + promise.finish(channelJids); + } else { + promise.finish(std::move(std::get(result))); + } + }); + + return promise.task(); +} + +/// +/// Requests the information of a MIX channel. +/// +/// \param channelJid JID of the channel whose information are requested +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestChannelInformation(const QString &channelJid) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->requestItems(channelJid, ns_mix_node_info); + task.then(this, [this, promise, channelJid](QXmppPubSubManager::ItemsResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(std::get>(result).items.constFirst())); + } + }); + + return promise.task(); +} + +/// +/// Updates the information of a MIX channel. +/// +/// In order to use this method, retrieve the current information via requestChannelInformation() +/// first, change the desired attributes and pass the information to this method. +/// +/// \param channelJid JID of the channel whose information is to be updated +/// \param information new information of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::updateChannelInformation(const QString &channelJid, QXmppMixInfoItem information) +{ + QXmppPromise promise; + + information.setFormType(QXmppDataForm::Submit); + + auto task = m_pubSubManager->publishItem(channelJid, ns_mix_node_info, information); + task.then(this, [this, promise](QXmppPubSubManager::PublishItemResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(QXmpp::Success())); + } + }); + + return promise.task(); +} + +/// +/// Creates a private MIX channel. +/// +/// The term "private" means that the channel cannot be discovered by anyone and only allowed JIDs +/// can participate in it. +/// Furthermore, the channel is created with a channel ID provided by the MIX service. +/// +/// The channel ID is the local part of the channel JID. +/// The MIX service JID is the domain part of the channel JID. +/// Example: "channel" is the channel ID and "mix.example.org" the service JID of the channel JID +/// "channel@mix.example.org". +/// +/// If you want to create a channel with a self-defined channel ID but that not everybody is allowed +/// to participate in, you can call createPublicChannel() first and then makeChannelPrivate(). +/// But note that such a channel can be discovered by anyone. +/// +/// \param serviceJid JID of the service +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::createPrivateChannel(const QString &serviceJid) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(serviceJid); + iq.setActionType(QXmppMixIq::Create); + + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> CreationResult { + return iq.channelJid().isEmpty() ? iq.channelId() % "@" % iq.from() : iq.channelJid(); + }); +} + +/// +/// Creates a public MIX channel. +/// +/// The term "public" means that the channel can be discovered by anyone and everybody except banned +/// JIDs can participate in it. +/// Furthermore, the channel is created with a self-defined channel ID. +/// +/// The channel ID is the local part of the channel JID. +/// The MIX service JID is the domain part of the channel JID. +/// Example: "channel" is the channel ID and "mix.example.org" the service JID of the channel JID +/// "channel@mix.example.org". +/// +/// \param serviceJid JID of the service +/// \param channelId ID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::createPublicChannel(const QString &serviceJid, const QString &channelId) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(serviceJid); + iq.setActionType(QXmppMixIq::Create); + iq.setChannelId(channelId); + + // TODO: Remove allowed node after creation + + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> CreationResult { + return iq.channelJid().isEmpty() ? iq.channelId() % "@" % iq.from() : iq.channelJid(); + }); +} + +/// +/// Requests whether a MIX channel is public (i.e., it is discoverable and can be joined by +/// everyone). +/// +/// A channel is considered private if it cannot be discovered. +/// That can be the case if it is not discoverable or does not exist. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::isChannelPublic(const QString &channelJid) +{ + QXmppPromise promise; + + auto task = requestNodes(channelJid); + task.then(this, [promise](QXmppDiscoveryManager::ItemsResult result) mutable { + if (const auto items = std::get_if>(&result)) { + promise.finish(!std::any_of(items->cbegin(), items->cend(), [](const QXmppDiscoveryIq::Item &item) { + return item.node() == ns_mix_node_allowed; + })); + } else { + const auto error = std::get(result); + + // Treat the channel as private if it cannot be discovered. + if (const auto stanzaError = error.value(); + stanzaError && + stanzaError->type() == QXmppStanza::Error::Cancel && + stanzaError->condition() == QXmppStanza::Error::ItemNotFound) { + promise.finish(false); + } else { + promise.finish(std::move(error)); + } + } + }); + + return promise.task(); +} + +/// +/// Transforms a public MIX channel into a private one. +/// +/// This cannot be used for channels with server-specified JIDs because they are already private at +/// any time. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::makeChannelPrivate(const QString &channelJid) const +{ + return m_pubSubManager->createNode(channelJid, ns_mix_node_allowed); +} + +/// +/// \fn QXmppMixManager::channelMadePrivate(const QString &channelJid) +/// +/// Emitted when a MIX channel is made private. +/// +/// \param channelJid JID of the channel which is made private +/// + +/// +/// Joins a MIX channel to become a participant of it. +/// +/// \param channelJid JID of the channel being joined +/// \param nickname nickname of the user which is usually required by the server (default: no +/// nickname is set) +/// \param nodes nodes of the channel that are subscribed to for receiving their updates (default: +/// all nodes are subcribed to) +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::joinChannel(const QString &channelJid, const QString &nickname, QXmppMixIq::Nodes nodes) +{ + return joinChannel(std::move(prepareJoinIq(channelJid, nickname, nodes))); +} + +/// +/// Invites a user to a MIX channel that the user is not yet allowed to participate in. +/// +/// This requests an invitation from the channel and sends it to the invitee. +/// The invitee can then use that invitation to join the channel. +/// +/// That invitation mechanism avoids storing allowed JIDs for an indefinite time if the invited user +/// never joins the channel. +/// By using this method, there is no need of allowing JIDs via allowJid() (while being permitted to +/// do so) and sending them invitations via sendInvitation() manually. +/// +/// This method can be used in the following cases: +/// * The inviter is an administrator of the channel. +/// * The inviter is a participant of the channel and the channel allows all participants to +/// invite new users. +/// +/// \param channelJid JID of the channel that the contact is invited to +/// \param inviteeJid JID of the invited user +/// \param messageBody body of the invitation message sent to the invited contact +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::invite(const QString &channelJid, const QString &inviteeJid, const QString &messageBody) +{ + QXmppPromise promise; + + QXmppMixIq iq; + iq.setType(QXmppIq::Get); + iq.setTo(channelJid); + iq.setActionType(QXmppMixIq::InvitationRequest); + iq.setInviteeJid(inviteeJid); + + auto task = client()->sendIq(std::move(iq)); + task.then(this, [this, promise, channelJid, inviteeJid, messageBody](QXmppClient::IqResult &&result) mutable { + if (const auto error = std::get_if(&result)) { + promise.finish(*error); + } else { + QXmppMixIq iq; + iq.parse(std::get(result)); + + auto task = sendInvitation(*(iq.invitation()), messageBody); + task.then(this, [this, promise, channelJid, inviteeJid, messageBody](QXmpp::SendResult result) mutable { + promise.finish(result); + }); + } + }); + + return promise.task(); +} + +/// +/// Sends a MIX channel invitation to a user without requesting an invitation from the channel. +/// +/// If you need to request an invitation from the channel, use invite() instead. +/// +/// This method can be used if the invitee is already allowed to participate in the channel but did +/// not join yet. +/// +/// This method can also be used if the channel does neither allow the invitee yet nor support +/// invitations via invite() but the inviter is permitted to allow JIDs. +/// In that case, the user's JID has to be added via allowJid() before calling this method. +/// +/// This method can be used in the following cases: +/// * Everybody is allowed to participate in the channel. +/// * The invitee is explicitly allowed to participate in the channel. +/// That is particularly relevant if the channel does not support invitations via invite() but +/// the inviter is permitted to allow JIDs via allowJid(). +/// In that case, the invitee's JID has to be allowed before calling this method. +/// +/// The sent invitation is not meant to be read by a human. +/// Instead, the receiving client needs to support it. +/// But you can add an appropriate text to the body of the invitation message to enable human users +/// of clients that do not support that feature to join the channel manually. +/// For example, you could add the JID of the channel or even an XMPP URI to the body. +/// +/// \param channelJid JID of the channel that the contact is invited to +/// \param inviteeJid JID of the invited user +/// \param messageBody body of the message sent to the invited contact +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::sendInvitation(const QString &channelJid, const QString &inviteeJid, const QString &messageBody) +{ + QXmppMixInvitation invitation; + invitation.setInviterJid(client()->configuration().jidBare()); + invitation.setInviteeJid(inviteeJid); + invitation.setChannelJid(channelJid); + + return sendInvitation(invitation, messageBody); +} + +/// +/// \fn QXmppMixManager::invited(const QXmppMixInvitation &invitation) +/// +/// Emitted when the user is invited to a MIX channel. +/// +/// \param invitation invitation used to join the channel +/// + +/// +/// Joins a MIX channel via an invitation to become a participant of it. +/// +/// \param invitation invitation to the channel +/// \param nickname nickname of the user which is usually required by the server (default: no +/// nickname is set) +/// \param nodes nodes of the channel that are subscribed to for receiving their updates (default: +/// all nodes are subcribed to) +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::acceptInvitation(const QXmppMixInvitation &invitation, const QString &nickname, QXmppMixIq::Nodes nodes) +{ + auto iq = prepareJoinIq(invitation.channelJid(), nickname, nodes); + + // Submit the invitation only if it was generated by the channel and thus needed to join. + if (!invitation.token().isEmpty()) { + iq.setInvitation(invitation); + } + + return joinChannel(std::move(iq)); +} + +/// +/// Updates the nickname within a channel. +/// +/// If the update succeeded, the new nickname is returned which may differ from the requested one. +/// +/// \param channelJid JID of the channel +/// \param nickname nickname to be set +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::updateNickname(const QString &channelJid, const QString &nickname) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(channelJid); + iq.setActionType(QXmppMixIq::SetNick); + iq.setNick(nickname); + + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> NicknameResult { + return iq.nick(); + }); +} + +/// +/// Updates the subscriptions to nodes of a MIX channel. +/// +/// \param channelJid JID of the channel +/// \param nodesToSubscribeTo nodes to subscribe to +/// \param nodesToUnsubscribeFrom nodes to unsubscribe from +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::updateSubscriptions(const QString &channelJid, QXmppMixIq::Nodes nodesToSubscribeTo, QXmppMixIq::Nodes nodesToUnsubscribeFrom) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(channelJid); + iq.setActionType(QXmppMixIq::UpdateSubscription); + iq.setNodesBeingSubscribedTo(nodesToSubscribeTo); + iq.setNodesBeingUnsubscribedFrom(nodesToUnsubscribeFrom); + + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> SubscriptionResult { + return QXmppMixManager::Subscription { iq.nodesBeingSubscribedTo(), iq.nodesBeingUnsubscribedFrom() }; + }); +} + +/// +/// Requests all JIDs which are allowed to participate in a MIX channel. +/// +/// The JIDs can specify users (e.g., "alice@example.org") or groups of users (e.g., "example.org") +/// to let all users join which have a JID containing the specified domain. +/// This is only relevant/used for private channels having a user-specified JID. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestAllowedJids(const QString &channelJid) +{ + return requestJids(channelJid, ns_mix_node_allowed); +} + +/// +/// Allows a JID to participate in a MIX channel. +/// +/// The JID can specify a user (e.g., "alice@example.org") or groups of users (e.g., "example.org") +/// to let all users join which have a JID containing the specified domain. +/// +/// If everybody was allowed to participate in the channel before, only allowed JIDs can participate +/// afterwards. +/// In order to allow everybody to participate in the channel again, call allowAllJids(). +/// +/// \param channelJid JID of the channel +/// \param jid bare JID to be allowed +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::allowJid(const QString &channelJid, const QString &jid) +{ + return addJidToNode(channelJid, ns_mix_node_allowed, jid); +} + +/// +/// \fn QXmppMixManager::jidAllowed(const QString &channelJid, const QString &jid) +/// +/// Emitted when a JID is allowed to participate in a MIX channel. +/// +/// That happens if allowJid() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// \param jid allowed bare JID +/// + +/// +/// Allows all JIDs to participate in a MIX channel. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::allowAllJids(const QString &channelJid) +{ + return deleteNode(channelJid, ns_mix_node_allowed); +} + +/// +/// \fn QXmppMixManager::allJidsAllowed(const QString &channelJid) +/// +/// Emitted when all JIDs are allowed to participate in a MIX channel. +/// +/// That happens if allowAllJids() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// + +/// +/// Disallows a formerly allowed JID to participate in a MIX channel. +/// +/// Only allowed JIDs can be disallowed via this method. +/// In order to disallow other JIDs, use banJid(). +/// +/// \param channelJid JID of the channel +/// \param jid bare JID to be disallowed +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::disallowJid(const QString &channelJid, const QString &jid) +{ + return m_pubSubManager->retractItem(channelJid, ns_mix_node_allowed, jid); +} + +/// +/// \fn QXmppMixManager::jidDisallowed(const QString &channelJid, const QString &jid) +/// +/// Emitted when a fomerly allowed JID is disallowed to participate in a MIX channel anymore. +/// +/// That happens if disallowJid() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// \param jid disallowed bare JID +/// + +/// +/// Disallows all formerly allowed JIDs to participate in a MIX channel. +/// +/// Only allowed JIDs can be disallowed via this method. +/// In order to disallow other JIDs, use banJid(). +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::disallowAllJids(const QString &channelJid) +{ + return m_pubSubManager->purgeItems(channelJid, ns_mix_node_allowed); +} + +/// +/// \fn QXmppMixManager::allJidsDisallowed(const QString &channelJid) +/// +/// Emitted when no JID is allowed to participate in a MIX channel anymore. +/// +/// That happens if disallowAllJids() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// + +/// +/// Requests all JIDs which are not allowed to participate in a MIX channel. +/// +/// \param channelJid JID of the corresponding channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestBannedJids(const QString &channelJid) +{ + return requestJids(channelJid, ns_mix_node_banned); +} + +/// +/// Bans a JID from participating in a MIX channel. +/// +/// The JID can specify a user (e.g., "alice@example.org") or groups of users (e.g., "example.org") +/// to ban all users which have a JID containing the specified domain. +/// +/// \param channelJid JID of the channel +/// \param jid bare JID to be banned +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::banJid(const QString &channelJid, const QString &jid) +{ + return addJidToNode(channelJid, ns_mix_node_banned, jid); +} + +/// +/// \fn QXmppMixManager::jidBanned(const QString &channelJid, const QString &jid) +/// +/// Emitted when a JID is banned from participating in a MIX channel. +/// +/// That happens if banJid() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// \param jid banned bare JID +/// + +/// +/// Unbans a formerly banned JID from participating in a MIX channel. +/// +/// \param channelJid JID of the channel +/// \param jid bare JID to be unbanned +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::unbanJid(const QString &channelJid, const QString &jid) +{ + return m_pubSubManager->retractItem(channelJid, ns_mix_node_banned, jid); +} + +/// +/// \fn QXmppMixManager::jidUnbanned(const QString &channelJid, const QString &jid) +/// +/// Emitted when a formerly banned JID is unbanned from participating in a MIX channel. +/// +/// That happens if unbanJid() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// \param jid unbanned bare JID +/// + +/// +/// Unbans all formerly banned JIDs from participating in a MIX channel. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::unbanAllJids(const QString &channelJid) +{ + return deleteNode(channelJid, ns_mix_node_banned); +} + +/// +/// \fn QXmppMixManager::allJidsUnbanned(const QString &channelJid) +/// +/// Emitted when all JIDs are unbanned from participating in a MIX channel. +/// +/// That happens if unbanAllJids() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// + +/// +/// Requests all participants of a MIX channel. +/// +/// In the case of a channel that not everybody is allowed to participate in, the participants are a +/// subset of the allowed JIDs. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestParticipants(const QString &channelJid) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->requestItems(channelJid, ns_mix_node_participants); + task.then(this, [this, promise](QXmppPubSubManager::ItemsResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(std::get>(result).items)); + } + }); + + return promise.task(); +} + +/// +/// \fn QXmppMixManager::userJoinedOrParticipantModified(const QString &channelJid, const QXmppMixParticipantItem &participantItem) +/// +/// Emitted when a user joined a MIX channel or a participant changed the nick. +/// +/// \param channelJid JID of the channel which is joined by the user or for which the participant changed the nick +/// \param participantItem item for the new or modified participant +/// + +/// +/// \fn QXmppMixManager::participantLeft(const QString &channelJid, const QString &participantId) +/// +/// Emitted when a participant left the MIX channel. +/// +/// \param channelJid JID of the channel which is left by the participant +/// \param participantId ID of the left participant +/// + +/// +/// Leaves a MIX channel. +/// +/// \param channelJid JID of the channel to be left +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::leaveChannel(const QString &channelJid) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(client()->configuration().jidBare()); + iq.setActionType(QXmppMixIq::ClientLeave); + iq.setChannelJid(channelJid); + + return client()->sendGenericIq(std::move(iq)); +} + +/// +/// Deletes a MIX channel. +/// +/// \param channelJid JID of the channel to be deleted +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::deleteChannel(const QString &channelJid) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(QXmppUtils::jidToDomain(channelJid)); + iq.setActionType(QXmppMixIq::Destroy); + iq.setChannelId(QXmppUtils::jidToUser(channelJid)); + + return client()->sendGenericIq(std::move(iq)); +} + +/// +/// \fn QXmppMixManager::channelDeleted(const QString &channelJid) +/// +/// Emitted when a MIX channel is deleted. +/// +/// \param channelJid JID of the deleted channel +/// + +/// \cond +void QXmppMixManager::setClient(QXmppClient *client) +{ + QXmppClientExtension::setClient(client); + + // Reset cached information after the client disconnected from the server. + connect(client, &QXmppClient::disconnected, this, [this]() { + setSupportedByServer(false); + setArchivingSupportedByServer(false); + removeServices(); + }); + + if (!(m_discoveryManager = client->findExtension())) { + m_discoveryManager = client->addNewExtension(); + } + + connect(m_discoveryManager, &QXmppDiscoveryManager::infoReceived, this, &QXmppMixManager::handleDiscoInfo); + + if (!(m_pubSubManager = client->findExtension())) { + m_pubSubManager = client->addNewExtension(); + } +} + +bool QXmppMixManager::handleMessage(const QXmppMessage &message) +{ + if (const auto invitation = message.mixInvitation()) { + Q_EMIT invited(*invitation); + return true; + } + + return false; +} + +bool QXmppMixManager::handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName) +{ + // TODO: That information must be retrieved from an update of the Config node (update of ) but that node is optional so that it cannot be ensured that users are always informed about a new Allowed node + // case QXmppPubSubIq::QueryType::CreateQuery: + // Q_EMIT channelMadePrivate(channelJid); + + if (nodeName == ns_mix_node_allowed && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + // Items have been published. + case QXmppPubSubEventBase::Items: { + const auto items = event.items(); + for (const auto &item : items) { + Q_EMIT jidAllowed(pubSubService, item.id()); + } + break; + } + // Specific items are deleted. + case QXmppPubSubEventBase::Retract: { + const auto ids = event.retractIds(); + for (const auto &id : ids) { + Q_EMIT jidDisallowed(pubSubService, id); + } + break; + } + // All items are deleted. + case QXmppPubSubEventBase::Purge: + Q_EMIT allJidsDisallowed(pubSubService); + break; + // The whole node is deleted. + case QXmppPubSubEventBase::Delete: + Q_EMIT allJidsAllowed(pubSubService); + break; + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } else if (nodeName == ns_mix_node_banned && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + // Items have been published. + case QXmppPubSubEventBase::Items: { + const auto items = event.items(); + for (const auto &item : items) { + Q_EMIT jidBanned(pubSubService, item.id()); + } + break; + } + // Specific items are deleted. + case QXmppPubSubEventBase::Retract: { + const auto ids = event.retractIds(); + for (const auto &id : ids) { + Q_EMIT jidUnbanned(pubSubService, id); + } + break; + } + // All items are deleted. + case QXmppPubSubEventBase::Purge: + // The whole node is deleted. + case QXmppPubSubEventBase::Delete: + Q_EMIT allJidsUnbanned(pubSubService); + break; + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } else if (nodeName == ns_mix_node_participants && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + // Items have been published. + case QXmppPubSubEventBase::Items: { + const auto items = event.items(); + for (const auto &item : items) { + Q_EMIT userJoinedOrParticipantModified(pubSubService, item); + } + break; + } + // Specific items are deleted. + case QXmppPubSubEventBase::Retract: { + const auto ids = event.retractIds(); + for (const auto &id : ids) { + Q_EMIT participantLeft(pubSubService, id); + } + break; + } + // All items are deleted. + case QXmppPubSubEventBase::Purge: + // The whole node is deleted. + case QXmppPubSubEventBase::Delete: + Q_EMIT channelDeleted(pubSubService); + break; + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } + + return false; +} + +/// +/// Pepares an IQ stanza for joining a MIX channel. +/// +/// \param channelJid JID of the channel being joined +/// \param nickname nickname of the user which is usually required by the server (default: no +/// nickname is set) +/// \param nodes nodes of the channel that are subscribed to for receiving their updates (default: +/// all nodes are subcribed to) +/// +/// \return the prepared MIX join IQ stanza +/// +QXmppMixIq QXmppMixManager::prepareJoinIq(const QString &channelJid, const QString &nickname, QXmppMixIq::Nodes nodes) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(client()->configuration().jidBare()); + iq.setActionType(QXmppMixIq::ClientJoin); + iq.setChannelJid(channelJid); + iq.setNick(nickname); + iq.setNodesBeingSubscribedTo(nodes); + + return iq; +} +/// \endcond + +/// +/// Joins a MIX channel. +/// +/// \param iq IQ stanza for joining a channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::joinChannel(QXmppMixIq &&iq) +{ + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> JoiningResult { + return Participation { iq.participantId(), iq.nick(), iq.nodesBeingSubscribedTo() }; + }); +} + +/// +/// Sends a MIX channel invitation to a user. +/// +/// \param invitation invitation to the channel +/// \param messageBody body of the message sent to the invited contact +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::sendInvitation(const QXmppMixInvitation &invitation, const QString &messageBody) +{ + QXmppMessage message; + message.setTo(invitation.inviteeJid()); + message.setMixInvitation(invitation); + + // A message having no body would neither be delivered to all clients via Message Carbons nor + // delivered to clients which are currently offline. + // To enforce that behavior, set a corresponding message type and message processing hint. + if (messageBody.isEmpty()) { + message.setType(QXmppMessage::Chat); + message.addHint(QXmppMessage::Store); + } else { + message.setBody(messageBody); + } + + return client()->sendSensitive(std::move(message)); +} + +/// +/// Requests all nodes of a MIX channel. +/// +/// Only nodes that are accessible by the user are retrieved. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestNodes(const QString &channelJid) +{ + return m_discoveryManager->requestDiscoItems(channelJid, MIX_SERVICE_DISCOVERY_NODE); +} + +QXmppTask QXmppMixManager::deleteNode(const QString &channelJid, const QString &node) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->deleteNode(channelJid, node); + task.then(this, [this, promise, channelJid](QXmppClient::EmptyResult result) mutable { + if (auto error = std::get_if(&result)) { + // If the node could not be deleted because it did not exist, consider the deletion as + // succeeded. + // If there is another error, return it directly. + if (const auto stanzaError = error->value(); + stanzaError && + stanzaError->type() == QXmppStanza::Error::Cancel && + stanzaError->condition() == QXmppStanza::Error::ItemNotFound) { + promise.finish(std::move(QXmpp::Success())); + } else { + promise.finish(std::move(*error)); + } + } else { + promise.finish(std::move(QXmpp::Success())); + } + }); + + return promise.task(); +} + +/// +/// Requests all JIDs of a node belonging to a MIX. +/// +/// This is only used for nodes storing items with IDs representing JIDs. +/// +/// \param channelJid JID of the channel +/// \param node node to be queried +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestJids(const QString &channelJid, const QString &node) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->requestItems(channelJid, node); + task.then(this, [this, promise](QXmppPubSubManager::ItemsResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + const auto items = std::get>(result).items; + QVector jids; + + std::for_each(items.cbegin(), items.cend(), [&jids](const QXmppPubSubBaseItem &item) mutable { + jids.append(item.id()); + }); + + promise.finish(std::move(jids)); + } + }); + + return promise.task(); +} + +/// +/// Adds a JID to a node of a MIX channel. +/// +/// This is only used for nodes storing items with IDs representing JIDs. +/// +/// \param channelJid JID of the channel +/// \param node node to which the JID is added +/// \param jid JID to be added +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::addJidToNode(const QString &channelJid, const QString &node, const QString &jid) +{ + QXmppPromise promise; + + const QXmppPubSubBaseItem item { jid }; + + auto task = m_pubSubManager->publishItem(channelJid, node, item); + task.then(this, [this, promise, channelJid, node, item](QXmppPubSubManager::PublishItemResult result) mutable { + if (auto error = std::get_if(&result)) { + // If the JID could not be added to the desired node because the node did not exist, + // create the node first and publish the corresponding item afterwards. + // If there is another error, return it directly. + if (const auto stanzaError = error->value(); + stanzaError && + stanzaError->type() == QXmppStanza::Error::Cancel && + stanzaError->condition() == QXmppStanza::Error::ItemNotFound) { + auto task = m_pubSubManager->createNode(channelJid, node); + task.then(this, [this, promise, channelJid, node, item](QXmppClient::EmptyResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + auto task = m_pubSubManager->publishItem(channelJid, node, item); + task.then(this, [this, promise, channelJid, node](QXmppPubSubManager::PublishItemResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(QXmpp::Success())); + } + }); + } + }); + } else { + promise.finish(std::move(*error)); + } + } else { + promise.finish(std::move(QXmpp::Success())); + } + }); + + return promise.task(); +} + +/// +/// Handles incoming service infos specified by \xep{0030, Service Discovery} +/// +/// \param iq received Service Discovery IQ stanza +/// +void QXmppMixManager::handleDiscoInfo(const QXmppDiscoveryIq &iq) +{ + // Check the server's functionality to support MIX clients. + if (iq.from().isEmpty() || iq.from() == client()->configuration().domain()) { + // Check whether MIX is supported. + if (iq.features().contains(ns_mix_pam)) { + setSupportedByServer(true); + + // Check whether MIX archiving is supported. + if (iq.features().contains(ns_mix_pam_archiving)) { + setArchivingSupportedByServer(true); + } + } else { + setSupportedByServer(false); + setArchivingSupportedByServer(false); + } + } + + const auto jid = iq.from().isEmpty() ? client()->configuration().domain() : iq.from(); + + // Search for a MIX service and check what it supports. + // if none can be found, remove them from the cache. + if (!iq.features().contains(ns_mix)) { + removeService(jid); + return; + } + + const auto identities = iq.identities(); + + for (const QXmppDiscoveryIq::Identity &identity : identities) { + // ' || identity.type() == "text"' is a workaround for older ejabberd versions. + if (identity.category() == "conference" && (identity.type() == MIX_SERVICE_DISCOVERY_NODE || identity.type() == "text")) { + Service service; + service.jid = iq.from().isEmpty() ? client()->configuration().domain() : iq.from(); + service.channelsSearchable = iq.features().contains(ns_mix_searchable); + service.channelCreationAllowed = iq.features().contains(ns_mix_create_channel); + + addService(service); + return; + } + } + + removeService(jid); +} + +/// +/// Sets whether the own server supports MIX. +/// +/// \param supportedByServer whether MIX is supported by the own server +/// +void QXmppMixManager::setSupportedByServer(bool supportedByServer) +{ + if (m_supportedByServer != supportedByServer) { + m_supportedByServer = supportedByServer; + Q_EMIT supportedByServerChanged(); + } +} + +/// +/// Sets whether the own server supports archiving messages via +/// \xep{0313, Message Archive Management} of MIX channels the user participates in. +/// +/// \param archivingSupportedByServer whether MIX messages are archived by the own server +/// +void QXmppMixManager::setArchivingSupportedByServer(bool archivingSupportedByServer) +{ + if (m_archivingSupportedByServer != archivingSupportedByServer) { + m_archivingSupportedByServer = archivingSupportedByServer; + Q_EMIT archivingSupportedByServerChanged(); + } +} + +/// +/// Adds a MIX service. +/// +/// \param service MIX service +/// +void QXmppMixManager::addService(const Service &service) +{ + auto itr = std::find_if(m_services.begin(), m_services.end(), [&jid = service.jid](const Service &service) { + return service.jid == jid; + }); + + if (itr == m_services.end()) { + m_services.append(service); + } else if (*itr == service) { + return; + } else { + *itr = service; + } + + Q_EMIT servicesChanged(); +} + +/// +/// Removes a MIX service. +/// +/// \param jid JID of the MIX service +/// +void QXmppMixManager::removeService(const QString &jid) +{ + auto itr = std::find_if(m_services.begin(), m_services.end(), [&jid](const Service &service) { + return service.jid == jid; + }); + + if (itr == m_services.end()) { + return; + } else { + m_services.erase(itr); + } + + Q_EMIT servicesChanged(); +} + +/// +/// Removes all MIX services. +/// +void QXmppMixManager::removeServices() +{ + if (!m_services.isEmpty()) { + m_services.clear(); + Q_EMIT servicesChanged(); + } +} diff --git a/src/client/QXmppMixManager.h b/src/client/QXmppMixManager.h new file mode 100644 index 000000000..1dcf62dc2 --- /dev/null +++ b/src/client/QXmppMixManager.h @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2023 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "QXmppClient.h" +#include "QXmppClientExtension.h" +#include "QXmppDiscoveryManager.h" +#include "QXmppMessageHandler.h" +#include "QXmppMixIq.h" +#include "QXmppMixParticipantItem.h" +#include "QXmppPubSubEventHandler.h" + +class QXmppMixInfoItem; + +class QXMPP_EXPORT QXmppMixManager : public QXmppClientExtension, public QXmppMessageHandler, public QXmppPubSubEventHandler +{ + Q_OBJECT + Q_PROPERTY(bool supportedByServer READ supportedByServer NOTIFY supportedByServerChanged) + Q_PROPERTY(bool archivingSupportedByServer READ archivingSupportedByServer NOTIFY archivingSupportedByServerChanged) + Q_PROPERTY(QList services READ services NOTIFY servicesChanged) + +public: + struct Service + { + QString jid; + bool channelsSearchable = false; + bool channelCreationAllowed = false; + + /// \cond + bool operator==(const Service &other) const; + /// \endcond + }; + + struct Subscription + { + QXmppMixIq::Nodes nodesBeingSubscribedTo; + QXmppMixIq::Nodes nodesBeingUnsubscribedFrom; + }; + + struct Participation + { + QString participantId; + QString nickname; + QXmppMixIq::Nodes nodesBeingSubscribedTo; + }; + + using Jid = QString; + using ChannelJid = QString; + using Nickname = QString; + + using ChannelJidResult = std::variant, QXmppError>; + using InformationResult = std::variant; + using CreationResult = std::variant; + using IsChannelPublicResult = std::variant; + using JoiningResult = std::variant; + using NicknameResult = std::variant; + using SubscriptionResult = std::variant; + using JidResult = std::variant, QXmppError>; + using ParticipantResult = std::variant, QXmppError>; + + QXmppMixManager(); + + QStringList discoveryFeatures() const override; + + bool supportedByServer() const; + Q_SIGNAL void supportedByServerChanged(); + + bool archivingSupportedByServer() const; + Q_SIGNAL void archivingSupportedByServerChanged(); + + QList services() const; + Q_SIGNAL void servicesChanged(); + + QXmppTask requestChannelJids(const QString &serviceJid); + + QXmppTask requestChannelInformation(const QString &channelJid); + QXmppTask updateChannelInformation(const QString &channelJid, QXmppMixInfoItem information); + + QXmppTask createPrivateChannel(const QString &serviceJid); + QXmppTask createPublicChannel(const QString &serviceJid, const QString &channelId); + + QXmppTask isChannelPublic(const QString &channelJid); + + QXmppTask makeChannelPrivate(const QString &channelJid) const; + Q_SIGNAL void channelMadePrivate(const QString &channelJid); + + QXmppTask joinChannel(const QString &channelJid, const QString &nickname = {}, QXmppMixIq::Nodes nodes = ~QXmppMixIq::Nodes()); + + QXmppTask invite(const QString &channelJid, const QString &inviteeJid, const QString &messageBody = {}); + QXmppTask sendInvitation(const QString &channelJid, const QString &inviteeJid, const QString &messageBody = {}); + Q_SIGNAL void invited(const QXmppMixInvitation &invitation); + QXmppTask acceptInvitation(const QXmppMixInvitation &invitation, const QString &nickname = {}, QXmppMixIq::Nodes nodes = ~QXmppMixIq::Nodes()); + + QXmppTask updateNickname(const QString &channelJid, const QString &nickname); + + QXmppTask updateSubscriptions(const QString &channelJid, QXmppMixIq::Nodes nodesToSubscribeTo = ~QXmppMixIq::Nodes(), QXmppMixIq::Nodes nodesToUnsubscribeFrom = ~QXmppMixIq::Nodes()); + + QXmppTask requestAllowedJids(const QString &channelJid); + QXmppTask allowJid(const QString &channelJid, const QString &jid); + Q_SIGNAL void jidAllowed(const QString &channelJid, const QString &jid); + QXmppTask allowAllJids(const QString &channelJid); + Q_SIGNAL void allJidsAllowed(const QString &channelJid); + + QXmppTask disallowJid(const QString &channelJid, const QString &jid); + Q_SIGNAL void jidDisallowed(const QString &channelJid, const QString &jid); + QXmppTask disallowAllJids(const QString &channelJid); + Q_SIGNAL void allJidsDisallowed(const QString &channelJid); + + QXmppTask requestBannedJids(const QString &channelJid); + QXmppTask banJid(const QString &channelJid, const QString &jid); + Q_SIGNAL void jidBanned(const QString &channelJid, const QString &jid); + + QXmppTask unbanJid(const QString &channelJid, const QString &jid); + Q_SIGNAL void jidUnbanned(const QString &channelJid, const QString &jid); + QXmppTask unbanAllJids(const QString &channelJid); + Q_SIGNAL void allJidsUnbanned(const QString &channelJid); + + QXmppTask requestParticipants(const QString &channelJid); + Q_SIGNAL void userJoinedOrParticipantModified(const QString &channelJid, const QXmppMixParticipantItem &participantItem); + Q_SIGNAL void participantLeft(const QString &channelJid, const QString &participantId); + + QXmppTask leaveChannel(const QString &channelJid); + + QXmppTask deleteChannel(const QString &channelJid); + Q_SIGNAL void channelDeleted(const QString &channelJid); + +protected: + /// \cond + void setClient(QXmppClient *client) override; + bool handleMessage(const QXmppMessage &message) override; + bool handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName) override; + /// \endcond + +private: + friend class tst_QXmppMixManager; + + QXmppMixIq prepareJoinIq(const QString &channelJid, const QString &nickname, QXmppMixIq::Nodes nodes); + QXmppTask joinChannel(QXmppMixIq &&iq); + QXmppTask sendInvitation(const QXmppMixInvitation &invitation, const QString &messageBody); + QXmppTask requestNodes(const QString &channelJid); + QXmppTask deleteNode(const QString &channelJid, const QString &node); + QXmppTask requestJids(const QString &channelJid, const QString &node); + QXmppTask addJidToNode(const QString &channelJid, const QString &node, const QString &jid); + + void handleDiscoInfo(const QXmppDiscoveryIq &iq); + + void setSupportedByServer(bool supportedByServer); + void setArchivingSupportedByServer(bool archivingSupportedByServer); + void addService(const Service &service); + void removeService(const QString &jid); + void removeServices(); + + QXmppPubSubManager *m_pubSubManager; + QXmppDiscoveryManager *m_discoveryManager; + bool m_supportedByServer = false; + bool m_archivingSupportedByServer = false; + QList m_services; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0213aeb0d..bce6ff117 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,6 +48,7 @@ add_simple_test(qxmppjinglemessageinitiationmanager) add_simple_test(qxmppmammanager) add_simple_test(qxmppmixinvitation) add_simple_test(qxmppmixitems) +add_simple_test(qxmppmixmanager TestClient.h) add_simple_test(qxmppmessage) add_simple_test(qxmppmessagereaction) add_simple_test(qxmppmessagereceiptmanager) diff --git a/tests/TestClient.h b/tests/TestClient.h index 9da12a972..6b939a4bd 100644 --- a/tests/TestClient.h +++ b/tests/TestClient.h @@ -46,6 +46,8 @@ class TestClient : public QXmppClient void expect(QString &&packet) { QVERIFY2(!m_sentPackets.empty(), "No packet was sent!"); + qDebug() << m_sentPackets.first(); + qDebug() << packet.replace(u'\'', u'"'); QCOMPARE(m_sentPackets.takeFirst(), packet.replace(u'\'', u'"')); resetIdCount(); } diff --git a/tests/qxmppmixiq/tst_qxmppmixiq.cpp b/tests/qxmppmixiq/tst_qxmppmixiq.cpp index 3ddf7ef77..18ec89ca2 100644 --- a/tests/qxmppmixiq/tst_qxmppmixiq.cpp +++ b/tests/qxmppmixiq/tst_qxmppmixiq.cpp @@ -9,6 +9,7 @@ #include Q_DECLARE_METATYPE(QXmppIq::Type) +Q_DECLARE_METATYPE(QXmppMixIq::Nodes) Q_DECLARE_METATYPE(QXmppMixIq::Type) class tst_QXmppMixIq : public QObject @@ -54,12 +55,10 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example\" " "from=\"hag66@shakespeare.example/UUID-a1j/7533\" " "type=\"set\">" - "" + "" "" - "" - "" - "" "" + "" "third witch" "" "hag66@shakespeare.example" @@ -76,10 +75,8 @@ void tst_QXmppMixIq::testBase_data() "from=\"hag66@shakespeare.example\" " "type=\"set\">" "" - "" - "" - "" "" + "" "stpeter" "" "hag66@shakespeare.example" @@ -94,11 +91,9 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example\" " "from=\"coven@mix.shakespeare.example\" " "type=\"result\">" - "" - "" - "" - "" + "" "" + "" "third witch" "" ""); @@ -107,13 +102,11 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example/UUID-a1j/7533\" " "from=\"hag66@shakespeare.example\" " "type=\"result\">" - "" + "" "" - "" - "" - "" + "id=\"123456\">" "" + "" "" "" ""); @@ -122,7 +115,7 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example\" " "from=\"hag66@shakespeare.example/UUID-a1j/7533\" " "type=\"set\">" - "" + "" "" "" ""); @@ -145,7 +138,7 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example/UUID-a1j/7533\" " "from=\"hag66@shakespeare.example\" " "type=\"result\">" - "" + "" "" "" ""); @@ -156,6 +149,7 @@ void tst_QXmppMixIq::testBase_data() "type=\"set\">" "" "" + "" "" ""); QByteArray updateSubscriptionResultXml( @@ -163,8 +157,9 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example/UUID-a1j/7533\" " "from=\"hag66@shakespeare.example\" " "type=\"result\">" - "" + "" "" + "" "" ""); QByteArray setNickSetXml( @@ -192,7 +187,7 @@ void tst_QXmppMixIq::testBase_data() "type=\"set\">" "" ""); - QByteArray createWithoutNameXml( + QByteArray createWithoutIdXml( ""); - QStringList emptyNodes; - QStringList defaultNodes; - defaultNodes << "urn:xmpp:mix:nodes:messages" - << "urn:xmpp:mix:nodes:presence" - << "urn:xmpp:mix:nodes:participants" - << "urn:xmpp:mix:nodes:info"; + QStringList emptyNodeList; + QStringList nodeList = { "urn:xmpp:mix:nodes:info", "urn:xmpp:mix:nodes:messages" }; + QXmppMixIq::Nodes nodesBeingSubscribedTo = { QXmppMixIq::Node::Information | QXmppMixIq::Node::Messages }; + QXmppMixIq::Nodes noNodes; + QXmppMixIq::Nodes nodesBeingUnsubscribedFrom = { QXmppMixIq::Node::Information | QXmppMixIq::Node::Configuration }; QTest::addColumn("xml"); QTest::addColumn("type"); QTest::addColumn("actionType"); QTest::addColumn("jid"); QTest::addColumn("channelName"); + QTest::addColumn("participantId"); + QTest::addColumn("channelId"); + QTest::addColumn("channelJid"); QTest::addColumn("nodes"); + QTest::addColumn("nodesBeingSubscribedTo"); + QTest::addColumn("nodesBeingUnsubscribedFrom"); QTest::addColumn("nick"); QTest::addColumn("inviteeJid"); QTest::addColumn("invitationToken"); @@ -235,7 +234,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::InvitationRequest << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << QStringLiteral("cat@shakespeare.example") << ""; @@ -245,7 +249,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::InvitationResponse << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << QStringLiteral("ABCDEF"); @@ -255,7 +264,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::ClientJoin << "coven@mix.shakespeare.example" << "" - << defaultNodes + << "" + << "" + << "coven@mix.shakespeare.example" + << nodeList + << nodesBeingSubscribedTo + << noNodes << "third witch" << "" << QStringLiteral("ABCDEF"); @@ -265,7 +279,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Join << "" << "" - << defaultNodes + << "" + << "" + << "" + << nodeList + << nodesBeingSubscribedTo + << noNodes << "stpeter" << "" << QStringLiteral("ABCDEF"); @@ -273,9 +292,14 @@ void tst_QXmppMixIq::testBase_data() << joinS2sResultXml << QXmppIq::Result << QXmppMixIq::Join - << "123456#coven@mix.shakespeare.example" << "" - << defaultNodes + << "" + << "123456" + << "" + << "" + << nodeList + << nodesBeingSubscribedTo + << noNodes << "third witch" << "" << ""; @@ -283,9 +307,14 @@ void tst_QXmppMixIq::testBase_data() << joinC2sResultXml << QXmppIq::Result << QXmppMixIq::ClientJoin - << "123456#coven@mix.shakespeare.example" << "" - << defaultNodes + << "" + << "123456" + << "" + << "" + << nodeList + << nodesBeingSubscribedTo + << noNodes << "" << "" << ""; @@ -295,7 +324,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::ClientLeave << "coven@mix.shakespeare.example" << "" - << emptyNodes + << "" + << "" + << "coven@mix.shakespeare.example" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -305,7 +339,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Leave << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -315,7 +354,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Leave << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -325,7 +369,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::ClientLeave << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -335,7 +384,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::UpdateSubscription << "" << "" - << (QStringList() << "urn:xmpp:mix:nodes:messages") + << "" + << "" + << "" + << QStringList { "urn:xmpp:mix:nodes:messages" } + << QXmppMixIq::Nodes { QXmppMixIq::Node::Messages } + << QXmppMixIq::Nodes { QXmppMixIq::Node::Configuration } << "" << "" << ""; @@ -343,9 +397,14 @@ void tst_QXmppMixIq::testBase_data() << updateSubscriptionResultXml << QXmppIq::Result << QXmppMixIq::UpdateSubscription - << "hag66@shakespeare.example" << "" - << (QStringList() << "urn:xmpp:mix:nodes:messages") + << "" + << "" + << "" + << "" + << QStringList { "urn:xmpp:mix:nodes:messages" } + << QXmppMixIq::Nodes { QXmppMixIq::Node::Messages } + << QXmppMixIq::Nodes { QXmppMixIq::Node::Configuration } << "" << "" << ""; @@ -355,7 +414,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::SetNick << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "thirdwitch" << "" << ""; @@ -365,7 +429,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::SetNick << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "thirdwitch" << "" << ""; @@ -375,17 +444,27 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Create << "" << "coven" - << emptyNodes + << "" + << "coven" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; - QTest::newRow("create-without-name") - << createWithoutNameXml + QTest::newRow("create-without-id") + << createWithoutIdXml << QXmppIq::Set << QXmppMixIq::Create << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -395,7 +474,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Destroy << "" << "coven" - << emptyNodes + << "" + << "coven" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -405,7 +489,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::None << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -418,7 +507,12 @@ void tst_QXmppMixIq::testBase() QFETCH(QXmppMixIq::Type, actionType); QFETCH(QString, jid); QFETCH(QString, channelName); + QFETCH(QString, participantId); + QFETCH(QString, channelId); + QFETCH(QString, channelJid); QFETCH(QStringList, nodes); + QFETCH(QXmppMixIq::Nodes, nodesBeingSubscribedTo); + QFETCH(QXmppMixIq::Nodes, nodesBeingUnsubscribedFrom); QFETCH(QString, nick); QFETCH(QString, inviteeJid); QFETCH(QString, invitationToken); @@ -429,7 +523,12 @@ void tst_QXmppMixIq::testBase() QCOMPARE(iq.actionType(), actionType); QCOMPARE(iq.jid(), jid); QCOMPARE(iq.channelName(), channelName); + QCOMPARE(iq.participantId(), participantId); + QCOMPARE(iq.channelId(), channelId); + QCOMPARE(iq.channelJid(), channelJid); QCOMPARE(iq.nodes(), nodes); + QCOMPARE(iq.nodesBeingSubscribedTo(), nodesBeingSubscribedTo); + QCOMPARE(iq.nodesBeingUnsubscribedFrom(), nodesBeingUnsubscribedFrom); QCOMPARE(iq.nick(), nick); QCOMPARE(iq.inviteeJid(), inviteeJid); QCOMPARE(iq.invitation().has_value(), !invitationToken.isEmpty()); @@ -445,7 +544,12 @@ void tst_QXmppMixIq::testDefaults() QCOMPARE(iq.actionType(), QXmppMixIq::None); QCOMPARE(iq.jid(), QString()); QCOMPARE(iq.channelName(), QString()); + QCOMPARE(iq.participantId(), QString()); + QCOMPARE(iq.channelId(), QString()); + QCOMPARE(iq.channelJid(), QString()); QCOMPARE(iq.nodes(), QStringList()); + QVERIFY(iq.nodesBeingSubscribedTo().testFlag(QXmppMixIq::Node::None)); + QVERIFY(iq.nodesBeingUnsubscribedFrom().testFlag(QXmppMixIq::Node::None)); QCOMPARE(iq.nick(), QString()); QVERIFY(iq.inviteeJid().isEmpty()); QVERIFY(!iq.invitation()); @@ -454,14 +558,34 @@ void tst_QXmppMixIq::testDefaults() void tst_QXmppMixIq::testSetters() { QXmppMixIq iq; + iq.setActionType(QXmppMixIq::Join); QCOMPARE(iq.actionType(), QXmppMixIq::Join); + iq.setJid("interestingnews@mix.example.com"); QCOMPARE(iq.jid(), QString("interestingnews@mix.example.com")); + iq.setChannelName("interestingnews"); QCOMPARE(iq.channelName(), QString("interestingnews")); - iq.setNodes(QStringList() << "com:example:mix:node:custom"); - QCOMPARE(iq.nodes(), QStringList() << "com:example:mix:node:custom"); + + iq.setParticipantId("123456"); + QCOMPARE(iq.participantId(), "123456"); + + iq.setChannelId("coven"); + QCOMPARE(iq.channelId(), "coven"); + + iq.setChannelJid("coven@mix.shakespeare.example"); + QCOMPARE(iq.channelJid(), "coven@mix.shakespeare.example"); + + iq.setNodes(QStringList() << "urn:xmpp:mix:nodes:info"); + QCOMPARE(iq.nodes(), QStringList() << "urn:xmpp:mix:nodes:info"); + + iq.setNodesBeingSubscribedTo(QXmppMixIq::Node::AllowedJids | QXmppMixIq::Node::BannedJids); + QCOMPARE(iq.nodesBeingSubscribedTo(), QXmppMixIq::Node::AllowedJids | QXmppMixIq::Node::BannedJids); + + iq.setNodesBeingUnsubscribedFrom(QXmppMixIq::Node::Information | QXmppMixIq::Node::Configuration); + QCOMPARE(iq.nodesBeingUnsubscribedFrom(), QXmppMixIq::Node::Information | QXmppMixIq::Node::Configuration); + iq.setNick("SMUDO"); QCOMPARE(iq.nick(), QString("SMUDO")); @@ -501,7 +625,7 @@ void tst_QXmppMixIq::testIsMixIq() "to=\"hag66@shakespeare.example\" " "from=\"hag66@shakespeare.example/UUID-a1j/7533\" " "type=\"set\">" - "" + "" "" "" ""); diff --git a/tests/qxmppmixitems/tst_qxmppmixitems.cpp b/tests/qxmppmixitems/tst_qxmppmixitems.cpp index 529dfedcc..7eaa68791 100644 --- a/tests/qxmppmixitems/tst_qxmppmixitems.cpp +++ b/tests/qxmppmixitems/tst_qxmppmixitems.cpp @@ -43,6 +43,7 @@ void tst_QXmppMixItem::testInfo() QXmppMixInfoItem item; parsePacket(item, xml); + QCOMPARE(item.formType(), QXmppDataForm::Result); QCOMPARE(item.name(), QString("Witches Coven")); QCOMPARE(item.description(), QString("A location not far from the blasted " "heath where the three witches meet")); @@ -52,6 +53,8 @@ void tst_QXmppMixItem::testInfo() serializePacket(item, xml); // test setters + item.setFormType(QXmppDataForm::Submit); + QCOMPARE(item.formType(), QXmppDataForm::Submit); item.setName("Skynet Development"); QCOMPARE(item.name(), QString("Skynet Development")); item.setDescription("Very cool development group."); diff --git a/tests/qxmppmixmanager/tst_qxmppmixmanager.cpp b/tests/qxmppmixmanager/tst_qxmppmixmanager.cpp new file mode 100644 index 000000000..b0081ee15 --- /dev/null +++ b/tests/qxmppmixmanager/tst_qxmppmixmanager.cpp @@ -0,0 +1,1785 @@ +// SPDX-FileCopyrightText: 2023 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppMixInfoItem.h" +#include "QXmppMixInvitation.h" +#include "QXmppMixManager.h" +#include "QXmppPubSubEvent.h" +#include "QXmppPubSubManager.h" + +#include "TestClient.h" + +struct Tester +{ + Tester() { } + + Tester(const QString &jid) + { + client.configuration().setJid(jid); + } + + TestClient client; + QXmppMixManager *manager = client.addNewExtension(); +}; + +struct MessageTester +{ + MessageTester() + { + client.logger()->setLoggingType(QXmppLogger::SignalLogging); + } + + MessageTester(const QString &jid) + : MessageTester() + { + client.configuration().setJid(jid); + } + + QXmppClient client; + QXmppMixManager *manager = client.addNewExtension(); +}; + +class tst_QXmppMixManager : public QObject +{ + Q_OBJECT + +private: + Q_SLOT void testDiscoveryFeatures(); + Q_SLOT void testSupportedByServer(); + Q_SLOT void testArchivingSupportedByServer(); + Q_SLOT void testService(); + Q_SLOT void testServices(); + Q_SLOT void testHandleDiscoInfo(); + Q_SLOT void testAddJidToNode(); + Q_SLOT void testRequestJids(); + Q_SLOT void testDeleteNode(); + Q_SLOT void testRequestNodes(); + Q_SLOT void testSendInvitationPrivate(); + Q_SLOT void testJoinChannelPrivate(); + Q_SLOT void testPrepareJoinIq(); + Q_SLOT void testHandlePubSubEvent(); + Q_SLOT void testHandleMessage(); + Q_SLOT void testSetClient(); + Q_SLOT void testRequestChannelJids(); + Q_SLOT void testRequestChannelInformation(); + Q_SLOT void testUpdateChannelInformation(); + Q_SLOT void testCreatePrivateChannel(); + Q_SLOT void testCreatePublicChannel(); + Q_SLOT void testIsChannelPublic(); + Q_SLOT void testMakeChannelPrivate(); + Q_SLOT void testJoinChannel(); + Q_SLOT void testJoinChannelWithNickname(); + Q_SLOT void testJoinChannelWithNodes(); + Q_SLOT void testSendInvitationPrivateWithBody(); + Q_SLOT void testSendInvitation(); + Q_SLOT void testSendInvitationWithBody(); + Q_SLOT void testInvite(); + Q_SLOT void testAcceptInvitation(); + Q_SLOT void testAcceptInvitationWithNickname(); + Q_SLOT void testAcceptInvitationWithNodes(); + Q_SLOT void testUpdateNickname(); + Q_SLOT void testUpdateSubscriptions(); + Q_SLOT void testRequestAllowedJids(); + Q_SLOT void testAllowJid(); + Q_SLOT void testAllowAllJids(); + Q_SLOT void testDisallowJid(); + Q_SLOT void testDisallowAllJids(); + Q_SLOT void testRequestBannedJids(); + Q_SLOT void testBanJid(); + Q_SLOT void testUnbanJid(); + Q_SLOT void testUnbanAllJids(); + Q_SLOT void testRequestParticipants(); + Q_SLOT void testLeaveChannel(); + Q_SLOT void testDeleteChannel(); + + template + void testErrorFromChannel(QXmppTask &task, TestClient &client); + template + void testErrorFromChannel(QXmppTask &task, TestClient &client, const QString &id); + template + void testError(QXmppTask &task, TestClient &client, const QString &id, const QString &from); +}; + +void tst_QXmppMixManager::testDiscoveryFeatures() +{ + QXmppMixManager manager; + QCOMPARE(manager.discoveryFeatures(), QStringList { "urn:xmpp:mix:core:1" }); +} + +void tst_QXmppMixManager::testSupportedByServer() +{ + QXmppMixManager manager; + QSignalSpy spy(&manager, &QXmppMixManager::supportedByServerChanged); + + QVERIFY(!manager.supportedByServer()); + manager.setSupportedByServer(true); + QVERIFY(manager.supportedByServer()); + QCOMPARE(spy.size(), 1); +} + +void tst_QXmppMixManager::testArchivingSupportedByServer() +{ + QXmppMixManager manager; + QSignalSpy spy(&manager, &QXmppMixManager::archivingSupportedByServerChanged); + + QVERIFY(!manager.archivingSupportedByServer()); + manager.setArchivingSupportedByServer(true); + QVERIFY(manager.archivingSupportedByServer()); + QCOMPARE(spy.size(), 1); +} + +void tst_QXmppMixManager::testService() +{ + QXmppMixManager::Service service1; + + QVERIFY(service1.jid.isEmpty()); + QVERIFY(!service1.channelsSearchable); + QVERIFY(!service1.channelCreationAllowed); + + service1.jid = QStringLiteral("mix.shakespeare.example"); + service1.channelsSearchable = true; + service1.channelCreationAllowed = false; + + QXmppMixManager::Service service2; + service2.jid = QStringLiteral("mix.shakespeare.example"); + service2.channelsSearchable = true; + service2.channelCreationAllowed = false; + + QCOMPARE(service1, service2); + + QXmppMixManager::Service service3; + service3.jid = QStringLiteral("mix.shakespeare.example"); + service3.channelsSearchable = true; + service3.channelCreationAllowed = true; + + QVERIFY(!(service1 == service3)); +} + +void tst_QXmppMixManager::testServices() +{ + QXmppMixManager manager; + QSignalSpy spy(&manager, &QXmppMixManager::servicesChanged); + + QXmppMixManager::Service service; + service.jid = QStringLiteral("mix.shakespeare.example"); + + QVERIFY(manager.services().isEmpty()); + + manager.addService(service); + QCOMPARE(manager.services().size(), 1); + QCOMPARE(manager.services().at(0).jid, service.jid); + manager.addService(service); + QCOMPARE(spy.size(), 1); + + manager.removeService(QStringLiteral("mix1.shakespeare.example")); + QCOMPARE(manager.services().size(), 1); + QCOMPARE(spy.size(), 1); + + manager.removeService(service.jid); + QVERIFY(manager.services().isEmpty()); + QCOMPARE(spy.size(), 2); + + manager.addService(service); + service.channelsSearchable = true; + manager.addService(service); + QCOMPARE(manager.services().size(), 1); + QCOMPARE(manager.services().at(0).jid, service.jid); + QCOMPARE(manager.services().at(0).channelsSearchable, service.channelsSearchable); + QCOMPARE(spy.size(), 4); + + service.jid = QStringLiteral("mix1.shakespeare.example"); + manager.addService(service); + manager.removeServices(); + QVERIFY(manager.services().isEmpty()); + QCOMPARE(spy.size(), 6); +} + +void tst_QXmppMixManager::testHandleDiscoInfo() +{ + auto [client, manager] = MessageTester(QStringLiteral("hag66@shakespeare.example")); + + QXmppDiscoveryIq::Identity identity; + identity.setCategory(QStringLiteral("conference")); + identity.setType(QStringLiteral("mix")); + + QXmppDiscoveryIq iq; + iq.setFeatures({ QStringLiteral("urn:xmpp:mix:pam:2"), + QStringLiteral("urn:xmpp:mix:pam:2#archive"), + QStringLiteral("urn:xmpp:mix:core:1"), + QStringLiteral("urn:xmpp:mix:core:1#searchable"), + QStringLiteral("urn:xmpp:mix:core:1#create-channel") }); + iq.setIdentities({ identity }); + + manager->handleDiscoInfo(iq); + + QVERIFY(manager->supportedByServer()); + QVERIFY(manager->archivingSupportedByServer()); + QCOMPARE(manager->services().at(0).jid, QStringLiteral("shakespeare.example")); + QVERIFY(manager->services().at(0).channelsSearchable); + QVERIFY(manager->services().at(0).channelCreationAllowed); + + iq.setFeatures({}); + iq.setIdentities({}); + + manager->handleDiscoInfo(iq); + + QVERIFY(!manager->supportedByServer()); + QVERIFY(!manager->archivingSupportedByServer()); + QVERIFY(manager->services().isEmpty()); +} + +void tst_QXmppMixManager::testAddJidToNode() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->addJidToNode(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("urn:xmpp:mix:nodes:allowed"), QStringLiteral("alice@wonderland.example")); + }; + + auto expect = [&client](const QString &id) { + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "") + .arg(id)); + }; + + auto inject = [&client](const QString &id) { + client.inject(QStringLiteral("").arg(id)); + }; + + auto task = call(); + + expect(QStringLiteral("qxmpp1")); + inject(QStringLiteral("qxmpp1")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); + + // TODO: Fix following test cases ("expect(QStringLiteral("qxmpp3"))" results in a stanza with a UUID as its ID instead of "qxmpp3") +// task = call(); + +// expect(QStringLiteral("qxmpp1")); +// client.inject(QStringLiteral("" +// "" +// "" +// "" +// "")); +// client.expect(QStringLiteral("" +// "" +// "" +// "" +// "")); +// client.inject(QStringLiteral("" +// "" +// "" +// "" +// "")); +// expect(QStringLiteral("qxmpp3")); +// inject(QStringLiteral("qxmpp3")); + +// expectFutureVariant(task); + +// task = call(); + +// expect(QStringLiteral("qxmpp1")); +// client.inject(QStringLiteral("" +// "" +// "" +// "" +// "")); + +// testErrorFromChannel(task = call(), client, QStringLiteral("qxmpp2")); + +// task = call(); + +// expect(QStringLiteral("qxmpp1")); +// client.inject(QStringLiteral("" +// "" +// "" +// "" +// "")); +// client.expect(QStringLiteral("" +// "" +// "" +// "" +// "")); +// client.inject(QStringLiteral("" +// "" +// "" +// "" +// "")); + + // testErrorFromChannel(task = call(), client, QStringLiteral("qxmpp3")); +} + +void tst_QXmppMixManager::testRequestJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestJids(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("urn:xmpp:mix:nodes:allowed")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto jids = expectFutureVariant>(task); + QCOMPARE(jids.at(0), QStringLiteral("shakespeare.example")); + QCOMPARE(jids.at(1), QStringLiteral("alice@wonderland.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testDeleteNode() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->deleteNode(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("urn:xmpp:mix:nodes:allowed")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + task = call(); + + client.ignore(); + client.inject(QStringLiteral("" + "" + "" + "" + "")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestNodes() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestNodes(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "")); + + auto nodes = expectFutureVariant>(task); + QCOMPARE(nodes.size(), 2); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testSendInvitationPrivate() +{ + auto [client, manager] = MessageTester(); + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + *messageSent = true; + + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + QCOMPARE(message.to(), QStringLiteral("cat@shakespeare.example")); + QCOMPARE(message.type(), QXmppMessage::Chat); + QVERIFY(message.hasHint(QXmppMessage::Store)); + QVERIFY(message.body().isEmpty()); + QVERIFY(message.mixInvitation()); + QCOMPARE(message.mixInvitation()->inviteeJid(), QStringLiteral("cat@shakespeare.example")); + } + }); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + manager->sendInvitation(invitation, {}); + + QVERIFY(messageSent); +} + +void tst_QXmppMixManager::testJoinChannelPrivate() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(QStringLiteral("hag66@shakespeare.example")); + iq.setActionType(QXmppMixIq::ClientJoin); + iq.setChannelJid(invitation.channelJid()); + iq.setNick(QStringLiteral("third witch")); + iq.setNodesBeingSubscribedTo(QXmppMixIq::Node::AllowedJids | QXmppMixIq::Node::BannedJids); + iq.setInvitation(invitation); + + return manager->joinChannel(std::move(iq)); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "third witch" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "third witch 2" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123456")); + QCOMPARE(result.nickname, QStringLiteral("third witch 2")); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixIq::Node::AllowedJids); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("hag66@shakespeare.example")); +} + +void tst_QXmppMixManager::testPrepareJoinIq() +{ + auto [client, manager] = Tester(QStringLiteral("hag66@shakespeare.example")); + auto iq = manager->prepareJoinIq(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("third witch"), QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); + + QCOMPARE(iq.type(), QXmppIq::Set); + QCOMPARE(iq.to(), QStringLiteral("hag66@shakespeare.example")); + QCOMPARE(iq.actionType(), QXmppMixIq::ClientJoin); + QCOMPARE(iq.channelJid(), QStringLiteral("coven@mix.shakespeare.example")); + QCOMPARE(iq.nick(), QStringLiteral("third witch")); + QCOMPARE(iq.nodesBeingSubscribedTo(), QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); +} + +void tst_QXmppMixManager::testHandlePubSubEvent() +{ + QXmppMixManager manager; + QSignalSpy jidAllowedSpy(&manager, &QXmppMixManager::jidAllowed); + QSignalSpy allJidsAllowedSpy(&manager, &QXmppMixManager::allJidsAllowed); + QSignalSpy jidDisallowedSpy(&manager, &QXmppMixManager::jidDisallowed); + QSignalSpy allJidsDisallowedSpy(&manager, &QXmppMixManager::allJidsDisallowed); + QSignalSpy jidBannedSpy(&manager, &QXmppMixManager::jidBanned); + QSignalSpy jidUnbannedSpy(&manager, &QXmppMixManager::jidUnbanned); + QSignalSpy allJidsUnbannedSpy(&manager, &QXmppMixManager::allJidsUnbanned); + + QSignalSpy userJoinedOrParticipantModifiedSpy(&manager, &QXmppMixManager::userJoinedOrParticipantModified); + QSignalSpy participantLeftSpy(&manager, &QXmppMixManager::participantLeft); + QSignalSpy channelDeletedSpy(&manager, &QXmppMixManager::channelDeleted); + + const QString channelJid = QStringLiteral("coven@mix.shakespeare.example"); + const QStringList nodes = { QStringLiteral("urn:xmpp:mix:nodes:allowed"), QStringLiteral("urn:xmpp:mix:nodes:banned") }; + const auto participantNode = QStringLiteral("urn:xmpp:mix:nodes:participants"); + const QStringList jids = { QStringLiteral("hag66@shakespeare.example"), QStringLiteral("cat@shakespeare.example") }; + + QXmppPubSubBaseItem item1; + item1.setId(jids.at(0)); + + QXmppPubSubBaseItem item2; + item2.setId(jids.at(1)); + + QXmppPubSubEvent event1; + event1.setItems({ item1, item2 }); + event1.setRetractIds(jids); + + for (const auto &node : nodes) { + event1.setEventType(QXmppPubSubEventBase::Items); + manager.handlePubSubEvent(writePacketToDom(event1), channelJid, node); + + event1.setEventType(QXmppPubSubEventBase::Retract); + manager.handlePubSubEvent(writePacketToDom(event1), channelJid, node); + + event1.setEventType(QXmppPubSubEventBase::Purge); + manager.handlePubSubEvent(writePacketToDom(event1), channelJid, node); + + event1.setEventType(QXmppPubSubEventBase::Delete); + manager.handlePubSubEvent(writePacketToDom(event1), channelJid, node); + + event1.setEventType(QXmppPubSubEventBase::Configuration); + manager.handlePubSubEvent(writePacketToDom(event1), channelJid, node); + + event1.setEventType(QXmppPubSubEventBase::Subscription); + manager.handlePubSubEvent(writePacketToDom(event1), channelJid, node); + } + + QXmppMixParticipantItem item3; + item3.setJid(jids.at(0)); + + QXmppMixParticipantItem item4; + item4.setJid(jids.at(1)); + + QXmppPubSubEvent event2; + event2.setItems({ item3, item4 }); + event2.setRetractIds(jids); + + event2.setEventType(QXmppPubSubEventBase::Items); + manager.handlePubSubEvent(writePacketToDom(event2), channelJid, participantNode); + + event2.setEventType(QXmppPubSubEventBase::Retract); + manager.handlePubSubEvent(writePacketToDom(event2), channelJid, participantNode); + + event2.setEventType(QXmppPubSubEventBase::Purge); + manager.handlePubSubEvent(writePacketToDom(event2), channelJid, participantNode); + + event2.setEventType(QXmppPubSubEventBase::Delete); + manager.handlePubSubEvent(writePacketToDom(event2), channelJid, participantNode); + + event2.setEventType(QXmppPubSubEventBase::Configuration); + manager.handlePubSubEvent(writePacketToDom(event2), channelJid, participantNode); + + event2.setEventType(QXmppPubSubEventBase::Subscription); + manager.handlePubSubEvent(writePacketToDom(event2), channelJid, participantNode); + + for (const auto &spy : { &jidAllowedSpy, &jidDisallowedSpy, &jidBannedSpy, &jidUnbannedSpy, &participantLeftSpy }) { + QCOMPARE(spy->size(), 2); + + for (auto i = 0; i < spy->size(); i++) { + const auto &arguments = spy->at(i); + QCOMPARE(arguments.at(0).toString(), channelJid); + QCOMPARE(arguments.at(1).toString(), jids.at(i)); + } + } + + for (const auto &spy : { &allJidsAllowedSpy, &allJidsDisallowedSpy }) { + QCOMPARE(spy->size(), 1); + auto arguments = spy->first(); + QCOMPARE(arguments.at(0).toString(), channelJid); + } + + for (const auto &spy : { &allJidsUnbannedSpy, &channelDeletedSpy }) { + QCOMPARE(spy->size(), 2); + for (const auto &arguments : *spy) { + QCOMPARE(arguments.at(0).toString(), channelJid); + } + } + + QCOMPARE(userJoinedOrParticipantModifiedSpy.size(), 2); + for (auto i = 0; i < userJoinedOrParticipantModifiedSpy.size(); i++) { + const auto &arguments = userJoinedOrParticipantModifiedSpy.at(i); + QCOMPARE(arguments.at(0).toString(), channelJid); + QCOMPARE(arguments.at(1).value().jid(), event2.items().at(i).jid()); + } +} + +void tst_QXmppMixManager::testHandleMessage() +{ + QXmppMixManager manager; + + const QObject context; + auto invitationEmitted = std::make_shared(false); + + connect(&manager, &QXmppMixManager::invited, &context, [invitationEmitted](const QXmppMixInvitation &invitation) { + *invitationEmitted = true; + + QCOMPARE(invitation.inviterJid(), QStringLiteral("hag66@shakespeare.example")); + QCOMPARE(invitation.inviteeJid(), QStringLiteral("cat@shakespeare.example")); + QCOMPARE(invitation.channelJid(), QStringLiteral("coven@mix.shakespeare.example")); + QCOMPARE(invitation.token(), QStringLiteral("ABCDEF")); + }); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + QXmppMessage message; + QVERIFY(!manager.handleMessage(message)); + + message.setMixInvitation(invitation); + QVERIFY(manager.handleMessage(message)); + QVERIFY(*invitationEmitted); +} + +void tst_QXmppMixManager::testSetClient() +{ + QXmppClient client; + QXmppMixManager manager; + + client.configuration().setJid(QStringLiteral("hag66@shakespeare.example")); + manager.setClient(&client); + + QXmppMixManager::Service service; + service.jid = QStringLiteral("mix.shakespeare.example"); + service.channelsSearchable = true; + service.channelCreationAllowed = false; + + manager.setSupportedByServer(true); + manager.setArchivingSupportedByServer(true); + manager.addService(service); + + Q_EMIT client.disconnected(); + QVERIFY(!manager.supportedByServer()); + QVERIFY(!manager.archivingSupportedByServer()); + QVERIFY(manager.services().isEmpty()); + + QVERIFY(client.findExtension()); + QVERIFY(manager.m_discoveryManager); + + QXmppDiscoveryIq iq; + iq.setFeatures({ QStringLiteral("urn:xmpp:mix:pam:2") }); + Q_EMIT manager.m_discoveryManager->infoReceived(iq); + QVERIFY(manager.supportedByServer()); + + QVERIFY(client.findExtension()); + QVERIFY(manager.m_pubSubManager); +} + +void tst_QXmppMixManager::testRequestChannelJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestChannelJids(QStringLiteral("mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + + auto jids = expectFutureVariant>(task); + QCOMPARE(jids.size(), 3); + QCOMPARE(jids.at(0), QStringLiteral("coven@mix.shakespeare.example")); + QCOMPARE(jids.at(1), QStringLiteral("spells@mix.shakespeare.example")); + QCOMPARE(jids.at(2), QStringLiteral("wizards@mix.shakespeare.example")); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("mix.shakespeare.example")); +} + +void tst_QXmppMixManager::testRequestChannelInformation() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->requestChannelInformation(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "urn:xmpp:mix:core:1" + "" + "" + "Witches Coven" + "" + "" + "A location not far from the blasted heath where the three witches meet" + "" + "" + "greymalkin@shakespeare.example" + "" + "" + "" + "" + "" + "")); + + auto information = expectFutureVariant(task); + QCOMPARE(information.name(), QStringLiteral("Witches Coven")); + QCOMPARE(information.description(), QStringLiteral("A location not far from the blasted heath where the three witches meet")); + QCOMPARE(information.contactJids(), QStringList { QStringLiteral("greymalkin@shakespeare.example") }); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUpdateChannelInformation() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + QXmppMixInfoItem information; + information.setId(QStringLiteral("2016-05-30T09:00:00")); + information.setName(QStringLiteral("The Coven")); + information.setDescription(QStringLiteral("A location not far from the blasted heath where the witches meet")); + information.setContactJids({ QStringLiteral("greymalkin1@shakespeare.example") }); + + auto call = [manager, information]() { + return manager->updateChannelInformation(QStringLiteral("coven@mix.shakespeare.example"), information); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "urn:xmpp:mix:core:1" + "" + "" + "The Coven" + "" + "" + "A location not far from the blasted heath where the witches meet" + "" + "" + "greymalkin1@shakespeare.example" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testCreatePrivateChannel() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->createPrivateChannel(QStringLiteral("mix.shakespeare.example")); + }; + + auto task = call(); + + client.inject(QStringLiteral("" + "" + "")); + client.expect(QStringLiteral("" + "" + "")); + + auto channelJid = expectFutureVariant(task); + QCOMPARE(channelJid, QStringLiteral("A1B2C345@mix.shakespeare.example")); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("mix.shakespeare.example")); +} + +void tst_QXmppMixManager::testCreatePublicChannel() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->createPublicChannel(QStringLiteral("mix.shakespeare.example"), QStringLiteral("coven")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("" + "" + "")); + + auto channelJid = expectFutureVariant(task); + QCOMPARE(channelJid, QStringLiteral("coven@mix.shakespeare.example")); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("mix.shakespeare.example")); +} + +void tst_QXmppMixManager::testIsChannelPublic() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->isChannelPublic(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "")); + + auto isChannelPublic = expectFutureVariant(task); + QVERIFY(isChannelPublic); + + task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "")); + + isChannelPublic = expectFutureVariant(task); + QVERIFY(!isChannelPublic); + + task = call(); + + client.ignore(); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "")); + + isChannelPublic = expectFutureVariant(task); + QVERIFY(!isChannelPublic); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testMakeChannelPrivate() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->makeChannelPrivate(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testJoinChannel() +{ + auto tester = Tester(QStringLiteral("hag66@shakespeare.example")); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->joinChannel(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123456")); + QVERIFY(result.nickname.isEmpty()); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("hag66@shakespeare.example")); +} + +void tst_QXmppMixManager::testJoinChannelWithNickname() +{ + auto [client, manager] = Tester(QStringLiteral("hag66@shakespeare.example")); + + auto task = manager->joinChannel(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("third witch")); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "third witch" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "third witch" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123456")); + QCOMPARE(result.nickname, QStringLiteral("third witch")); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); +} + +void tst_QXmppMixManager::testJoinChannelWithNodes() +{ + auto [client, manager] = Tester(QStringLiteral("hag66@shakespeare.example")); + + auto task = manager->joinChannel(QStringLiteral("coven@mix.shakespeare.example"), {}, QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, "123456"); + QVERIFY(result.nickname.isEmpty()); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); +} + +void tst_QXmppMixManager::testSendInvitationPrivateWithBody() +{ + auto [client, manager] = MessageTester(QStringLiteral("hag66@shakespeare.example")); + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + *messageSent = true; + + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + QCOMPARE(message.to(), QStringLiteral("cat@shakespeare.example")); + QVERIFY(!message.hasHint(QXmppMessage::Store)); + QCOMPARE(message.body(), QStringLiteral("Would you like to join the coven?")); + QVERIFY(message.mixInvitation()); + QCOMPARE(message.mixInvitation()->token(), QStringLiteral("ABCDEF")); + } + }); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + manager->sendInvitation(invitation, QStringLiteral("Would you like to join the coven?")); + + QVERIFY(messageSent); +} + +void tst_QXmppMixManager::testSendInvitation() +{ + auto [client, manager] = MessageTester(QStringLiteral("hag66@shakespeare.example")); + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + *messageSent = true; + + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + QVERIFY(message.body().isEmpty()); + QVERIFY(message.mixInvitation()); + QCOMPARE(message.mixInvitation()->inviterJid(), QStringLiteral("hag66@shakespeare.example")); + QCOMPARE(message.mixInvitation()->inviteeJid(), QStringLiteral("cat@shakespeare.example")); + QCOMPARE(message.mixInvitation()->channelJid(), QStringLiteral("coven@mix.shakespeare.example")); + QVERIFY(message.mixInvitation()->token().isEmpty()); + } + }); + + manager->sendInvitation(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("cat@shakespeare.example")); + + QVERIFY(messageSent); +} + +void tst_QXmppMixManager::testSendInvitationWithBody() +{ + auto [client, manager] = MessageTester(QStringLiteral("hag66@shakespeare.example")); + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + *messageSent = true; + + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + QCOMPARE(message.body(), QStringLiteral("Would you like to join the coven?")); + } + }); + + manager->sendInvitation(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("cat@shakespeare.example"), QStringLiteral("Would you like to join the coven?")); + + QVERIFY(messageSent); +} + +void tst_QXmppMixManager::testInvite() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + auto call = [&client, manager]() { + return manager->invite(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("cat@shakespeare.example")); + }; + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + // Ignore stream management stanzas enabled by Tester. + if (message.mixInvitation()) { + *messageSent = true; + + QCOMPARE(message.to(), QStringLiteral("cat@shakespeare.example")); + QCOMPARE(message.type(), QXmppMessage::Chat); + QVERIFY(message.hasHint(QXmppMessage::Store)); + QVERIFY(message.body().isEmpty()); + QVERIFY(message.mixInvitation()); + QCOMPARE(message.mixInvitation()->token(), QStringLiteral("ABCDEF")); + } + } + }); + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "cat@shakespeare.example" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "")); + + QVERIFY(*messageSent); + + // TODO: Find a way such that the following line succeeds. + // expectFutureVariant(task); + + // TODO: Fix error parsing in QXmppStream::handleIqResponse() to make sendIq() return QXmppError instead of returning stanza as QDomElement + // testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testAcceptInvitation() +{ + auto tester = Tester(QStringLiteral("cat@shakespeare.example")); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + return manager->acceptInvitation(invitation); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123457")); + QVERIFY(result.nickname.isEmpty()); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("cat@shakespeare.example")); +} + +void tst_QXmppMixManager::testAcceptInvitationWithNickname() +{ + auto [client, manager] = Tester(QStringLiteral("cat@shakespeare.example")); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + auto task = manager->acceptInvitation(invitation, QStringLiteral("fourth witch")); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "fourth witch" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "fourth witch" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123457")); + QCOMPARE(result.nickname, QStringLiteral("fourth witch")); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); +} + +void tst_QXmppMixManager::testAcceptInvitationWithNodes() +{ + auto [client, manager] = Tester(QStringLiteral("cat@shakespeare.example")); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + auto task = manager->acceptInvitation(invitation, {}, QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, "123457"); + QVERIFY(result.nickname.isEmpty()); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); +} + +void tst_QXmppMixManager::testUpdateNickname() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->updateNickname(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("third witch")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "third witch" + "" + "")); + client.inject(QStringLiteral("" + "" + "third witch 2" + "" + "")); + + auto nickname = expectFutureVariant(task); + QCOMPARE(nickname, "third witch 2"); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUpdateSubscriptions() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->updateSubscriptions(QStringLiteral("coven@mix.shakespeare.example"), QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence, QXmppMixIq::Node::Configuration | QXmppMixIq::Node::Information); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixIq::Node::Messages | QXmppMixIq::Node::Presence); + QCOMPARE(result.nodesBeingUnsubscribedFrom, QXmppMixIq::Node::Configuration | QXmppMixIq::Node::Information); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestAllowedJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestAllowedJids(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto allowedJids = expectFutureVariant>(task); + QCOMPARE(allowedJids.at(0), QStringLiteral("shakespeare.example")); + QCOMPARE(allowedJids.at(1), QStringLiteral("alice@wonderland.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testAllowJid() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->allowJid(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("alice@wonderland.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testAllowAllJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->allowAllJids(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testDisallowJid() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->disallowJid(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("alice@wonderland.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testDisallowAllJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->disallowAllJids(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestBannedJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestBannedJids(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto allowedJids = expectFutureVariant>(task); + QCOMPARE(allowedJids.at(0), QStringLiteral("lear@shakespeare.example")); + QCOMPARE(allowedJids.at(1), QStringLiteral("macbeth@shakespeare.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testBanJid() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->banJid(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("macbeth@shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUnbanJid() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->unbanJid(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("macbeth@shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUnbanAllJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->unbanAllJids(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestParticipants() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestParticipants(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "thirdwitch" + "hag66@shakespeare.example" + "" + "" + "" + "" + "fourthwitch" + "hag67@shakespeare.example" + "" + "" + "" + "" + "")); + + auto participants = expectFutureVariant>(task); + QCOMPARE(participants.at(0).jid(), QStringLiteral("hag66@shakespeare.example")); + QCOMPARE(participants.at(1).jid(), QStringLiteral("hag67@shakespeare.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testLeaveChannel() +{ + auto tester = Tester(QStringLiteral("hag66@shakespeare.example")); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->leaveChannel(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "")); + + expectFutureVariant(task); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("hag66@shakespeare.example")); +} + +void tst_QXmppMixManager::testDeleteChannel() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->deleteChannel(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("mix.shakespeare.example")); +} + +template +void tst_QXmppMixManager::testErrorFromChannel(QXmppTask &task, TestClient &client) +{ + testErrorFromChannel(task, client, QStringLiteral("qxmpp1")); +} + +template +void tst_QXmppMixManager::testErrorFromChannel(QXmppTask &task, TestClient &client, const QString &id) +{ + testError(task, client, id, QStringLiteral("coven@mix.shakespeare.example")); +} + +template +void tst_QXmppMixManager::testError(QXmppTask &task, TestClient &client, const QString &id, const QString &from) +{ + client.ignore(); + client.inject(QStringLiteral("" + "" + "" + "" + "") + .arg(id, from)); + + expectFutureVariant(task); +} + +QTEST_MAIN(tst_QXmppMixManager) +#include "tst_qxmppmixmanager.moc"