From b7ae1fbb2362aef3892be1af2525981642426ba0 Mon Sep 17 00:00:00 2001 From: Melvin Keskin Date: Mon, 6 Nov 2023 20:02:49 +0100 Subject: [PATCH] Add QXmppMixManager This implements a manager for MIX as specified by: * 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 Closes #176 --- doc/doap.xml | 18 +- src/CMakeLists.txt | 2 + src/base/QXmppConstants.cpp | 3 +- src/base/QXmppConstants_p.h | 1 + src/client/QXmppMixManager.cpp | 1449 ++++++++++++++ src/client/QXmppMixManager.h | 157 ++ tests/CMakeLists.txt | 1 + tests/qxmppmixmanager/tst_qxmppmixmanager.cpp | 1718 +++++++++++++++++ 8 files changed, 3343 insertions(+), 6 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 9de59bfd5..add4468d3 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,18 @@ SPDX-License-Identifier: CC0-1.0 - partial + complete 0.5 1.3 - Only IQ queries implemented + Manager since 1.6 + + + + + + complete + 0.3 + 1.6 @@ -585,7 +593,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 3e51e10db..939577fb2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -121,6 +121,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 @@ -257,6 +258,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 dd95c1c39..e6595f361 100644 --- a/src/base/QXmppConstants.cpp +++ b/src/base/QXmppConstants.cpp @@ -201,7 +201,8 @@ const char *ns_omemo_2_devices = "urn:xmpp:omemo:2:devices"; // XEP-0404: Mediated Information eXchange (MIX): JID Hidden Channels const char *ns_mix_node_jidmap = "urn:xmpp:mix:nodes:jidmap"; // 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-0406: Mediated Information eXchange (MIX): MIX Administration diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h index c43d33ec9..1dacdd59e 100644 --- a/src/base/QXmppConstants_p.h +++ b/src/base/QXmppConstants_p.h @@ -219,6 +219,7 @@ inline constexpr QStringView ns_omemo_2_devices = u"urn:xmpp:omemo:2:devices"; inline constexpr QStringView ns_mix_node_jidmap = u"urn:xmpp:mix:nodes:jidmap"; // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements inline constexpr QStringView ns_mix_pam = u"urn:xmpp:mix:pam:2"; +inline constexpr QStringView ns_mix_pam_archiving = u"urn:xmpp:mix:pam:2#archive"; inline constexpr QStringView ns_mix_roster = u"urn:xmpp:mix:roster:0"; inline constexpr QStringView ns_mix_presence = u"urn:xmpp:presence:0"; // XEP-0406: Mediated Information eXchange (MIX): MIX Administration diff --git a/src/client/QXmppMixManager.cpp b/src/client/QXmppMixManager.cpp new file mode 100644 index 000000000..af22134bf --- /dev/null +++ b/src/client/QXmppMixManager.cpp @@ -0,0 +1,1449 @@ +// 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 updates of the channel (e.g., its configuration or allowed +/// JIDs), make sure to subscribe to the corresponding nodes. +/// +/// In order to send a message to a MIX channel, you have to set the type QXmppMessage::GroupChat. +/// +/// Example for an unencrypted message: +/// \code +/// message->setType(QXmppMessage::GroupChat); +/// message->setTo("group@mix.example.org") +/// client->send(std::move(message)); +/// \endcode +/// +/// Example for an encrypted message decryptable by Alice and Bob: +/// \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::CreationResult +/// +/// Contains the JID of the created MIX channel a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::ChannelJidResult +/// +/// Contains the JIDs of all discoverable MIX channels of a MIX service or a QXmppError if it +/// failed. +/// + +/// +/// \typedef QXmppMixManager::ChannelNodeResult +/// +/// Contains all nodes of the requested MIX channel that can be subscribed by the user or a +/// QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::ConfigurationResult +/// +/// Contains the configuration of the MIX channel or a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::InformationResult +/// +/// Contains the information of the MIX channel or a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::JoiningResult +/// +/// Contains the result of the joined MIX channel or a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::NicknameResult +/// +/// Contains the new nickname within a joined MIX channel or a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::SubscriptionResult +/// +/// Contains the result of the subscribed/unsubscribed nodes belonging to a MIX channel or a +/// QXmppError on failure. +/// + +/// +/// \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 on failure. +/// + +/// +/// \typedef QXmppMixManager::ParticipantResult +/// +/// Contains the participants of a MIX channel or a QXmppError on failure. +/// + +constexpr QStringView MIX_SERVICE_DISCOVERY_NODE = u"mix"; + +/// +/// Constructs a MIX manager. +/// +QXmppMixManager::QXmppMixManager() = default; + +QStringList QXmppMixManager::discoveryFeatures() const +{ + return { ns_mix.toString() }; +} + +/// +/// 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. +/// + +/// +/// Creates a MIX channel. +/// +/// If no channelId is passed, the channel is created with an ID provided by the MIX service. +/// Furthermore, the channel cannot be discovered by anyone. +/// A channel with the mentioned properties is called an "ad-hoc channel". +/// +/// 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 (default: provided by the server) +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::createChannel(const QString &serviceJid, const QString &channelId) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(serviceJid); + iq.setActionType(QXmppMixIq::Create); + iq.setChannelId(channelId); + + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> CreationResult { + return iq.channelJid().isEmpty() ? iq.channelId() % u'@' % iq.from() : iq.channelJid(); + }); +} + +/// +/// 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 jids; + + std::for_each(items->cbegin(), items->cend(), [&jids](const QXmppDiscoveryIq::Item &item) { + jids.append(item.jid()); + }); + + promise.finish(jids); + } else { + promise.finish(std::move(std::get(result))); + } + }); + + return promise.task(); +} + +/// +/// Requests all nodes of a MIX channel that can be subscribed by the user. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestChannelNodes(const QString &channelJid) +{ + QXmppPromise promise; + + auto task = m_discoveryManager->requestDiscoItems(channelJid, MIX_SERVICE_DISCOVERY_NODE.toString()); + task.then(this, [promise](QXmppDiscoveryManager::ItemsResult result) mutable { + if (const auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + const auto items = std::get>(result); + QVector nodes; + + std::for_each(items.cbegin(), items.cend(), [&nodes](const QXmppDiscoveryIq::Item &item) { + nodes.append(item.node()); + }); + + promise.finish(QXmppMixIq::listToNodes(nodes)); + } + }); + + return promise.task(); +} + +/// +/// Requests the configuration of a MIX channel. +/// +/// \param channelJid JID of the channel whose configuration is requested +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestChannelConfiguration(const QString &channelJid) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->requestItems(channelJid, ns_mix_node_config.toString()); + 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 configuration of a MIX channel. +/// +/// In order to use this method, retrieve the current configuration via +/// requestChannelConfiguration() first, change the desired attributes and pass the configuration to +/// this method. +/// +/// \param channelJid JID of the channel whose configuration is to be updated +/// \param configuration new configuration of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::updateChannelConfiguration(const QString &channelJid, QXmppMixConfigItem configuration) +{ + QXmppPromise promise; + + configuration.setFormType(QXmppDataForm::Submit); + + auto task = m_pubSubManager->publishItem(channelJid, ns_mix_node_config.toString(), configuration); + 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(); +} + +/// +/// \fn QXmppMixManager::channelConfigurationUpdated(const QString &channelJid, const QXmppMixConfigItem &configuration) +/// +/// Emitted when the configuration of a MIX channel is updated. +/// +/// \param channelJid JID of the channel whose configuration is updated +/// \param configuration new channel configuration +/// + +/// +/// Requests the information of a MIX channel. +/// +/// \param channelJid JID of the channel whose information is 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.toString()); + 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.toString(), 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(); +} + +/// +/// \fn QXmppMixManager::channelInformationUpdated(const QString &channelJid, const QXmppMixInfoItem &information) +/// +/// Emitted when the information of a MIX channel is updated. +/// +/// \param channelJid JID of the channel whose information is updated +/// \param information new channel information +/// + +/// +/// 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, QXmppMixConfigItem::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::Invitation); + 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, QXmppMixConfigItem::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, QXmppMixConfigItem::Nodes nodesToSubscribeTo, QXmppMixConfigItem::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.toString()); +} + +/// +/// 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. +/// +/// Allowing a JID is only needed if the channel does not allow anyone to participate. +/// That is the case when QXmppMixConfigItem::Node::AllowedJids exists for the channel. +/// Use requestChannelConfiguration() and QXmppMixConfigItem::nodes() to determine that. +/// Call updateChannelConfiguration() and QXmppMixConfigItem::setNodes() to update it accordingly. +/// In order to allow all JIDs to participate in a channel, you need to remove +/// QXmppMixConfigItem::Node::AllowedJids from the channel's nodes. +/// +/// \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.toString(), 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 +/// + +/// +/// \fn QXmppMixManager::allJidsAllowed(const QString &channelJid) +/// +/// Emitted when all JIDs are allowed to participate in a MIX channel. +/// +/// That happens if QXmppMixConfigItem::Node::AllowedJids is removed from a channel. +/// +/// \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.toString(), 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.toString()); +} + +/// +/// \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.toString()); +} + +/// +/// 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. +/// +/// Before calling this, make sure that QXmppMixConfigItem::Node::BannedJids exists for the channel. +/// Use requestChannelConfiguration() and QXmppMixConfigItem::nodes() to determine that. +/// Call updateChannelConfiguration() and QXmppMixConfigItem::setNodes() to update it accordingly. +/// +/// \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.toString(), 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.toString(), 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.toString()); +} + +/// +/// \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.toString()); + 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 that 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 that 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) +{ + 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_config && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + case QXmppPubSubEventBase::Items: { + const auto item = event.items().constFirst(); + Q_EMIT channelConfigurationUpdated(pubSubService, item); + break; + } + case QXmppPubSubEventBase::Retract: + case QXmppPubSubEventBase::Purge: + case QXmppPubSubEventBase::Delete: + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } else if (nodeName == ns_mix_node_info && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + case QXmppPubSubEventBase::Items: { + const auto item = event.items().constFirst(); + Q_EMIT channelInformationUpdated(pubSubService, item); + break; + } + case QXmppPubSubEventBase::Retract: + case QXmppPubSubEventBase::Purge: + case QXmppPubSubEventBase::Delete: + 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, QXmppMixConfigItem::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)); +} + +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)) { + 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() == u"conference" && (identity.type() == MIX_SERVICE_DISCOVERY_NODE || identity.type() == u"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..804c9272a --- /dev/null +++ b/src/client/QXmppMixManager.h @@ -0,0 +1,157 @@ +// 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 "QXmppMixInfoItem.h" +#include "QXmppMixIq.h" +#include "QXmppMixParticipantItem.h" +#include "QXmppPubSubEventHandler.h" + +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 + { + QXmppMixConfigItem::Nodes nodesBeingSubscribedTo; + QXmppMixConfigItem::Nodes nodesBeingUnsubscribedFrom; + }; + + struct Participation + { + QString participantId; + QString nickname; + QXmppMixConfigItem::Nodes nodesBeingSubscribedTo; + }; + + using Jid = QString; + using ChannelJid = QString; + using Nickname = QString; + + using CreationResult = std::variant; + using ChannelJidResult = std::variant, QXmppError>; + using ChannelNodeResult = std::variant; + using ConfigurationResult = std::variant; + using InformationResult = 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 createChannel(const QString &serviceJid, const QString &channelId = {}); + + QXmppTask requestChannelJids(const QString &serviceJid); + QXmppTask requestChannelNodes(const QString &channelJid); + + QXmppTask requestChannelConfiguration(const QString &channelJid); + QXmppTask updateChannelConfiguration(const QString &channelJid, QXmppMixConfigItem configuration); + Q_SIGNAL void channelConfigurationUpdated(const QString &channelJid, const QXmppMixConfigItem &configuration); + + QXmppTask requestChannelInformation(const QString &channelJid); + QXmppTask updateChannelInformation(const QString &channelJid, QXmppMixInfoItem information); + Q_SIGNAL void channelInformationUpdated(const QString &channelJid, const QXmppMixInfoItem &information); + + QXmppTask joinChannel(const QString &channelJid, const QString &nickname = {}, QXmppMixConfigItem::Nodes nodes = ~QXmppMixConfigItem::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 = {}, QXmppMixConfigItem::Nodes nodes = ~QXmppMixConfigItem::Nodes()); + + QXmppTask updateNickname(const QString &channelJid, const QString &nickname); + QXmppTask updateSubscriptions(const QString &channelJid, QXmppMixConfigItem::Nodes nodesToSubscribeTo = ~QXmppMixConfigItem::Nodes(), QXmppMixConfigItem::Nodes nodesToUnsubscribeFrom = ~QXmppMixConfigItem::Nodes()); + + QXmppTask requestAllowedJids(const QString &channelJid); + QXmppTask allowJid(const QString &channelJid, const QString &jid); + Q_SIGNAL void jidAllowed(const QString &channelJid, const QString &jid); + 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, QXmppMixConfigItem::Nodes nodes); + QXmppTask joinChannel(QXmppMixIq &&iq); + QXmppTask sendInvitation(const QXmppMixInvitation &invitation, const QString &messageBody); + 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/qxmppmixmanager/tst_qxmppmixmanager.cpp b/tests/qxmppmixmanager/tst_qxmppmixmanager.cpp new file mode 100644 index 000000000..0dce4f4c2 --- /dev/null +++ b/tests/qxmppmixmanager/tst_qxmppmixmanager.cpp @@ -0,0 +1,1718 @@ +// 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 testSendInvitationPrivate(); + Q_SLOT void testJoinChannelPrivate(); + Q_SLOT void testPrepareJoinIq(); + Q_SLOT void testHandlePubSubEvent(); + Q_SLOT void testHandleMessage(); + Q_SLOT void testSetClient(); + Q_SLOT void testCreateChannel(); + Q_SLOT void testCreateChannelWithId(); + Q_SLOT void testRequestChannelJids(); + Q_SLOT void testRequestChannelNodes(); + Q_SLOT void testRequestChannelConfiguration(); + Q_SLOT void testUpdateChannelConfiguration(); + Q_SLOT void testRequestChannelInformation(); + Q_SLOT void testUpdateChannelInformation(); + 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 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 task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +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::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(QXmppMixConfigItem::Node::AllowedJids | QXmppMixConfigItem::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, QXmppMixConfigItem::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"), QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::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(), QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::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 channelConfigurationUpdatedSpy(&manager, &QXmppMixManager::channelConfigurationUpdated); + QSignalSpy channelInformationUpdatedSpy(&manager, &QXmppMixManager::channelInformationUpdated); + QSignalSpy userJoinedOrParticipantModifiedSpy(&manager, &QXmppMixManager::userJoinedOrParticipantModified); + QSignalSpy participantLeftSpy(&manager, &QXmppMixManager::participantLeft); + QSignalSpy channelDeletedSpy(&manager, &QXmppMixManager::channelDeleted); + + const auto channelJid = QStringLiteral("coven@mix.shakespeare.example"); + const auto channelName = QStringLiteral("The Coven"); + const QStringList nodes = { QStringLiteral("urn:xmpp:mix:nodes:allowed"), QStringLiteral("urn:xmpp:mix:nodes:banned") }; + const auto configurationNode = QStringLiteral("urn:xmpp:mix:nodes:config"); + const auto informationNode = QStringLiteral("urn:xmpp:mix:nodes:info"); + const auto participantNode = QStringLiteral("urn:xmpp:mix:nodes:participants"); + const QStringList jids = { QStringLiteral("hag66@shakespeare.example"), QStringLiteral("cat@shakespeare.example") }; + + const auto eventTypes = QVector { QXmppPubSubEventBase::EventType::Configuration, + QXmppPubSubEventBase::EventType::Delete, + QXmppPubSubEventBase::EventType::Items, + QXmppPubSubEventBase::EventType::Retract, + QXmppPubSubEventBase::EventType::Purge, + QXmppPubSubEventBase::EventType::Subscription }; + + QXmppPubSubBaseItem allowedOrBannedJidsItem1; + allowedOrBannedJidsItem1.setId(jids.at(0)); + + QXmppPubSubBaseItem allowedOrBannedJidsItem2; + allowedOrBannedJidsItem2.setId(jids.at(1)); + + QXmppPubSubEvent allowedOrBannedJidsEvent; + allowedOrBannedJidsEvent.setItems({ allowedOrBannedJidsItem1, allowedOrBannedJidsItem2 }); + allowedOrBannedJidsEvent.setRetractIds(jids); + + QXmppMixParticipantItem participantItem1; + participantItem1.setJid(jids.at(0)); + + QXmppMixParticipantItem participantItem2; + participantItem2.setJid(jids.at(1)); + + QXmppPubSubEvent participantEvent; + participantEvent.setItems({ participantItem1, participantItem2 }); + participantEvent.setRetractIds(jids); + + QXmppMixConfigItem configurationItem; + configurationItem.setFormType(QXmppDataForm::Result); + configurationItem.setOwnerJids(jids); + + QXmppPubSubEvent configurationEvent; + configurationEvent.setItems({ configurationItem }); + configurationEvent.setRetractIds(jids); + + QXmppMixInfoItem informationItem; + informationItem.setFormType(QXmppDataForm::Result); + informationItem.setName(channelName); + + QXmppPubSubEvent informationEvent; + informationEvent.setItems({ informationItem }); + informationEvent.setRetractIds(jids); + + for (const auto &node : nodes) { + for (auto eventType : eventTypes) { + allowedOrBannedJidsEvent.setEventType(eventType); + manager.handlePubSubEvent(writePacketToDom(allowedOrBannedJidsEvent), channelJid, node); + } + } + + for (auto eventType : eventTypes) { + participantEvent.setEventType(eventType); + manager.handlePubSubEvent(writePacketToDom(participantEvent), channelJid, participantNode); + } + + for (auto eventType : eventTypes) { + configurationEvent.setEventType(eventType); + manager.handlePubSubEvent(writePacketToDom(configurationEvent), channelJid, configurationNode); + } + + for (auto eventType : eventTypes) { + informationEvent.setEventType(eventType); + manager.handlePubSubEvent(writePacketToDom(informationEvent), channelJid, informationNode); + } + + 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->constFirst(); + 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(), participantEvent.items().at(i).jid()); + } + + for (const auto &spy : { &channelConfigurationUpdatedSpy, &channelInformationUpdatedSpy }) { + QCOMPARE(spy->size(), 1); + auto arguments = spy->constFirst(); + QCOMPARE(arguments.at(0).toString(), channelJid); + } + + QCOMPARE(channelConfigurationUpdatedSpy.first().at(1).value().ownerJids(), jids); + QCOMPARE(channelInformationUpdatedSpy.first().at(1).value().name(), channelName); +} + +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::testCreateChannel() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->createChannel(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::testCreateChannelWithId() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->createChannel(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::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::testRequestChannelNodes() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestChannelNodes(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "")); + + auto nodes = expectFutureVariant(task); + QCOMPARE(nodes, QXmppMixConfigItem::Node::AllowedJids | QXmppMixConfigItem::Node::Presence); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestChannelConfiguration() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->requestChannelConfiguration(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "urn:xmpp:mix:admin:0" + "" + "" + "greymalkin@shakespeare.example" + "" + "" + "" + "" + "" + "")); + + auto configuration = expectFutureVariant(task); + QCOMPARE(configuration.lastEditorJid(), QStringLiteral("greymalkin@shakespeare.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUpdateChannelConfiguration() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + QXmppMixConfigItem configuration; + configuration.setId(QStringLiteral("2016-05-30T09:00:00")); + configuration.setOwnerJids({ QStringLiteral("greymalkin@shakespeare.example") }); + + auto call = [manager, configuration]() { + return manager->updateChannelConfiguration(QStringLiteral("coven@mix.shakespeare.example"), configuration); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "urn:xmpp:mix:admin:0" + "" + "" + "greymalkin@shakespeare.example" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +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" + "" + "" + "" + "" + "" + "")); + + auto information = expectFutureVariant(task); + QCOMPARE(information.name(), QStringLiteral("Witches Coven")); + + 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")); + + 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" + "" + "" + "" + "" + "" + "")); + 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, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::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, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); +} + +void tst_QXmppMixManager::testJoinChannelWithNodes() +{ + auto [client, manager] = Tester(QStringLiteral("hag66@shakespeare.example")); + + auto task = manager->joinChannel(QStringLiteral("coven@mix.shakespeare.example"), {}, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, "123456"); + QVERIFY(result.nickname.isEmpty()); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::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, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::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, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::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, {}, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::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, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::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"), QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence, QXmppMixConfigItem::Node::Configuration | QXmppMixConfigItem::Node::Information); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); + QCOMPARE(result.nodesBeingUnsubscribedFrom, QXmppMixConfigItem::Node::Configuration | QXmppMixConfigItem::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::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"