diff --git a/doc/doap.xml b/doc/doap.xml
index 13763dcd3..7c9150870 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.7
@@ -581,10 +581,18 @@ SPDX-License-Identifier: CC0-1.0
- partial
+ complete
0.5
1.3
- Only IQ queries implemented
+ Manager since 1.7
+
+
+
+
+
+ complete
+ 0.3
+ 1.7
@@ -593,7 +601,7 @@ SPDX-License-Identifier: CC0-1.0
partial
0.1
1.4
- Only invitations implemented
+ Only invitations implemented; Manager since 1.7
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2ed2b3e47..3ed74ba65 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -126,6 +126,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
@@ -262,6 +263,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_p.h b/src/base/QXmppConstants_p.h
index 8028451a7..4d01667e2 100644
--- a/src/base/QXmppConstants_p.h
+++ b/src/base/QXmppConstants_p.h
@@ -227,6 +227,7 @@ inline constexpr QStringView ns_mix_node_presence = u"urn:xmpp:mix:nodes:presenc
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..b7df0cc36
--- /dev/null
+++ b/src/client/QXmppMixManager.cpp
@@ -0,0 +1,1382 @@
+// 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 "QXmppMixIq_p.h"
+#include "QXmppPubSubEvent.h"
+#include "QXmppPubSubManager.h"
+#include "QXmppRosterManager.h"
+#include "QXmppUtils.h"
+
+#include "Algorithms.h"
+
+#include
+
+using namespace QXmpp::Private;
+
+class QXmppMixManagerPrivate
+{
+public:
+ QXmppPubSubManager *pubSubManager = nullptr;
+ QXmppDiscoveryManager *discoveryManager = nullptr;
+ bool supportedByServer = false;
+ bool archivingSupportedByServer = false;
+ QList services;
+};
+
+///
+/// \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, make sure to add all managers needed by this manager:
+/// \code
+/// client->addNewExtension();
+/// client->addNewExtension();
+/// \endcode
+///
+/// Afterwards, you need to add this manager 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
+///
+/// If you receive a message, you can check whether it contains an invitation to a MIX channel and
+/// use that invitation to join the channel:
+/// \code
+/// if (auto invitation = message.mixInvitation()) {
+/// manager->joinChannel(*invitation, nickname, nodes);
+/// }
+/// \endcode
+///
+/// In order to invite someone to a MIX channel that allows the invitee to participate, you can
+/// create an invitation on your own.
+///
+/// That can be done 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 received via
+/// requestInvitation() but the inviter is permitted to allow JIDs via allowJid().
+/// In that case, the invitee's JID has to be allowed before the invitee tries to join the
+/// channel.
+///
+/// Invitations are sent as regular messages.
+/// They are 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 an XMPP URI to the body.
+///
+/// If the body (i.e., the invitation text) is empty, the message would neither be delivered to all
+/// clients via \xep{0280, Message Carbons} nor delivered to clients which are currently offline via
+/// \xep{0313, Message Archive Management}.
+/// To enforce that behavior, set a corresponding message type and message processing hint:
+/// \code
+/// QXmppMixInvitation invitation;
+///
+/// invitation.setInviterJid(client->configuration().jidBare());
+/// invitation.setInviteeJid(inviteeJid);
+/// invitation.setChannelJid(channelJid);
+///
+/// QXmppMessage message;
+///
+/// message.setTo(inviteeJid);
+/// message.setMixInvitation(invitation);
+///
+/// if (messageBody.isEmpty()) {
+/// message.setType(QXmppMessage::Chat);
+/// message.addHint(QXmppMessage::Store);
+/// } else {
+/// message.setBody(messageBody);
+/// }
+///
+/// client()->sendSensitive(std::move(message));
+/// \endcode
+///
+/// In order to invite someone to a MIX channel that does not yet allow the invitee to participate,
+/// you can request an invitation from the channel (if permitted to do so):
+/// \code
+/// manager->requestInvitation().then(this, [](QXmppMixManager::InvitationResult &&result) mutable {
+/// if (auto *error = std::get_if(&result)) {
+/// // Handle the error.
+/// } else {
+/// auto invitation = std::get(std::move(result));
+/// // Create and send the invitation.
+/// }
+/// });
+/// \endcode
+///
+/// \ingroup Managers
+///
+/// \since QXmpp 1.7
+///
+
+///
+/// \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::additions
+///
+/// Nodes belonging to the channel that are subscribed to.
+///
+/// \var QXmppMixManager::Subscription::removals
+///
+/// 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::subscriptions
+///
+/// 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 or 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::InvitationResult
+///
+/// Contains the requested invitation to a 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()
+ : d(new QXmppMixManagerPrivate())
+{
+}
+
+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 d->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 d->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 d->services;
+}
+
+///
+/// \fn QXmppMixManager::servicesChanged()
+///
+/// Emitted when the services providing MIX on the own server changed.
+///
+
+///
+/// Creates a MIX channel.
+///
+/// If no channel ID 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)
+{
+ return chain(d->discoveryManager->requestDiscoItems(serviceJid), this, [](QXmppDiscoveryManager::ItemsResult &&result) -> ChannelJidResult {
+ if (auto *error = std::get_if(&result)) {
+ return std::move(*error);
+ }
+
+ const auto &items = std::get>(result);
+ return transform>(items, [](const QXmppDiscoveryIq::Item &item) {
+ return item.jid();
+ });
+ });
+}
+
+///
+/// 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)
+{
+ return chain(d->discoveryManager->requestDiscoItems(channelJid, MIX_SERVICE_DISCOVERY_NODE.toString()), this, [](QXmppDiscoveryManager::ItemsResult &&result) -> ChannelNodeResult {
+ if (auto *error = std::get_if(&result)) {
+ return std::move(*error);
+ }
+
+ const auto &items = std::get>(result);
+ const auto nodes = transform>(items, [](const QXmppDiscoveryIq::Item &item) {
+ return item.node();
+ });
+
+ return listToMixNodes(nodes);
+ });
+}
+
+///
+/// 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)
+{
+ return chain(d->pubSubManager->requestItems(channelJid, ns_mix_node_config.toString()), this, [](QXmppPubSubManager::ItemsResult &&result) -> ConfigurationResult {
+ if (auto *error = std::get_if(&result)) {
+ return std::move(*error);
+ }
+
+ return std::get>(result).items.takeFirst();
+ });
+}
+
+///
+/// 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)
+{
+ configuration.setFormType(QXmppDataForm::Submit);
+
+ return chain(d->pubSubManager->publishItem(channelJid, ns_mix_node_config.toString(), configuration), this, [](QXmppPubSubManager::PublishItemResult &&result) -> QXmppClient::EmptyResult {
+ if (auto *error = std::get_if(&result)) {
+ return std::move(*error);
+ }
+
+ return QXmpp::Success();
+ });
+}
+
+///
+/// \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)
+{
+ return chain(d->pubSubManager->requestItems(channelJid, ns_mix_node_info.toString()), this, [](QXmppPubSubManager::ItemsResult &&result) -> InformationResult {
+ if (auto *error = std::get_if(&result)) {
+ return std::move(*error);
+ }
+
+ return std::get>(result).items.takeFirst();
+ });
+}
+
+///
+/// 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)
+{
+ information.setFormType(QXmppDataForm::Submit);
+
+ return chain(d->pubSubManager->publishItem(channelJid, ns_mix_node_info.toString(), information), this, [](QXmppPubSubManager::PublishItemResult &&result) -> QXmppClient::EmptyResult {
+ if (auto *error = std::get_if(&result)) {
+ return std::move(*error);
+ }
+
+ return QXmpp::Success();
+ });
+}
+
+///
+/// \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(prepareJoinIq(channelJid, nickname, nodes));
+}
+
+///
+/// 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::joinChannel(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 subscriptionAdditions nodes to subscribe to
+/// \param subscriptionRemovals nodes to unsubscribe from
+///
+/// \return the result of the action
+///
+QXmppTask QXmppMixManager::updateSubscriptions(const QString &channelJid, QXmppMixConfigItem::Nodes subscriptionAdditions, QXmppMixConfigItem::Nodes subscriptionRemovals)
+{
+ QXmppMixSubscriptionUpdateIq iq;
+ iq.setType(QXmppIq::Set);
+ iq.setTo(channelJid);
+ iq.setAdditions(subscriptionAdditions);
+ iq.setRemovals(subscriptionRemovals);
+
+ return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixSubscriptionUpdateIq &&iq) -> SubscriptionResult {
+ return Subscription { iq.additions(), iq.removals() };
+ });
+}
+
+///
+/// Requests an invitation to a MIX channel that the invitee is not yet allowed to participate in.
+///
+/// The invitee can use the invitation to join the channel.
+///
+/// That invitation mechanism avoids storing allowed JIDs for an indefinite time if the
+/// corresponding user never joins the channel.
+/// By using this method, there is no need to allow the invitee to participate in the channel via
+/// allowJid().
+///
+/// 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 invitee is invited to
+/// \param inviteeJid JID of the invitee
+///
+/// \return the result of the action
+///
+QXmppTask QXmppMixManager::requestInvitation(const QString &channelJid, const QString &inviteeJid)
+{
+ QXmppMixInvitationRequestIq iq;
+ iq.setType(QXmppIq::Get);
+ iq.setTo(channelJid);
+ iq.setInviteeJid(inviteeJid);
+
+ return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixInvitationResponseIq &&iq) -> InvitationResult {
+ return iq.invitation();
+ });
+}
+
+///
+/// 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 d->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 d->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 d->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 d->pubSubManager->purgeItems(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.
+/// Furthermore, that happens if QXmppMixConfigItem::Node::BannedJids is removed from a channel.
+///
+/// \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)
+{
+ return chain(d->pubSubManager->requestItems(channelJid, ns_mix_node_participants.toString()), this, [](QXmppPubSubManager::ItemsResult &&result) -> ParticipantResult {
+ if (auto *error = std::get_if(&result)) {
+ return std::move(*error);
+ }
+
+ return std::get>(std::move(result)).items;
+ });
+}
+
+///
+/// \fn QXmppMixManager::participantReceived(const QString &channelJid, const QXmppMixParticipantItem &participant)
+///
+/// Emitted when a user joined a MIX channel or a participant is updated.
+///
+/// \param channelJid JID of the channel that the user joined or whose participant is updated
+/// \param participant new or updated 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::onRegistered(QXmppClient *client)
+{
+ connect(client, &QXmppClient::connected, this, [this, client]() {
+ if (client->streamManagementState() == QXmppClient::NewStream) {
+ resetCachedData();
+ }
+ });
+
+ d->discoveryManager = client->findExtension();
+ Q_ASSERT_X(d->discoveryManager, "QXmppMixManager", "QXmppDiscoveryManager is missing");
+
+ connect(d->discoveryManager, &QXmppDiscoveryManager::infoReceived, this, &QXmppMixManager::handleDiscoInfo);
+
+ d->pubSubManager = client->findExtension();
+ Q_ASSERT_X(d->discoveryManager, "QXmppMixManager", "QXmppPubSubManager is missing");
+}
+
+void QXmppMixManager::onUnregistered(QXmppClient *client)
+{
+ disconnect(d->discoveryManager, &QXmppDiscoveryManager::infoReceived, this, &QXmppMixManager::handleDiscoInfo);
+ resetCachedData();
+ disconnect(client, &QXmppClient::disconnected, this, nullptr);
+}
+
+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;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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 participantReceived(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.setSubscriptions(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.subscriptions() };
+ });
+}
+
+///
+/// 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)
+{
+ return chain(d->pubSubManager->requestItems(channelJid, node), this, [](QXmppPubSubManager::ItemsResult &&result) -> JidResult {
+ if (auto error = std::get_if(&result)) {
+ return std::move(*error);
+ }
+
+ const auto &items = std::get>(result).items;
+ return transform>(items, [](const QXmppPubSubBaseItem &item) {
+ return item.id();
+ });
+ });
+}
+
+///
+/// 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)
+{
+ const QXmppPubSubBaseItem item { jid };
+
+ return chain(d->pubSubManager->publishItem(channelJid, node, item), this, [](QXmppPubSubManager::PublishItemResult &&result) -> QXmppClient::EmptyResult {
+ if (auto *error = std::get_if(&result)) {
+ return std::move(*error);
+ }
+
+ return QXmpp::Success();
+ });
+}
+
+///
+/// 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 (d->supportedByServer != supportedByServer) {
+ d->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 (d->archivingSupportedByServer != archivingSupportedByServer) {
+ d->archivingSupportedByServer = archivingSupportedByServer;
+ Q_EMIT archivingSupportedByServerChanged();
+ }
+}
+
+///
+/// Adds a MIX service.
+///
+/// \param service MIX service
+///
+void QXmppMixManager::addService(const Service &service)
+{
+ const auto jidsEqual = [&jid = service.jid](const Service &service) {
+ return service.jid == jid;
+ };
+
+ auto itr = std::find_if(d->services.begin(), d->services.end(), jidsEqual);
+
+ if (itr == d->services.end()) {
+ d->services.append(service);
+ } else if (*itr == service) {
+ // Do not emit "servicesChanged()" if the service is already cached with the same
+ // properties.
+ return;
+ } else {
+ *itr = service;
+ }
+
+ Q_EMIT servicesChanged();
+}
+
+///
+/// Removes a MIX service.
+///
+/// \param jid JID of the MIX service
+///
+void QXmppMixManager::removeService(const QString &jid)
+{
+ const auto jidsEqual = [&jid](const Service &service) {
+ return service.jid == jid;
+ };
+
+ auto itr = std::find_if(d->services.begin(), d->services.end(), jidsEqual);
+
+ if (itr == d->services.end()) {
+ return;
+ }
+
+ d->services.erase(itr);
+
+ Q_EMIT servicesChanged();
+}
+
+///
+/// Removes all MIX services.
+///
+void QXmppMixManager::removeServices()
+{
+ if (!d->services.isEmpty()) {
+ d->services.clear();
+ Q_EMIT servicesChanged();
+ }
+}
+
+///
+/// Resets the cached data.
+///
+void QXmppMixManager::resetCachedData()
+{
+ setSupportedByServer(false);
+ setArchivingSupportedByServer(false);
+ removeServices();
+}
diff --git a/src/client/QXmppMixManager.h b/src/client/QXmppMixManager.h
new file mode 100644
index 000000000..91687111a
--- /dev/null
+++ b/src/client/QXmppMixManager.h
@@ -0,0 +1,154 @@
+// SPDX-FileCopyrightText: 2023 Melvin Keskin
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef QXMPPMIXMANAGER_H
+#define QXMPPMIXMANAGER_H
+
+#include "QXmppClient.h"
+#include "QXmppClientExtension.h"
+#include "QXmppMixConfigItem.h"
+#include "QXmppMixInfoItem.h"
+#include "QXmppMixParticipantItem.h"
+#include "QXmppPubSubEventHandler.h"
+
+class QXmppMixIq;
+class QXmppMixInvitation;
+class QXmppMixManagerPrivate;
+
+class QXMPP_EXPORT QXmppMixManager : public QXmppClientExtension, 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 QXMPP_EXPORT Service {
+ QString jid;
+ bool channelsSearchable = false;
+ bool channelCreationAllowed = false;
+
+ /// \cond
+ bool operator==(const Service &other) const;
+ /// \endcond
+ };
+
+ struct Subscription {
+ QXmppMixConfigItem::Nodes additions;
+ QXmppMixConfigItem::Nodes removals;
+ };
+
+ struct Participation {
+ QString participantId;
+ QString nickname;
+ QXmppMixConfigItem::Nodes subscriptions;
+ };
+
+ 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 InvitationResult = std::variant;
+ using SubscriptionResult = std::variant;
+ using JidResult = std::variant, QXmppError>;
+ using ParticipantResult = std::variant, QXmppError>;
+
+ QXmppMixManager();
+ ~QXmppMixManager() override;
+
+ 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 joinChannel(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 subscriptionAdditions = ~QXmppMixConfigItem::Nodes(), QXmppMixConfigItem::Nodes subscriptionRemovals = ~QXmppMixConfigItem::Nodes());
+
+ QXmppTask requestInvitation(const QString &channelJid, const QString &inviteeJid);
+
+ 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 participantReceived(const QString &channelJid, const QXmppMixParticipantItem &participant);
+ 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 onRegistered(QXmppClient *client) override;
+ void onUnregistered(QXmppClient *client) override;
+ bool handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName) override;
+ /// \endcond
+
+private:
+ QXmppMixIq prepareJoinIq(const QString &channelJid, const QString &nickname, QXmppMixConfigItem::Nodes nodes);
+ QXmppTask joinChannel(QXmppMixIq &&iq);
+ 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();
+ void resetCachedData();
+
+ const std::unique_ptr d;
+
+ friend class tst_QXmppMixManager;
+};
+
+#endif // QXMPPMIXMANAGER_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index ed964e97a..4c87fa425 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -51,6 +51,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..144586574
--- /dev/null
+++ b/tests/qxmppmixmanager/tst_qxmppmixmanager.cpp
@@ -0,0 +1,1546 @@
+// SPDX-FileCopyrightText: 2023 Melvin Keskin
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppDiscoveryManager.h"
+#include "QXmppMixInfoItem.h"
+#include "QXmppMixInvitation.h"
+#include "QXmppMixIq.h"
+#include "QXmppMixManager.h"
+#include "QXmppMixParticipantItem.h"
+#include "QXmppPubSubEvent.h"
+#include "QXmppPubSubManager.h"
+
+#include "TestClient.h"
+
+struct Tester {
+ Tester()
+ {
+ client.addNewExtension();
+ client.addNewExtension();
+ manager = client.addNewExtension();
+ }
+
+ Tester(const QString &jid)
+ : Tester()
+ {
+ client.configuration().setJid(jid);
+ }
+
+ TestClient client;
+ QXmppMixManager *manager;
+};
+
+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 testResetCachedData();
+ Q_SLOT void testHandleDiscoInfo();
+ Q_SLOT void testAddJidToNode();
+ Q_SLOT void testRequestJids();
+ Q_SLOT void testJoinChannelPrivate();
+ Q_SLOT void testPrepareJoinIq();
+ Q_SLOT void testHandlePubSubEvent();
+ Q_SLOT void testOnRegistered();
+ Q_SLOT void testOnUnregistered();
+ 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 testJoinChannelViaInvitation();
+ Q_SLOT void testJoinChannelViaInvitationWithNickname();
+ Q_SLOT void testJoinChannelViaInvitationWithNodes();
+ Q_SLOT void testUpdateNickname();
+ Q_SLOT void testUpdateSubscriptions();
+ Q_SLOT void testRequestInvitation();
+ 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::testResetCachedData()
+{
+ QXmppMixManager manager;
+
+ QXmppMixManager::Service service;
+ service.jid = QStringLiteral("mix.shakespeare.example");
+
+ manager.setSupportedByServer(true);
+ manager.setArchivingSupportedByServer(true);
+ manager.addService(service);
+
+ manager.resetCachedData();
+
+ QVERIFY(!manager.supportedByServer());
+ QVERIFY(!manager.archivingSupportedByServer());
+ QVERIFY(manager.services().isEmpty());
+}
+
+void tst_QXmppMixManager::testHandleDiscoInfo()
+{
+ auto [client, manager] = Tester(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::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.setSubscriptions(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.subscriptions, 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.subscriptions(), 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 participantReceivedSpy(&manager, &QXmppMixManager::participantReceived);
+ 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 QVector eventTypes = { 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(participantReceivedSpy.size(), 2);
+ for (auto i = 0; i < participantReceivedSpy.size(); i++) {
+ const auto &arguments = participantReceivedSpy.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::testOnRegistered()
+{
+ TestClient client;
+ QXmppMixManager manager;
+
+ client.addNewExtension();
+ client.addNewExtension();
+
+ client.configuration().setJid(QStringLiteral("hag66@shakespeare.example"));
+ client.addExtension(&manager);
+
+ QXmppMixManager::Service service;
+ service.jid = QStringLiteral("mix.shakespeare.example");
+
+ manager.setSupportedByServer(true);
+ manager.setArchivingSupportedByServer(true);
+ manager.addService(service);
+
+ client.setStreamManagementState(QXmppClient::NewStream);
+ Q_EMIT client.connected();
+ QVERIFY(!manager.supportedByServer());
+ QVERIFY(!manager.archivingSupportedByServer());
+ QVERIFY(manager.services().isEmpty());
+
+ QXmppDiscoveryIq iq;
+ iq.setFeatures({ QStringLiteral("urn:xmpp:mix:pam:2") });
+ Q_EMIT manager.client()->findExtension()->infoReceived(iq);
+ QVERIFY(manager.supportedByServer());
+}
+
+void tst_QXmppMixManager::testOnUnregistered()
+{
+ QXmppClient client;
+ QXmppMixManager manager;
+
+ client.addNewExtension();
+ client.addNewExtension();
+
+ client.configuration().setJid(QStringLiteral("hag66@shakespeare.example"));
+ client.addExtension(&manager);
+
+ QXmppMixManager::Service service;
+ service.jid = QStringLiteral("mix.shakespeare.example");
+
+ manager.setSupportedByServer(true);
+ manager.setArchivingSupportedByServer(true);
+ manager.addService(service);
+
+ manager.onUnregistered(&client);
+
+ QXmppDiscoveryIq iq;
+ iq.setFeatures({ QStringLiteral("urn:xmpp:mix:pam:2") });
+ Q_EMIT manager.client()->findExtension()->infoReceived(iq);
+ QVERIFY(!manager.supportedByServer());
+
+ QVERIFY(!manager.supportedByServer());
+ QVERIFY(!manager.archivingSupportedByServer());
+ QVERIFY(manager.services().isEmpty());
+
+ manager.setSupportedByServer(true);
+ manager.setArchivingSupportedByServer(true);
+ manager.addService(service);
+
+ Q_EMIT client.disconnected();
+ QVERIFY(manager.supportedByServer());
+ QVERIFY(manager.archivingSupportedByServer());
+ QVERIFY(!manager.services().isEmpty());
+}
+
+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.subscriptions, 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.subscriptions, 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.subscriptions, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence);
+}
+
+void tst_QXmppMixManager::testJoinChannelViaInvitation()
+{
+ 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->joinChannel(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.subscriptions, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence);
+
+ testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("cat@shakespeare.example"));
+}
+
+void tst_QXmppMixManager::testJoinChannelViaInvitationWithNickname()
+{
+ 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->joinChannel(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.subscriptions, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence);
+}
+
+void tst_QXmppMixManager::testJoinChannelViaInvitationWithNodes()
+{
+ 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->joinChannel(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.subscriptions, 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 subscription = expectFutureVariant(task);
+ QCOMPARE(subscription.additions, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence);
+ QCOMPARE(subscription.removals, QXmppMixConfigItem::Node::Configuration | QXmppMixConfigItem::Node::Information);
+
+ testErrorFromChannel(task = call(), client);
+}
+
+void tst_QXmppMixManager::testRequestInvitation()
+{
+ auto tester = Tester();
+ auto &client = tester.client;
+ auto manager = tester.manager;
+ auto logger = client.logger();
+
+ auto call = [&client, manager]() {
+ return manager->requestInvitation(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("cat@shakespeare.example"));
+ };
+
+ auto task = call();
+
+ client.expect(QStringLiteral(""
+ ""
+ "cat@shakespeare.example"
+ ""
+ ""));
+ client.inject(QStringLiteral(""
+ ""
+ ""
+ "hag66@shakespeare.example"
+ "cat@shakespeare.example"
+ "coven@mix.shakespeare.example"
+ "ABCDEF"
+ ""
+ ""
+ ""));
+
+ const auto invitation = expectFutureVariant(task);
+ QCOMPARE(invitation.token(), QStringLiteral("ABCDEF"));
+
+ 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"