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..077042e8f --- /dev/null +++ b/src/client/QXmppMixManager.cpp @@ -0,0 +1,1381 @@ +// 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 +#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::participantUpdated(const QString &channelJid, const QXmppMixParticipantItem &participantItem) +/// +/// Emitted when a user joined a MIX channel or a participant changed their nick. +/// +/// \param channelJid JID of the channel that the user joined or for whom the participant changed +/// their 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::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 participantUpdated(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) +{ + auto itr = std::find_if(d->services.begin(), d->services.end(), [&jid = service.jid](const Service &service) { + return service.jid == jid; + }); + + if (itr == d->services.end()) { + d->services.append(service); + } else if (*itr == service) { + // Do not emit "servicesChanged()". + 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..40d088b31 --- /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 participantUpdated(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 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..f9b241129 --- /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 participantUpdatedSpy(&manager, &QXmppMixManager::participantUpdated); + 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(participantUpdatedSpy.size(), 2); + for (auto i = 0; i < participantUpdatedSpy.size(); i++) { + const auto &arguments = participantUpdatedSpy.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"