From 6466c3a9c9cf2921c55f8ac807c9a7bae25ed106 Mon Sep 17 00:00:00 2001 From: Melvin Keskin Date: Mon, 11 Sep 2023 21:11:43 +0200 Subject: [PATCH] WIP: Implement MIX TODO: Create PR that includes https://github.com/xsf/xeps/pull/919 and a version block --- doc/doap.xml | 18 +- src/CMakeLists.txt | 3 + src/base/QXmppConstants.cpp | 16 +- src/base/QXmppConstants_p.h | 10 + src/base/QXmppMixConfigItem.h | 131 ++ src/base/QXmppMixInfoItem.h | 10 +- src/base/QXmppMixInvitation.cpp | 10 + src/base/QXmppMixInvitation.h | 9 - src/base/QXmppMixIq.cpp | 375 +++- src/base/QXmppMixIq.h | 45 +- src/base/QXmppMixItems.cpp | 924 ++++++++- src/base/QXmppMixParticipantItem.h | 5 +- src/client/QXmppMixManager.cpp | 1452 ++++++++++++++ src/client/QXmppMixManager.h | 158 ++ tests/CMakeLists.txt | 1 + tests/TestClient.h | 2 + tests/qxmppmixiq/tst_qxmppmixiq.cpp | 228 ++- tests/qxmppmixitems/tst_qxmppmixitems.cpp | 246 ++- tests/qxmppmixmanager/tst_qxmppmixmanager.cpp | 1721 +++++++++++++++++ 19 files changed, 5209 insertions(+), 155 deletions(-) create mode 100644 src/base/QXmppMixConfigItem.h create mode 100644 src/client/QXmppMixManager.cpp create mode 100644 src/client/QXmppMixManager.h create mode 100644 tests/qxmppmixmanager/tst_qxmppmixmanager.cpp diff --git a/doc/doap.xml b/doc/doap.xml index 9660268ce..2ca2b464a 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -540,10 +540,10 @@ SPDX-License-Identifier: CC0-1.0 - partial + complete 0.14 1.1 - Only IQ queries implemented + IQ stanzas for participants and channel information since 1.5; Manager since 1.6 @@ -573,10 +573,18 @@ SPDX-License-Identifier: CC0-1.0 - partial + complete 0.5 1.3 - Only IQ queries implemented + Manager since 1.6 + + + + + + complete + 0.3 + 1.6 @@ -585,7 +593,7 @@ SPDX-License-Identifier: CC0-1.0 partial 0.1 1.4 - Only invitations implemented + Only invitations implemented; Manager since 1.6 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b1ab54e06..d7c4525b9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -41,6 +41,7 @@ set(INSTALL_HEADER_FILES base/QXmppMamIq.h base/QXmppMessage.h base/QXmppMessageReaction.h + base/QXmppMixConfigItem.h base/QXmppMixInfoItem.h base/QXmppMixInvitation.h base/QXmppMixIq.h @@ -115,6 +116,7 @@ set(INSTALL_HEADER_FILES client/QXmppMamManager.h client/QXmppMessageHandler.h client/QXmppMessageReceiptManager.h + client/QXmppMixManager.h client/QXmppMucManager.h client/QXmppOutgoingClient.h client/QXmppRegistrationManager.h @@ -252,6 +254,7 @@ set(SOURCE_FILES client/QXmppJingleMessageInitiationManager.cpp client/QXmppMamManager.cpp client/QXmppMessageReceiptManager.cpp + client/QXmppMixManager.cpp client/QXmppMucManager.cpp client/QXmppOutgoingClient.cpp client/QXmppRosterManager.cpp diff --git a/src/base/QXmppConstants.cpp b/src/base/QXmppConstants.cpp index 4e21df3c1..e6595f361 100644 --- a/src/base/QXmppConstants.cpp +++ b/src/base/QXmppConstants.cpp @@ -70,6 +70,9 @@ const char *ns_authFeature = "http://jabber.org/features/iq-auth"; // XEP-0080: User Location const char *ns_geoloc = "http://jabber.org/protocol/geoloc"; const char *ns_geoloc_notify = "http://jabber.org/protocol/geoloc+notify"; +// XEP-0084: User Avatar +const char *ns_user_avatar_data = "urn:xmpp:avatar:data"; +const char *ns_user_avatar_metadata = "urn:xmpp:avatar:metadata"; // XEP-0085: Chat State Notifications const char *ns_chat_states = "http://jabber.org/protocol/chatstates"; // XEP-0091: Legacy Delayed Delivery @@ -176,11 +179,13 @@ const char *ns_message_attaching = "urn:xmpp:message-attaching:1"; const char *ns_mix = "urn:xmpp:mix:core:1"; const char *ns_mix_create_channel = "urn:xmpp:mix:core:1#create-channel"; const char *ns_mix_searchable = "urn:xmpp:mix:core:1#searchable"; +const char *ns_mix_node_allowed = "urn:xmpp:mix:nodes:allowed"; +const char *ns_mix_node_banned = "urn:xmpp:mix:nodes:banned"; +const char *ns_mix_node_config = "urn:xmpp:mix:nodes:config"; +const char *ns_mix_node_info = "urn:xmpp:mix:nodes:info"; const char *ns_mix_node_messages = "urn:xmpp:mix:nodes:messages"; const char *ns_mix_node_participants = "urn:xmpp:mix:nodes:participants"; const char *ns_mix_node_presence = "urn:xmpp:mix:nodes:presence"; -const char *ns_mix_node_config = "urn:xmpp:mix:nodes:config"; -const char *ns_mix_node_info = "urn:xmpp:mix:nodes:info"; // XEP-0373: OpenPGP for XMPP const char *ns_ox = "urn:xmpp:openpgp:0"; // XEP-0380: Explicit Message Encryption @@ -193,10 +198,15 @@ const char *ns_omemo_1 = "urn:xmpp:omemo:1"; const char *ns_omemo_2 = "urn:xmpp:omemo:2"; const char *ns_omemo_2_bundles = "urn:xmpp:omemo:2:bundles"; const char *ns_omemo_2_devices = "urn:xmpp:omemo:2:devices"; +// XEP-0404: Mediated Information eXchange (MIX): JID Hidden Channels +const char *ns_mix_node_jidmap = "urn:xmpp:mix:nodes:jidmap"; // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements -const char *ns_mix_pam = "urn:xmpp:mix:pam:1"; +const char *ns_mix_pam = "urn:xmpp:mix:pam:2"; +const char *ns_mix_pam_archiving = "urn:xmpp:mix:pam:2#archive"; const char *ns_mix_roster = "urn:xmpp:mix:roster:0"; const char *ns_mix_presence = "urn:xmpp:presence:0"; +// XEP-0406: Mediated Information eXchange (MIX): MIX Administration +const char *ns_mix_admin = "urn:xmpp:mix:admin:0"; // XEP-0407: Mediated Information eXchange (MIX): Miscellaneous Capabilities const char *ns_mix_misc = "urn:xmpp:mix:misc:0"; // XEP-0428: Fallback Indication diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h index 1b5ddf313..b7dc3dcf4 100644 --- a/src/base/QXmppConstants_p.h +++ b/src/base/QXmppConstants_p.h @@ -82,6 +82,9 @@ extern const char *ns_authFeature; // XEP-0080: User Location extern const char *ns_geoloc; extern const char *ns_geoloc_notify; +// XEP-0084: User Avatar +extern const char *ns_user_avatar_data; +extern const char *ns_user_avatar_metadata; // XEP-0085: Chat State Notifications extern const char *ns_chat_states; // XEP-0091: Legacy Delayed Delivery @@ -193,6 +196,8 @@ extern const char *ns_mix_node_participants; extern const char *ns_mix_node_presence; extern const char *ns_mix_node_config; extern const char *ns_mix_node_info; +extern const char *ns_mix_node_allowed; +extern const char *ns_mix_node_banned; // XEP-0373: OpenPGP for XMPP extern const char *ns_ox; // XEP-0380: Explicit Message Encryption @@ -205,10 +210,15 @@ extern const char *ns_omemo_1; extern const char *ns_omemo_2; extern const char *ns_omemo_2_bundles; extern const char *ns_omemo_2_devices; +// XEP-0404: Mediated Information eXchange (MIX): JID Hidden Channels +extern const char *ns_mix_node_jidmap; // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements extern const char *ns_mix_pam; +extern const char *ns_mix_pam_archiving; extern const char *ns_mix_roster; extern const char *ns_mix_presence; +// XEP-0406: Mediated Information eXchange (MIX): MIX Administration +extern const char *ns_mix_admin; // XEP-0407: Mediated Information eXchange (MIX): Miscellaneous Capabilities extern const char *ns_mix_misc; // XEP-0428: Fallback Indication diff --git a/src/base/QXmppMixConfigItem.h b/src/base/QXmppMixConfigItem.h new file mode 100644 index 000000000..9c8825a62 --- /dev/null +++ b/src/base/QXmppMixConfigItem.h @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2023 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "QXmppDataForm.h" +#include "QXmppPubSubBaseItem.h" + +class QXmppMixConfigItemPrivate; + +class QXMPP_EXPORT QXmppMixConfigItem : public QXmppPubSubBaseItem +{ +public: + enum class Role { + None, + Owner, + Administrator, + Participant, + Allowed, + Anyone, + Nobody, + }; + + enum class Node { + AllowedJids = 1 << 0, + AvatarData = 1 << 1, + AvatarMetadata = 1 << 2, + BannedJids = 1 << 3, + Configuration = 1 << 4, + Information = 1 << 5, + JidMap = 1 << 6, + Messages = 1 << 7, + Participants = 1 << 8, + Presence = 1 << 9, + }; + Q_DECLARE_FLAGS(Nodes, Node) + + QXmppMixConfigItem(); + QXmppMixConfigItem(const QXmppMixConfigItem &); + QXmppMixConfigItem(QXmppMixConfigItem &&); + ~QXmppMixConfigItem(); + + QXmppMixConfigItem &operator=(const QXmppMixConfigItem &); + QXmppMixConfigItem &operator=(QXmppMixConfigItem &&); + + QXmppDataForm::Type formType() const; + void setFormType(QXmppDataForm::Type formType); + + QString lastEditorJid() const; + void setLastEditorJid(const QString &lastEditorJid); + + QStringList ownerJids() const; + void setOwnerJids(const QStringList &ownerJids); + + QStringList administratorJids() const; + void setAdministratorJids(const QStringList &administratorJids); + + QDateTime channelDeletion() const; + void setChannelDeletion(const QDateTime &channelDeletion); + + Nodes nodes() const; + void setNodes(Nodes nodes); + + Role messagesSubscribeRole() const; + void setMessagesSubscribeRole(Role messagesSubscribeRole); + + Role messagesRetractRole() const; + void setMessagesRetractRole(Role messagesRetractRole); + + Role presenceSubscribeRole() const; + void setPresenceSubscribeRole(Role presenceSubscribeRole); + + Role participantsSubscribeRole() const; + void setParticipantsSubscribeRole(Role participantsSubscribeRole); + + Role informationSubscribeRole() const; + void setInformationSubscribeRole(Role informationSubscribeRole); + + Role informationUpdateRole() const; + void setInformationUpdateRole(Role informationUpdateRole); + + Role allowedJidsSubscribeRole() const; + void setAllowedJidsSubscribeRole(Role allowedJidsSubscribeRole); + + Role bannedJidsSubscribeRole() const; + void setBannedJidsSubscribeRole(Role bannedJidsSubscribeRole); + + Role configurationReadRole() const; + void setConfigurationReadRole(Role configurationReadRole); + + Role avatarUpdateRole() const; + void setAvatarUpdateRole(Role avatarUpdateRole); + + std::optional nicknameRequired() const; + void setNicknameRequired(std::optional nicknameRequired); + + std::optional presenceRequired() const; + void setPresenceRequired(std::optional presenceRequired); + + std::optional onlyParticipantsPermittedToSubmitPresence() const; + void setOnlyParticipantsPermittedToSubmitPresence(std::optional onlyParticipantsPermittedToSubmitPresence); + + std::optional ownMessageRetractionPermitted() const; + void setOwnMessageRetractionPermitted(std::optional ownMessageRetractionPermitted); + + std::optional invitationsPermitted() const; + void setInvitationsPermitted(std::optional invitationsPermitted); + + std::optional privateMessagesPermitted() const; + void setPrivateMessagesPermitted(std::optional privateMessagesPermitted); + + static bool isItem(const QDomElement &itemElement); + +protected: + /// \cond + void parsePayload(const QDomElement &payloadElement) override; + void serializePayload(QXmlStreamWriter *writer) const override; + /// \endcond + +private: + QSharedDataPointer d; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QXmppMixConfigItem::Nodes) +/// \cond +// Scoped enums (enum class) are not implicitly converted to int. +inline uint qHash(QXmppMixConfigItem::Node key, uint seed) noexcept { return qHash(std::underlying_type_t(key), seed); } +/// \endcond + +Q_DECLARE_METATYPE(QXmppMixConfigItem) diff --git a/src/base/QXmppMixInfoItem.h b/src/base/QXmppMixInfoItem.h index 313c3e145..481087237 100644 --- a/src/base/QXmppMixInfoItem.h +++ b/src/base/QXmppMixInfoItem.h @@ -1,10 +1,11 @@ // SPDX-FileCopyrightText: 2019 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifndef QXMPPMIXINFOITEM_H -#define QXMPPMIXINFOITEM_H +#pragma once +#include "QXmppDataForm.h" #include "QXmppPubSubBaseItem.h" class QXmppMixInfoItemPrivate; @@ -20,6 +21,9 @@ class QXMPP_EXPORT QXmppMixInfoItem : public QXmppPubSubBaseItem QXmppMixInfoItem &operator=(const QXmppMixInfoItem &); QXmppMixInfoItem &operator=(QXmppMixInfoItem &&); + QXmppDataForm::Type formType() const; + void setFormType(QXmppDataForm::Type formType); + const QString &name() const; void setName(QString); @@ -41,4 +45,4 @@ class QXMPP_EXPORT QXmppMixInfoItem : public QXmppPubSubBaseItem QSharedDataPointer d; }; -#endif // QXMPPMIXINFOITEM_H +Q_DECLARE_METATYPE(QXmppMixInfoItem) diff --git a/src/base/QXmppMixInvitation.cpp b/src/base/QXmppMixInvitation.cpp index cc4109f12..776c06bef 100644 --- a/src/base/QXmppMixInvitation.cpp +++ b/src/base/QXmppMixInvitation.cpp @@ -19,6 +19,16 @@ class QXmppMixInvitationPrivate : public QSharedData QString token; }; +/// +/// \brief The QXmppMixInvitation class is used to invite a user to a +/// \xep{0369}: Mediated Information eXchange (MIX) channel as defined by +/// \xep{0407}: Mediated Information eXchange (MIX): Miscellaneous Capabilities. +/// +/// \ingroup Stanzas +/// +/// \since QXmpp 1.4 +/// + /// /// Default constructor /// diff --git a/src/base/QXmppMixInvitation.h b/src/base/QXmppMixInvitation.h index e01d81ace..e0fd681de 100644 --- a/src/base/QXmppMixInvitation.h +++ b/src/base/QXmppMixInvitation.h @@ -11,15 +11,6 @@ class QXmppMixInvitationPrivate; -/// -/// \brief The QXmppMixInvitation class is used to invite a user to a -/// \xep{0369}: Mediated Information eXchange (MIX) channel as defined by -/// \xep{0407}: Mediated Information eXchange (MIX): Miscellaneous Capabilities. -/// -/// \ingroup Stanzas -/// -/// \since QXmpp 1.4 -/// class QXMPP_EXPORT QXmppMixInvitation { public: diff --git a/src/base/QXmppMixIq.cpp b/src/base/QXmppMixIq.cpp index 6af9bd924..726d0c596 100644 --- a/src/base/QXmppMixIq.cpp +++ b/src/base/QXmppMixIq.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2019 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin // // SPDX-License-Identifier: LGPL-2.1-or-later @@ -25,19 +26,54 @@ static const QStringList MIX_ACTION_TYPES = { QStringLiteral("destroy") }; +static const QMap NODES = { + { QXmppMixConfigItem::Node::AllowedJids, ns_mix_node_allowed }, + { QXmppMixConfigItem::Node::AvatarData, ns_user_avatar_data }, + { QXmppMixConfigItem::Node::AvatarMetadata, ns_user_avatar_metadata }, + { QXmppMixConfigItem::Node::BannedJids, ns_mix_node_banned }, + { QXmppMixConfigItem::Node::Configuration, ns_mix_node_config }, + { QXmppMixConfigItem::Node::Information, ns_mix_node_info }, + { QXmppMixConfigItem::Node::JidMap, ns_mix_node_jidmap }, + { QXmppMixConfigItem::Node::Messages, ns_mix_node_messages }, + { QXmppMixConfigItem::Node::Participants, ns_mix_node_participants }, + { QXmppMixConfigItem::Node::Presence, ns_mix_node_presence }, +}; + class QXmppMixIqPrivate : public QSharedData { public: - QString jid; - QString channelName; - QStringList nodes; + QString participantId; + QString channelId; + QString channelJid; + QXmppMixConfigItem::Nodes nodesBeingSubscribedTo; + QXmppMixConfigItem::Nodes nodesBeingUnsubscribedFrom; QString nick; QString inviteeJid; std::optional invitation; QXmppMixIq::Type actionType = QXmppMixIq::None; }; +/// +/// \class QXmppMixIq +/// +/// This class represents an IQ used to do actions on a MIX channel as defined by +/// \xep{0369, Mediated Information eXchange (MIX)}, +/// \xep{0405, Mediated Information eXchange (MIX): Participant Server Requirements} and +/// \xep{0407, Mediated Information eXchange (MIX): Miscellaneous Capabilities}. +/// +/// \since QXmpp 1.1 +/// +/// \ingroup Stanzas +/// + +/// +/// \enum QXmppMixIq::Type +/// +/// Action type of the MIX IQ stanza. +/// + QXmppMixIq::QXmppMixIq() + // : QXmppIq(), d(new QXmppMixIqPrivate) : d(new QXmppMixIqPrivate) { } @@ -46,69 +82,253 @@ QXmppMixIq::QXmppMixIq() QXmppMixIq::QXmppMixIq(const QXmppMixIq &) = default; /// Default move-constructor QXmppMixIq::QXmppMixIq(QXmppMixIq &&) = default; +// QXmppMixIq::QXmppMixIq(const QXmppMixIq &other) = default; + QXmppMixIq::~QXmppMixIq() = default; /// Default assignment operator QXmppMixIq &QXmppMixIq::operator=(const QXmppMixIq &) = default; /// Default move-assignment operator QXmppMixIq &QXmppMixIq::operator=(QXmppMixIq &&) = default; +// QXmppMixIq& QXmppMixIq::operator=(const QXmppMixIq &other) = default; -/// Returns the channel JID. It also contains a participant id for Join/ -/// ClientJoin results. - +/// +/// Returns the channel JID, in case of a Join/ClientJoin query result, containing the participant +/// ID. +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use \c QXmppMixIq::channelJid() and +/// \c QXmppMixIq::participantId() instead. +/// QString QXmppMixIq::jid() const { - return d->jid; -} + if (d->participantId.isEmpty()) { + return d->channelJid; + } -/// Sets the channel JID. For results of Join/ClientJoin queries this also -/// needs to contain a participant id. + if (d->channelJid.isEmpty()) { + return {}; + } + + return d->participantId + "#" + d->channelJid; +} +/// +/// Sets the channel JID, in case of a Join/ClientJoin query result, containing the participant ID. +/// +/// \param jid channel JID including a possible participant ID +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use \c QXmppMixIq::setChannelJid() and +/// \c QXmppMixIq::setParticipantId() instead. +/// void QXmppMixIq::setJid(const QString &jid) { - d->jid = jid; + const auto jidParts = jid.split("#"); + + if (jidParts.size() == 1) { + d->channelJid = jid; + } else if (jidParts.size() == 2) { + d->participantId = jidParts.at(0); + d->channelJid = jidParts.at(1); + } } -/// Returns the channel name (the name part of the channel JID). This may still -/// be empty, if a JID was set. +/// +/// Returns the participant ID for a Join/ClientJoin result. +/// +/// \return the participant ID +/// +/// \since QXmpp 1.6 +/// +QString QXmppMixIq::participantId() const +{ + return d->participantId; +} -QString QXmppMixIq::channelName() const +/// +/// Sets the participant ID for a Join/ClientJoin result. +/// +/// @param participantId ID of the user in the channel +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setParticipantId(const QString &participantId) { - return d->channelName; + d->participantId = participantId; } -/// Sets the channel name for creating/destroying specific channels. When you -/// create a new channel, this can also be left empty to let the server -/// generate a name. +/// +/// Returns the channel's ID (the local part of the channel JID). +/// +/// It can be empty if a JID was set. +/// +/// \return the ID of the channel +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use \c QXmppMixIq::channelId() instead. +/// +QString QXmppMixIq::channelName() const +{ + return d->channelId; +} +/// +/// Sets the channel's ID (the local part of the channel JID) for creating or destroying a channel. +/// +/// If you create a new channel, the channel ID can be left empty to let the server generate an ID. +/// +/// \param channelName ID of the channel +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use \c QXmppMixIq::setChannelId() +/// instead. +/// void QXmppMixIq::setChannelName(const QString &channelName) { - d->channelName = channelName; + d->channelId = channelName; } -/// Returns the list of nodes to subscribe to. +/// +/// Returns the channel's ID (the local part of the channel JID). +/// +/// It can be empty if a JID was set. +/// +/// \return the ID of the channel +/// +/// \since QXmpp 1.6 +/// +QString QXmppMixIq::channelId() const +{ + return d->channelId; +} -QStringList QXmppMixIq::nodes() const +/// +/// Sets the channel's ID (the local part of the channel JID) for creating or destroying a channel. +/// +/// If you create a new channel, the channel ID can be left empty to let the server generate an ID. +/// +/// @param channelId channel ID to be set +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setChannelId(const QString &channelId) { - return d->nodes; + d->channelId = channelId; } -/// Sets the nodes to subscribe to. Note that for UpdateSubscription queries -/// you only need to include the new subscriptions. +/// +/// Returns the channel's JID. +/// +/// \return the JID of the channel +/// +/// \since QXmpp 1.6 +/// +QString QXmppMixIq::channelJid() const +{ + return d->channelJid; +} + +/// +/// Sets the channel's JID. +/// +/// @param channelJid JID to be set +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setChannelJid(const QString &channelJid) +{ + d->channelJid = channelJid; +} +/// +/// Returns the nodes being subscribed to. +/// +/// \return the nodes being subscribed to +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use +/// \c QXmppMixIq::nodesBeingSubscribedTo() instead. +/// +QStringList QXmppMixIq::nodes() const +{ + return nodesToList(d->nodesBeingSubscribedTo).toList(); +} + +/// +/// Sets the nodes being subscribe to. +/// +/// In case of an UpdateSubscription query, you only need to set new subscriptions. +/// +/// \param nodes nodes being subscribed to +/// +/// \deprecated This method is deprecated since QXmpp 1.6. Use +/// \c QXmppMixIq::setNodesBeingSubscribedTo() instead. +/// void QXmppMixIq::setNodes(const QStringList &nodes) { - d->nodes = nodes; + d->nodesBeingSubscribedTo = listToNodes(nodes.toVector()); } -/// Returns the user's nickname in the channel. +/// +/// Returns the nodes to subscribe to. +/// +/// \return the nodes being subscribed to +/// +/// \since QXmpp 1.6 +/// +QXmppMixConfigItem::Nodes QXmppMixIq::nodesBeingSubscribedTo() const +{ + return d->nodesBeingSubscribedTo; +} +/// +/// Sets the nodes to subscribe to. +/// +/// In case of an UpdateSubscription query, you only need to set new subscriptions. +/// +/// \param nodes nodes being subscribed to +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setNodesBeingSubscribedTo(QXmppMixConfigItem::Nodes nodes) +{ + d->nodesBeingSubscribedTo = nodes; +} + +/// +/// Returns the nodes to unsubscribe from. +/// +/// \return the nodes being unsubscribed from +/// +/// \since QXmpp 1.6 +/// +QXmppMixConfigItem::Nodes QXmppMixIq::nodesBeingUnsubscribedFrom() const +{ + return d->nodesBeingUnsubscribedFrom; +} + +/// +/// Sets the nodes to unsubscribe from. +/// +/// \param nodes nodes being unsubscribed from +/// +/// \since QXmpp 1.6 +/// +void QXmppMixIq::setNodesBeingUnsubscribedFrom(QXmppMixConfigItem::Nodes nodes) +{ + d->nodesBeingUnsubscribedFrom = nodes; +} + +/// +/// Returns the user's nickname in the channel. +/// +/// \return the nickname of the user +/// QString QXmppMixIq::nick() const { return d->nick; } -/// Sets the nickname for the channel. - +/// +/// Sets the user's nickname used for the channel. +/// +/// \param nick nick of the user to be set +/// void QXmppMixIq::setNick(const QString &nick) { d->nick = nick; @@ -174,20 +394,65 @@ void QXmppMixIq::setInvitation(const std::optional &invitati d->invitation = invitation; } -/// Returns the MIX channel action type. - +/// Returns the MIX channel's action type. +/// +/// \return the action type of the channel +/// QXmppMixIq::Type QXmppMixIq::actionType() const { return d->actionType; } -/// Sets the channel action. - +/// +/// Sets the MIX channel's action type. +/// +/// \param type action type of the channel +/// void QXmppMixIq::setActionType(QXmppMixIq::Type type) { d->actionType = type; } +/// +/// Converts a nodes flag to a list of nodes. +/// +/// \param nodes nodes to convert +/// +/// \return the list of nodes +/// +QVector QXmppMixIq::nodesToList(QXmppMixConfigItem::Nodes nodes) +{ + QVector nodeList; + + for (auto itr = NODES.cbegin(); itr != NODES.cend(); ++itr) { + if (nodes.testFlag(itr.key())) { + nodeList.append(itr.value()); + } + } + + return nodeList; +} + +/// +/// Converst a list of nodes to a nodes flag +/// +/// \param nodeList list of nodes to convert +/// +/// \return the nodes flag +/// +QXmppMixConfigItem::Nodes QXmppMixIq::listToNodes(const QVector &nodeList) +{ + QXmppMixConfigItem::Nodes nodes; + + for (auto itr = NODES.cbegin(); itr != NODES.cend(); ++itr) { + if (nodeList.contains(itr.value())) { + nodes |= itr.key(); + } + } + + return nodes; +} + /// \cond bool QXmppMixIq::isMixIq(const QDomElement &element) { @@ -214,30 +479,38 @@ void QXmppMixIq::parseElementFromChild(const QDomElement &element) return; } - if (auto index = MIX_ACTION_TYPES.indexOf(child.tagName()); index >= 0) { - d->actionType = Type(index); - } + const auto actionTypeIndex = MIX_ACTION_TYPES.indexOf(child.tagName()); + d->actionType = actionTypeIndex == -1 ? None : (QXmppMixIq::Type)actionTypeIndex; if (child.namespaceURI() == ns_mix_pam) { if (child.hasAttribute(QStringLiteral("channel"))) { - d->jid = child.attribute(QStringLiteral("channel")); + d->channelJid = child.attribute(QStringLiteral("channel")); } child = child.firstChildElement(); } if (!child.isNull() && child.namespaceURI() == ns_mix) { - if (child.hasAttribute(QStringLiteral("jid"))) { - d->jid = child.attribute(QStringLiteral("jid")); + // TODO: Will those attributes finally be adapted by the XEP? + if (child.hasAttribute(QStringLiteral("id"))) { + d->participantId = child.attribute(QStringLiteral("id")); + } + if (child.hasAttribute(QStringLiteral("jid")) && d->actionType != QXmppMixIq::UpdateSubscription) { + d->channelJid = (child.attribute(QStringLiteral("jid"))).split("#").last(); } if (child.hasAttribute(QStringLiteral("channel"))) { - d->channelName = child.attribute(QStringLiteral("channel")); + d->channelId = child.attribute(QStringLiteral("channel")); } QDomElement subChild = child.firstChildElement(); + QVector nodesBeingSubscribedTo; + QVector nodesBeingUnsubscribedFrom; + while (!subChild.isNull()) { if (subChild.tagName() == QStringLiteral("subscribe")) { - d->nodes << subChild.attribute(QStringLiteral("node")); + nodesBeingSubscribedTo << subChild.attribute(QStringLiteral("node")); + } else if (subChild.tagName() == QStringLiteral("unsubscribe")) { + nodesBeingUnsubscribedFrom << subChild.attribute(QStringLiteral("node")); } else if (subChild.tagName() == QStringLiteral("nick")) { d->nick = subChild.text(); } else if (subChild.tagName() == QStringLiteral("invitation")) { @@ -247,6 +520,9 @@ void QXmppMixIq::parseElementFromChild(const QDomElement &element) subChild = subChild.nextSiblingElement(); } + + d->nodesBeingSubscribedTo = listToNodes(nodesBeingSubscribedTo); + d->nodesBeingUnsubscribedFrom = listToNodes(nodesBeingUnsubscribedFrom); } } @@ -274,9 +550,8 @@ void QXmppMixIq::toXmlElementFromChild(QXmlStreamWriter *writer) const if (d->actionType == ClientJoin || d->actionType == ClientLeave) { writer->writeDefaultNamespace(ns_mix_pam); if (type() == Set) { - helperToXmlAddAttribute(writer, QStringLiteral("channel"), d->jid); + helperToXmlAddAttribute(writer, QStringLiteral("channel"), d->channelJid); } - if (d->actionType == ClientJoin) { writer->writeStartElement(QStringLiteral("join")); } else if (d->actionType == ClientLeave) { @@ -285,16 +560,25 @@ void QXmppMixIq::toXmlElementFromChild(QXmlStreamWriter *writer) const } writer->writeDefaultNamespace(ns_mix); - helperToXmlAddAttribute(writer, QStringLiteral("channel"), d->channelName); + helperToXmlAddAttribute(writer, QStringLiteral("channel"), d->channelId); if (type() == Result) { - helperToXmlAddAttribute(writer, QStringLiteral("jid"), d->jid); + helperToXmlAddAttribute(writer, QStringLiteral("id"), d->participantId); } - for (const auto &node : d->nodes) { + const auto nodesBeingSubscribedTo = nodesToList(d->nodesBeingSubscribedTo); + for (const auto &node : nodesBeingSubscribedTo) { writer->writeStartElement(QStringLiteral("subscribe")); writer->writeAttribute(QStringLiteral("node"), node); writer->writeEndElement(); } + + const auto nodesBeingUnsubscribedFrom = nodesToList(d->nodesBeingUnsubscribedFrom); + for (const auto &node : nodesBeingUnsubscribedFrom) { + writer->writeStartElement(QStringLiteral("unsubscribe")); + writer->writeAttribute(QStringLiteral("node"), node); + writer->writeEndElement(); + } + if (!d->nick.isEmpty()) { writer->writeTextElement(QStringLiteral("nick"), d->nick); } @@ -304,6 +588,7 @@ void QXmppMixIq::toXmlElementFromChild(QXmlStreamWriter *writer) const } writer->writeEndElement(); + if (d->actionType == ClientJoin || d->actionType == ClientLeave) { writer->writeEndElement(); } diff --git a/src/base/QXmppMixIq.h b/src/base/QXmppMixIq.h index 340c41f8f..54e8370e8 100644 --- a/src/base/QXmppMixIq.h +++ b/src/base/QXmppMixIq.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2019 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin // // SPDX-License-Identifier: LGPL-2.1-or-later @@ -6,26 +7,16 @@ #define QXMPPMIXIQ_H #include "QXmppIq.h" +#include "QXmppMixConfigItem.h" #include class QXmppMixInvitation; class QXmppMixIqPrivate; -/// -/// \brief The QXmppMixIq class represents an IQ used to do actions on a MIX -/// channel as defined by \xep{0369, Mediated Information eXchange (MIX)}, -/// \xep{0405, Mediated Information eXchange (MIX): Participant Server -/// Requirements} and \xep{0407, Mediated Information eXchange (MIX): Miscellaneous Capabilities}. -/// -/// \since QXmpp 1.1 -/// -/// \ingroup Stanzas -/// class QXMPP_EXPORT QXmppMixIq : public QXmppIq { public: - /// The action type of the MIX query IQ. enum Type { None, ClientJoin, @@ -52,16 +43,31 @@ class QXMPP_EXPORT QXmppMixIq : public QXmppIq void setActionType(QXmppMixIq::Type); QString jid() const; - void setJid(const QString &); + void setJid(const QString &jid); QString channelName() const; - void setChannelName(const QString &); + void setChannelName(const QString &channelName); + + QString participantId() const; + void setParticipantId(const QString &participantId); + + QString channelId() const; + void setChannelId(const QString &channelId); + + QString channelJid() const; + void setChannelJid(const QString &channelJid); QStringList nodes() const; - void setNodes(const QStringList &); + void setNodes(const QStringList &nodes); + + QXmppMixConfigItem::Nodes nodesBeingSubscribedTo() const; + void setNodesBeingSubscribedTo(QXmppMixConfigItem::Nodes nodes); + + QXmppMixConfigItem::Nodes nodesBeingUnsubscribedFrom() const; + void setNodesBeingUnsubscribedFrom(QXmppMixConfigItem::Nodes nodes); QString nick() const; - void setNick(const QString &); + void setNick(const QString &nick); QString inviteeJid() const; void setInviteeJid(const QString &inviteeJid); @@ -69,14 +75,17 @@ class QXMPP_EXPORT QXmppMixIq : public QXmppIq std::optional invitation() const; void setInvitation(const std::optional &invitation); + static QVector nodesToList(QXmppMixConfigItem::Nodes nodes); + static QXmppMixConfigItem::Nodes listToNodes(const QVector &nodeList); + /// \cond - static bool isMixIq(const QDomElement &); + static bool isMixIq(const QDomElement &element); /// \endcond protected: /// \cond - void parseElementFromChild(const QDomElement &) override; - void toXmlElementFromChild(QXmlStreamWriter *) const override; + void parseElementFromChild(const QDomElement &element) override; + void toXmlElementFromChild(QXmlStreamWriter *writer) const override; /// \endcond private: diff --git a/src/base/QXmppMixItems.cpp b/src/base/QXmppMixItems.cpp index a19edbb0d..505395253 100644 --- a/src/base/QXmppMixItems.cpp +++ b/src/base/QXmppMixItems.cpp @@ -1,19 +1,912 @@ // SPDX-FileCopyrightText: 2019 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin // // SPDX-License-Identifier: LGPL-2.1-or-later #include "QXmppConstants_p.h" #include "QXmppDataFormBase.h" +#include "QXmppMixConfigItem.h" #include "QXmppMixInfoItem.h" #include "QXmppMixParticipantItem.h" +#include + static const auto NAME = QStringLiteral("Name"); static const auto DESCRIPTION = QStringLiteral("Description"); static const auto CONTACT_JIDS = QStringLiteral("Contact"); +static const auto LAST_EDITOR_JID_KEY = QStringLiteral("Last Change Made By"); +static const auto OWNER_JIDS_KEY = QStringLiteral("Owner"); +static const auto ADMINISTRATOR_JIDS_KEY = QStringLiteral("Administrator"); +static const auto CHANNEL_DELETION_KEY = QStringLiteral("End of Life"); +static const auto NODES_KEY = QStringLiteral("Nodes Present"); +static const auto MESSAGES_SUBSCRIBE_ROLE_KEY = QStringLiteral("Messages Node Subscription"); +static const auto MESSAGES_RETRACT_ROLE_KEY = QStringLiteral("Administrator Message Retraction Rights"); +static const auto PRESENCE_SUBSCRIBE_ROLE_KEY = QStringLiteral("Presence Node Subscription"); +static const auto PARTICIPANTS_SUBSCRIBE_ROLE_KEY = QStringLiteral("Participants Node Subscription"); +static const auto INFORMATION_SUBSCRIBE_ROLE_KEY = QStringLiteral("Information Node Subscription"); +static const auto INFORMATION_UPDATE_ROLE_KEY = QStringLiteral("Information Node Update Rights"); +static const auto ALLOWED_JIDS_SUBSCRIBE_ROLE_KEY = QStringLiteral("Allowed Node Subscription"); +static const auto BANNED_JIDS_SUBSCRIBE_ROLE_KEY = QStringLiteral("Banned Node Subscription"); +static const auto CONFIGURATION_READ_ROLE_KEY = QStringLiteral("Configuration Node Access"); +static const auto AVATARS_UPDATE_ROLE_KEY = QStringLiteral("Avatar Nodes Update Rights"); +static const auto NICKNAME_REQUIRED_KEY = QStringLiteral("Mandatory Nicks"); +static const auto PRESENCE_REQUIRED_KEY = QStringLiteral("Participants Must Provide Presence"); +static const auto ONLY_PARTICIPANTS_PERMITTED_TO_SUBMIT_PRESENCE_KEY = QStringLiteral("Open Presence"); +static const auto OWN_MESSAGE_RETRACTION_PERMITTED_KEY = QStringLiteral("User Message Retraction"); +static const auto INVITATIONS_PERMITTED_KEY = QStringLiteral("Participation Addition by Invitation from Participant"); +static const auto PRIVATE_MESSAGES_PERMITTED_KEY = QStringLiteral("Private Messages"); + +static const QMap ROLES = { + { QXmppMixConfigItem::Role::None, {} }, + { QXmppMixConfigItem::Role::Owner, QStringLiteral("owners") }, + { QXmppMixConfigItem::Role::Administrator, QStringLiteral("admins") }, + { QXmppMixConfigItem::Role::Participant, QStringLiteral("participants") }, + { QXmppMixConfigItem::Role::Allowed, QStringLiteral("allowed") }, + { QXmppMixConfigItem::Role::Anyone, QStringLiteral("anyone") }, + { QXmppMixConfigItem::Role::Nobody, QStringLiteral("nobody") }, +}; + +static const QMap NODES = { + { QXmppMixConfigItem::Node::AllowedJids, QStringLiteral("allowed") }, + { QXmppMixConfigItem::Node::AvatarData, QStringLiteral("avatar") }, + { QXmppMixConfigItem::Node::AvatarMetadata, QStringLiteral("avatar") }, + { QXmppMixConfigItem::Node::BannedJids, QStringLiteral("banned") }, + { QXmppMixConfigItem::Node::Information, QStringLiteral("information") }, + { QXmppMixConfigItem::Node::JidMap, QStringLiteral("jidmap-visible") }, + { QXmppMixConfigItem::Node::Participants, QStringLiteral("participants") }, + { QXmppMixConfigItem::Node::Presence, QStringLiteral("presence") }, +}; + +class QXmppMixConfigItemPrivate : public QSharedData, public QXmppDataFormBase +{ +public: + QXmppDataForm::Type dataFormType = QXmppDataForm::None; + QString lastEditorJid; + QStringList ownerJids; + QStringList administratorJids; + QDateTime channelDeletion; + QXmppMixConfigItem::Nodes nodes; + QXmppMixConfigItem::Role messagesSubscribeRole = QXmppMixConfigItem::Role::None; + QXmppMixConfigItem::Role messagesRetractRole = QXmppMixConfigItem::Role::None; + QXmppMixConfigItem::Role presenceSubscribeRole = QXmppMixConfigItem::Role::None; + QXmppMixConfigItem::Role participantsSubscribeRole = QXmppMixConfigItem::Role::None; + QXmppMixConfigItem::Role informationSubscribeRole = QXmppMixConfigItem::Role::None; + QXmppMixConfigItem::Role informationUpdateRole = QXmppMixConfigItem::Role::None; + QXmppMixConfigItem::Role allowedJidsSubscribeRole = QXmppMixConfigItem::Role::None; + QXmppMixConfigItem::Role bannedJidsSubscribeRole = QXmppMixConfigItem::Role::None; + QXmppMixConfigItem::Role configurationReadRole = QXmppMixConfigItem::Role::None; + QXmppMixConfigItem::Role avatarUpdateRole = QXmppMixConfigItem::Role::None; + std::optional nicknameRequired; + std::optional presenceRequired; + std::optional onlyParticipantsPermittedToSubmitPresence; + std::optional ownMessageRetractionPermitted; + std::optional invitationsPermitted; + std::optional privateMessagesPermitted; + + ~QXmppMixConfigItemPrivate() override = default; + + void reset() + { + dataFormType = QXmppDataForm::None; + lastEditorJid.clear(); + ownerJids.clear(); + administratorJids.clear(); + channelDeletion = {}; + nodes = {}; + messagesSubscribeRole = QXmppMixConfigItem::Role::None; + messagesRetractRole = QXmppMixConfigItem::Role::None; + presenceSubscribeRole = QXmppMixConfigItem::Role::None; + participantsSubscribeRole = QXmppMixConfigItem::Role::None; + informationSubscribeRole = QXmppMixConfigItem::Role::None; + informationUpdateRole = QXmppMixConfigItem::Role::None; + allowedJidsSubscribeRole = QXmppMixConfigItem::Role::None; + bannedJidsSubscribeRole = QXmppMixConfigItem::Role::None; + configurationReadRole = QXmppMixConfigItem::Role::None; + avatarUpdateRole = QXmppMixConfigItem::Role::None; + nicknameRequired = std::nullopt; + presenceRequired = std::nullopt; + onlyParticipantsPermittedToSubmitPresence = std::nullopt; + ownMessageRetractionPermitted = std::nullopt; + invitationsPermitted = std::nullopt; + privateMessagesPermitted = std::nullopt; + } + + QString formType() const override + { + return ns_mix_admin; + } + + void parseForm(const QXmppDataForm &form) override + { + dataFormType = form.type(); + const auto fields = form.fields(); + + for (const auto &field : fields) { + const auto key = field.key(); + const auto value = field.value(); + + if (key == LAST_EDITOR_JID_KEY) { + lastEditorJid = value.toString(); + } else if (key == OWNER_JIDS_KEY) { + ownerJids = value.toStringList(); + } else if (key == ADMINISTRATOR_JIDS_KEY) { + const auto administratorJidList = value.toStringList(); + administratorJids = value.toStringList(); + } else if (key == CHANNEL_DELETION_KEY) { + channelDeletion = value.toDateTime(); + } else if (key == NODES_KEY) { + nodes = listToNodes(value.toStringList()); + } else if (key == MESSAGES_SUBSCRIBE_ROLE_KEY) { + messagesSubscribeRole = stringToRole(value.toString()); + } else if (key == MESSAGES_RETRACT_ROLE_KEY) { + messagesRetractRole = stringToRole(value.toString()); + } else if (key == PRESENCE_SUBSCRIBE_ROLE_KEY) { + presenceSubscribeRole = stringToRole(value.toString()); + } else if (key == PARTICIPANTS_SUBSCRIBE_ROLE_KEY) { + participantsSubscribeRole = stringToRole(value.toString()); + } else if (key == INFORMATION_SUBSCRIBE_ROLE_KEY) { + informationSubscribeRole = stringToRole(value.toString()); + } else if (key == INFORMATION_UPDATE_ROLE_KEY) { + informationUpdateRole = stringToRole(value.toString()); + } else if (key == ALLOWED_JIDS_SUBSCRIBE_ROLE_KEY) { + allowedJidsSubscribeRole = stringToRole(value.toString()); + } else if (key == BANNED_JIDS_SUBSCRIBE_ROLE_KEY) { + bannedJidsSubscribeRole = stringToRole(value.toString()); + } else if (key == CONFIGURATION_READ_ROLE_KEY) { + configurationReadRole = stringToRole(value.toString()); + } else if (key == AVATARS_UPDATE_ROLE_KEY) { + avatarUpdateRole = stringToRole(value.toString()); + } else if (key == NICKNAME_REQUIRED_KEY) { + nicknameRequired = value.toBool(); + } else if (key == PRESENCE_REQUIRED_KEY) { + presenceRequired = value.toBool(); + } else if (key == ONLY_PARTICIPANTS_PERMITTED_TO_SUBMIT_PRESENCE_KEY) { + onlyParticipantsPermittedToSubmitPresence = value.toBool(); + } else if (key == OWN_MESSAGE_RETRACTION_PERMITTED_KEY) { + ownMessageRetractionPermitted = value.toBool(); + } else if (key == INVITATIONS_PERMITTED_KEY) { + invitationsPermitted = value.toBool(); + } else if (key == PRIVATE_MESSAGES_PERMITTED_KEY) { + privateMessagesPermitted = value.toBool(); + } + } + } + void serializeForm(QXmppDataForm &form) const override + { + form.setType(dataFormType); + + using Type = QXmppDataForm::Field::Type; + + serializeNullable(form, Type::JidSingleField, LAST_EDITOR_JID_KEY, lastEditorJid); + serializeEmptyable(form, Type::JidMultiField, OWNER_JIDS_KEY, ownerJids); + serializeEmptyable(form, Type::JidMultiField, ADMINISTRATOR_JIDS_KEY, administratorJids); + serializeDatetime(form, CHANNEL_DELETION_KEY, channelDeletion); + serializeEmptyable(form, Type::ListMultiField, NODES_KEY, nodesToList(nodes)); + serializeRole(form, MESSAGES_SUBSCRIBE_ROLE_KEY, messagesSubscribeRole); + serializeRole(form, MESSAGES_RETRACT_ROLE_KEY, messagesRetractRole); + serializeRole(form, PRESENCE_SUBSCRIBE_ROLE_KEY, presenceSubscribeRole); + serializeRole(form, PARTICIPANTS_SUBSCRIBE_ROLE_KEY, participantsSubscribeRole); + serializeRole(form, INFORMATION_SUBSCRIBE_ROLE_KEY, informationSubscribeRole); + serializeRole(form, INFORMATION_UPDATE_ROLE_KEY, informationUpdateRole); + serializeRole(form, ALLOWED_JIDS_SUBSCRIBE_ROLE_KEY, allowedJidsSubscribeRole); + serializeRole(form, BANNED_JIDS_SUBSCRIBE_ROLE_KEY, bannedJidsSubscribeRole); + serializeRole(form, CONFIGURATION_READ_ROLE_KEY, configurationReadRole); + serializeRole(form, AVATARS_UPDATE_ROLE_KEY, avatarUpdateRole); + serializeOptional(form, Type::ListSingleField, NICKNAME_REQUIRED_KEY, nicknameRequired); + serializeOptional(form, Type::ListSingleField, PRESENCE_REQUIRED_KEY, presenceRequired); + serializeOptional(form, Type::ListSingleField, ONLY_PARTICIPANTS_PERMITTED_TO_SUBMIT_PRESENCE_KEY, onlyParticipantsPermittedToSubmitPresence); + serializeOptional(form, Type::ListSingleField, OWN_MESSAGE_RETRACTION_PERMITTED_KEY, ownMessageRetractionPermitted); + serializeOptional(form, Type::ListSingleField, INVITATIONS_PERMITTED_KEY, invitationsPermitted); + serializeOptional(form, Type::ListSingleField, PRIVATE_MESSAGES_PERMITTED_KEY, privateMessagesPermitted); + } + + /// + /// Serializes a role to a form field. + /// + /// \param form data form + /// \param name name of the form field + /// \param role role to serialize + /// + static void serializeRole(QXmppDataForm &form, const QString &name, QXmppMixConfigItem::Role role) + { + serializeNullable(form, QXmppDataForm::Field::Type::ListSingleField, name, roleToString(role)); + } + + /// + /// Converts a role to a string. + /// + /// \param role role to convert + /// + /// \return the string representation of the role + /// + static QString roleToString(QXmppMixConfigItem::Role role) + { + return ROLES.value(role); + } + + /// + /// Converts a string to a role. + /// + /// \param roleString string to convert + /// + /// \return the role for its string representation + /// + static QXmppMixConfigItem::Role stringToRole(const QString &roleString) + { + return ROLES.key(roleString); + } + + /// + /// Converts a nodes flag to a list of nodes. + /// + /// \param nodes nodes to convert + /// + /// \return the list of nodes + /// + static QStringList nodesToList(QXmppMixConfigItem::Nodes nodes) + { + QStringList nodeList; + + for (auto itr = NODES.cbegin(); itr != NODES.cend(); ++itr) { + if (nodes.testFlag(itr.key())) { + nodeList.append(itr.value()); + } + } + + return nodeList; + } + + /// + /// Converst a list of nodes to a nodes flag + /// + /// \param nodeList list of nodes to convert + /// + /// \return the nodes flag + /// + static QXmppMixConfigItem::Nodes listToNodes(const QStringList &nodeList) + { + QXmppMixConfigItem::Nodes nodes; + + for (auto itr = NODES.cbegin(); itr != NODES.cend(); ++itr) { + if (nodeList.contains(itr.value())) { + nodes |= itr.key(); + } + } + + return nodes; + } +}; + +/// +/// \class QXmppMixConfigItem +/// +/// \brief The QXmppMixConfigItem class represents a PubSub item of a MIX channel containing its +/// configuration as defined by \xep{0369, Mediated Information eXchange (MIX)}. +/// +/// \since QXmpp 1.6 +/// +/// \ingroup Stanzas +/// + +// TODO: Document nodes more generic not only according to the subscriptions + +/// +/// \enum QXmppMixConfigItem::Node +/// +/// PubSub node belonging to a MIX channel. +/// +/// \var QXmppMixConfigItem::Node::AllowedJids +/// +/// JIDs allowed to participate in the channel. +/// +/// If this node does not exist, all JIDs are allowed to participate in the channel. +/// +/// \var QXmppMixConfigItem::Node::AvatarData +/// +/// Channel's avatar data. +/// +/// \var QXmppMixConfigItem::Node::AvatarMetadata +/// +/// Channel's avatar metadata. +/// +/// \var QXmppMixConfigItem::Node::BannedJids +/// +/// JIDs banned from participating in the channel. +/// +/// \var QXmppMixConfigItem::Node::Configuration +/// +/// Channel's onfiguration. +/// +/// \var QXmppMixConfigItem::Node::Information +/// +/// Channel's information. +/// +/// \var QXmppMixConfigItem::Node::JidMap +/// +/// Mappings from the partipants' IDs to their JIDs. +/// +/// This is needed for JID hidden channels. +/// +/// \var QXmppMixConfigItem::Node::Messages +/// +/// Messages sent through the channel. +/// +/// \var QXmppMixConfigItem::Node::Participants +/// +/// Users participating in the channel. +/// +/// \var QXmppMixConfigItem::Node::Presence +/// +/// Presence of users participating in the channel. +/// + +/// +/// \enum QXmppMixConfigItem::Role +/// +/// Roles for a MIX channel with various rights. +/// +/// The rights are defined in a strictly hierarchical manner following the order of this +/// enumeration, so that for example Owners will always have rights that Administrators have. +/// +/// \var QXmppMixConfigItem::Role::Owner +/// +/// Allowed to update the channel configuration. +/// Specified by the channel configuration. +/// +/// \var QXmppMixConfigItem::Role::Administrator +/// +/// Allowed to update the JIDs that are allowed to participate or banned from participating in a +/// channel. +/// Specified in the channel configuration. +/// +/// \var QXmppMixConfigItem::Role::Participant +/// +/// Participant of the channel. +/// +/// \var QXmppMixConfigItem::Role::Allowed +/// +/// User that is allowed to participate in the channel. +/// Users are allowed if their JIDs do not match a JID in the node Node::BannedJids and either there +/// is no node Node::AllowedJids or their JIDs match a JID in it. +/// +/// \var QXmppMixConfigItem::Role::Anyone +/// +/// Any user, including users in the node BannedJids. +/// +/// \var QXmppMixConfigItem::Role::Nobody +/// +/// No user, including owners and administrators. +/// + +QXmppMixConfigItem::QXmppMixConfigItem() + : d(new QXmppMixConfigItemPrivate) +{ +} + +/// Default copy-constructor +QXmppMixConfigItem::QXmppMixConfigItem(const QXmppMixConfigItem &) = default; +/// Default move-constructor +QXmppMixConfigItem::QXmppMixConfigItem(QXmppMixConfigItem &&) = default; +/// Default assignment operator +QXmppMixConfigItem &QXmppMixConfigItem::operator=(const QXmppMixConfigItem &) = default; +/// Default move-assignment operator +QXmppMixConfigItem &QXmppMixConfigItem::operator=(QXmppMixConfigItem &&) = default; +QXmppMixConfigItem::~QXmppMixConfigItem() = default; + +/// +/// Returns the type of the data form that contains the channel's configuration. +/// +/// \return the data form's type +/// +QXmppDataForm::Type QXmppMixConfigItem::formType() const +{ + return d->dataFormType; +} + +/// +/// Sets the type of the data form that contains the channel's configuration. +/// +/// \param formType data form's type +/// +void QXmppMixConfigItem::setFormType(QXmppDataForm::Type formType) +{ + d->dataFormType = formType; +} + +/// +/// Returns the bare JID of the user that made the latest change to the channel's configuration. +/// +/// The JID is set by the server on each configuration change. +/// +/// \return the JID of the last editor +/// +QString QXmppMixConfigItem::lastEditorJid() const +{ + return d->lastEditorJid; +} + +/// +/// Sets the bare JID of the user that made the latest change to the channel's configuration. +/// +/// \see lastEditorJid() +/// +/// \param lastEditorJid last editor JID +/// +void QXmppMixConfigItem::setLastEditorJid(const QString &lastEditorJid) +{ + d->lastEditorJid = lastEditorJid; +} + +/// +/// Returns the bare JIDs of the channel owners. +/// +/// When a channel is created, the JID of the user that created it is set as the first owner. +/// +/// \see Role::Owner +/// +/// \return the JIDs of the owners +/// +QStringList QXmppMixConfigItem::ownerJids() const +{ + return d->ownerJids; +} + +/// +/// Sets the bare JIDs of the channel owners. +/// +/// \see ownerJids() +/// +/// \param ownerJids JIDs of the owners +/// +void QXmppMixConfigItem::setOwnerJids(const QStringList &ownerJids) +{ + d->ownerJids = ownerJids; +} + +/// +/// Returns the bare JIDs of the channel administrators. +/// +/// \see Role::Administrator +/// +/// \return the JIDs of the administrators +/// +QStringList QXmppMixConfigItem::administratorJids() const +{ + return d->administratorJids; +} + +/// +/// Sets the bare JIDs of the channel administrators. +/// +/// \see administratorJids() +/// +/// \param administratorJids JIDs of the administrators +/// +void QXmppMixConfigItem::setAdministratorJids(const QStringList &administratorJids) +{ + d->administratorJids = administratorJids; +} + +/// +/// Returns the date and time when the channel is automatically deleted. +/// +/// If no date/time is set, the channel is permanent. +/// +/// \return the channel deletion date/time +/// +QDateTime QXmppMixConfigItem::channelDeletion() const +{ + return d->channelDeletion; +} + +/// +/// Sets the date and time when the channel is automatically deleted. +/// +/// \see channelDeletion() +/// +/// \param channelDeletion channel deletion date/time +/// +void QXmppMixConfigItem::setChannelDeletion(const QDateTime &channelDeletion) +{ + d->channelDeletion = channelDeletion; +} + +/// +/// Returns which nodes are present for the channel. +/// +/// \return the present nodes +/// +QXmppMixConfigItem::Nodes QXmppMixConfigItem::nodes() const +{ + return d->nodes; +} + +/// +/// Sets which nodes are present for the channel. +/// +/// \param nodes present nodes +/// +void QXmppMixConfigItem::setNodes(Nodes nodes) +{ + d->nodes = nodes; +} + +/// +/// Returns the role that is permitted to subscribe to messages sent through the channel. +/// +/// \return the role permitted to subscribe to the messages +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::messagesSubscribeRole() const +{ + return d->messagesSubscribeRole; +} + +/// +/// Sets the role that is permitted to subscribe to messages sent through the channel. +/// +/// \param messagesSubscribeRole role permitted to subscribe to the messages +/// +void QXmppMixConfigItem::setMessagesSubscribeRole(Role messagesSubscribeRole) +{ + d->messagesSubscribeRole = messagesSubscribeRole; +} + +/// +/// Returns the role that is permitted to retract any message sent through the channel. +/// +/// \return the role permitted to retract any message +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::messagesRetractRole() const +{ + return d->messagesRetractRole; +} + +/// +/// Sets the role that is permitted to retract any message sent through the channel. +/// +/// \param messagesRetractRole role permitted to retract any message +/// +void QXmppMixConfigItem::setMessagesRetractRole(Role messagesRetractRole) +{ + d->messagesRetractRole = messagesRetractRole; +} + +/// +/// Returns the role that is permitted to subscribe to the channel's user' presence. +/// +/// \return the role permitted to subscribe to the presence +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::presenceSubscribeRole() const +{ + return d->presenceSubscribeRole; +} + +/// +/// Sets the role that is permitted to subscribe to the channel's users' presence. +/// +/// \param presenceSubscribeRole role permitted to subscribe to the presence +/// +void QXmppMixConfigItem::setPresenceSubscribeRole(Role presenceSubscribeRole) +{ + d->presenceSubscribeRole = presenceSubscribeRole; +} + +/// +/// Returns the role that is permitted to subscribe to the channel's participants. +/// +/// \return the role permitted to subscribe to the participants +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::participantsSubscribeRole() const +{ + return d->participantsSubscribeRole; +} + +/// +/// Sets the role that is permitted to subscribe to the channel's participants. +/// +/// \param participantsSubscribeRole role permitted to subscribe to the participants +/// +void QXmppMixConfigItem::setParticipantsSubscribeRole(Role participantsSubscribeRole) +{ + d->participantsSubscribeRole = participantsSubscribeRole; +} + +/// +/// Returns the role that is permitted to subscribe to the channel's information. +/// +/// \return the role permitted to subscribe to the information +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::informationSubscribeRole() const +{ + return d->informationSubscribeRole; +} + +/// +/// Sets the role that is permitted to subscribe to the channel's information. +/// +/// \param informationSubscribeRole role permitted to subscribe to the information +/// +void QXmppMixConfigItem::setInformationSubscribeRole(Role informationSubscribeRole) +{ + d->informationSubscribeRole = informationSubscribeRole; +} + +/// +/// Returns the role that is permitted to update the channel's information. +/// +/// \return the role permitted to update the information +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::informationUpdateRole() const +{ + return d->informationUpdateRole; +} + +/// +/// Sets the role that is permitted to update the channel's information. +/// +/// \param informationUpdateRole role permitted to update the information +/// +void QXmppMixConfigItem::setInformationUpdateRole(Role informationUpdateRole) +{ + d->informationUpdateRole = informationUpdateRole; +} + +/// +/// Returns the role that is permitted to subscribe to the JIDs that are allowed to participate in +/// the channel. +/// +/// \return the role permitted to subscribe to the allowed JIDs +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::allowedJidsSubscribeRole() const +{ + return d->allowedJidsSubscribeRole; +} + +/// +/// Sets the role that is permitted to subscribe to the JIDs that are allowed to participate in the +/// channel. +/// +/// \param allowedJidsSubscribeRole role permitted to subscribe to the allowed JIDs +/// +void QXmppMixConfigItem::setAllowedJidsSubscribeRole(Role allowedJidsSubscribeRole) +{ + d->allowedJidsSubscribeRole = allowedJidsSubscribeRole; +} + +/// +/// Returns the role that is permitted to subscribe to the JIDs that are banned from participating +/// in the channel. +/// +/// \return the role permitted to subscribe to the banned JIDs +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::bannedJidsSubscribeRole() const +{ + return d->bannedJidsSubscribeRole; +} + +/// +/// Sets the role that is permitted to subscribe to the JIDs that are banned from participating in +/// the channel. +/// +/// \param bannedJidsSubscribeRole role permitted to subscribe to the banned JIDs +/// +void QXmppMixConfigItem::setBannedJidsSubscribeRole(Role bannedJidsSubscribeRole) +{ + d->bannedJidsSubscribeRole = bannedJidsSubscribeRole; +} + +/// +/// Returns the role that is permitted to subscribe to and read the channel's configuration. +/// +/// \return the role permitted to subscribe to and read the configuration +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::configurationReadRole() const +{ + return d->configurationReadRole; +} + +/// +/// Sets the role that is permitted to subscribe to and read the channel's configuration. +/// +/// \param configurationReadRole role permitted to subscribe to and read the configuration +/// +void QXmppMixConfigItem::setConfigurationReadRole(Role configurationReadRole) +{ + d->configurationReadRole = configurationReadRole; +} + +/// +/// Returns the role that is permitted to update the channel's avatar. +/// +/// \return the role permitted to update the avatar +/// +QXmppMixConfigItem::Role QXmppMixConfigItem::avatarUpdateRole() const +{ + return d->avatarUpdateRole; +} + +/// +/// Sets the role that is permitted to update the channel's avatar. +/// +/// \param avatarUpdateRole role permitted to update the avatar +/// +void QXmppMixConfigItem::setAvatarUpdateRole(Role avatarUpdateRole) +{ + d->avatarUpdateRole = avatarUpdateRole; +} + +/// +/// Returns whether participants need nicknames. +/// +/// \return whether nicknames are required +/// +std::optional QXmppMixConfigItem::nicknameRequired() const +{ + return d->nicknameRequired; +} + +/// +/// Sets whether participants need nicknames. +/// +/// \param nicknameRequired whether nicknames are required +/// +void QXmppMixConfigItem::setNicknameRequired(std::optional nicknameRequired) +{ + d->nicknameRequired = nicknameRequired; +} + +/// +/// Returns whether participants need to share their presence. +/// +/// \return whether presence is required +/// +std::optional QXmppMixConfigItem::presenceRequired() const +{ + return d->presenceRequired; +} + +/// +/// Sets whether participants need to share their presence. +/// +/// \param presenceRequired whether presence is required +/// +void QXmppMixConfigItem::setPresenceRequired(std::optional presenceRequired) +{ + d->presenceRequired = presenceRequired; +} + +/// +/// Returns whether only participants are permitted to share their presence. +/// +/// \return whether only participants are permitted to share their presence +/// +std::optional QXmppMixConfigItem::onlyParticipantsPermittedToSubmitPresence() const +{ + return d->onlyParticipantsPermittedToSubmitPresence; +} + +/// +/// Sets whether only participants are permitted to share their presence. +/// +/// \param onlyParticipantsPermittedToSubmitPresence whether only participants are permitted to +/// share their presence +/// +void QXmppMixConfigItem::setOnlyParticipantsPermittedToSubmitPresence(std::optional onlyParticipantsPermittedToSubmitPresence) +{ + d->onlyParticipantsPermittedToSubmitPresence = onlyParticipantsPermittedToSubmitPresence; +} + +/// +/// Returns whether users are permitted to retract their own messages sent through the channel. +/// +/// \return whether users are permitted to retract their own messages +/// +std::optional QXmppMixConfigItem::ownMessageRetractionPermitted() const +{ + return d->ownMessageRetractionPermitted; +} + +/// +/// Sets whether users are permitted to retract their own messages sent through the channel. +/// +/// \param ownMessageRetractionPermitted whether users are permitted to retract their own messages +/// +void QXmppMixConfigItem::setOwnMessageRetractionPermitted(std::optional ownMessageRetractionPermitted) +{ + d->ownMessageRetractionPermitted = ownMessageRetractionPermitted; +} + +/// +/// Returns whether participants are permitted to invite users to the channel. +/// +/// In order to use that feature, the participant must request the invitation from the channel and +/// send it to the invitee. +/// The invitee can use the invitation to join the channel. +/// +/// \sa QXmppMixInvitation +/// +/// \return whether channel participants are permitted to invite users +/// +std::optional QXmppMixConfigItem::invitationsPermitted() const +{ + return d->invitationsPermitted; +} + +/// +/// Sets whether participants are permitted to invite users to the channel. +/// +/// \see invitationsPermitted() +/// +/// \param invitationsPermitted whether participants are permitted to invite users +/// +void QXmppMixConfigItem::setInvitationsPermitted(std::optional invitationsPermitted) +{ + d->invitationsPermitted = invitationsPermitted; +} + +/// +/// Returns whether participants are permitted to exchange private messages through the channel. +/// +/// \return whether participants are permitted to exchange private messages +/// +std::optional QXmppMixConfigItem::privateMessagesPermitted() const +{ + return d->privateMessagesPermitted; +} + +/// +/// Sets whether participants are permitted to exchange private messages through the channel. +/// +/// \param privateMessagesPermitted whether participants are permitted to exchange private messages +/// +void QXmppMixConfigItem::setPrivateMessagesPermitted(std::optional privateMessagesPermitted) +{ + d->privateMessagesPermitted = privateMessagesPermitted; +} + +/// +/// Returns true if the given DOM element is a MIX channel config item. +/// +bool QXmppMixConfigItem::isItem(const QDomElement &element) +{ + return QXmppPubSubBaseItem::isItem(element, [](const QDomElement &payload) { + // Check FORM_TYPE without parsing a full QXmppDataForm. + if (payload.tagName() != u'x' || payload.namespaceURI() != ns_data) { + return false; + } + for (auto fieldEl = payload.firstChildElement(); + !fieldEl.isNull(); + fieldEl = fieldEl.nextSiblingElement()) { + if (fieldEl.attribute(QStringLiteral("var")) == QStringLiteral(u"FORM_TYPE")) { + return fieldEl.firstChildElement(QStringLiteral("value")).text() == ns_mix_admin; + } + } + return false; + }); +} + +/// \cond +void QXmppMixConfigItem::parsePayload(const QDomElement &payload) +{ + d->reset(); + + QXmppDataForm form; + form.parse(payload); + + d->parseForm(form); +} + +void QXmppMixConfigItem::serializePayload(QXmlStreamWriter *writer) const +{ + d->toDataForm().toXml(writer); +} +/// \endcond + class QXmppMixInfoItemPrivate : public QSharedData, public QXmppDataFormBase { public: + QXmppDataForm::Type dataFormType = QXmppDataForm::None; QString name; QString description; QStringList contactJids; @@ -22,6 +915,7 @@ class QXmppMixInfoItemPrivate : public QSharedData, public QXmppDataFormBase void reset() { + dataFormType = QXmppDataForm::None; name.clear(); description.clear(); contactJids.clear(); @@ -34,7 +928,9 @@ class QXmppMixInfoItemPrivate : public QSharedData, public QXmppDataFormBase void parseForm(const QXmppDataForm &form) override { + dataFormType = form.type(); const auto fields = form.fields(); + for (const auto &field : fields) { const auto key = field.key(); const auto value = field.value(); @@ -50,6 +946,8 @@ class QXmppMixInfoItemPrivate : public QSharedData, public QXmppDataFormBase } void serializeForm(QXmppDataForm &form) const override { + form.setType(dataFormType); + using Type = QXmppDataForm::Field::Type; serializeNullable(form, Type::TextSingleField, NAME, name); serializeNullable(form, Type::TextSingleField, DESCRIPTION, description); @@ -84,6 +982,26 @@ QXmppMixInfoItem &QXmppMixInfoItem::operator=(const QXmppMixInfoItem &) = defaul QXmppMixInfoItem &QXmppMixInfoItem::operator=(QXmppMixInfoItem &&) = default; QXmppMixInfoItem::~QXmppMixInfoItem() = default; +/// +/// Returns the type of the data form that contains the channel information. +/// +/// \return the data form's type +/// +QXmppDataForm::Type QXmppMixInfoItem::formType() const +{ + return d->dataFormType; +} + +/// +/// Sets the type of the data form that contains the channel information. +/// +/// \param formType data form's type +/// +void QXmppMixInfoItem::setFormType(QXmppDataForm::Type formType) +{ + d->dataFormType = formType; +} + /// /// Returns the user-specified name of the MIX channel. This is not the name /// part of the channel's JID. @@ -139,7 +1057,7 @@ void QXmppMixInfoItem::setContactJids(QStringList contactJids) bool QXmppMixInfoItem::isItem(const QDomElement &element) { return QXmppPubSubBaseItem::isItem(element, [](const QDomElement &payload) { - // check FORM_TYPE without parsing a full QXmppDataForm + // Check FORM_TYPE without parsing a full QXmppDataForm. if (payload.tagName() != u'x' || payload.namespaceURI() != ns_data) { return false; } @@ -167,9 +1085,7 @@ void QXmppMixInfoItem::parsePayload(const QDomElement &payload) void QXmppMixInfoItem::serializePayload(QXmlStreamWriter *writer) const { - auto form = d->toDataForm(); - form.setType(QXmppDataForm::Result); - form.toXml(writer); + d->toDataForm().toXml(writer); } /// \endcond diff --git a/src/base/QXmppMixParticipantItem.h b/src/base/QXmppMixParticipantItem.h index ce179fbe4..f209fb83f 100644 --- a/src/base/QXmppMixParticipantItem.h +++ b/src/base/QXmppMixParticipantItem.h @@ -2,8 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later -#ifndef QXMPPMIXPARTICIPANTITEM_H -#define QXMPPMIXPARTICIPANTITEM_H +#pragma once #include "QXmppPubSubBaseItem.h" @@ -38,4 +37,4 @@ class QXMPP_EXPORT QXmppMixParticipantItem : public QXmppPubSubBaseItem QSharedDataPointer d; }; -#endif // QXMPPMIXPARTICIPANTITEM_H +Q_DECLARE_METATYPE(QXmppMixParticipantItem) diff --git a/src/client/QXmppMixManager.cpp b/src/client/QXmppMixManager.cpp new file mode 100644 index 000000000..6a02b3181 --- /dev/null +++ b/src/client/QXmppMixManager.cpp @@ -0,0 +1,1452 @@ +// SPDX-FileCopyrightText: 2023 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppMixManager.h" + +#include "QXmppClient.h" +#include "QXmppConstants_p.h" +#include "QXmppDiscoveryIq.h" +#include "QXmppDiscoveryManager.h" +#include "QXmppMessage.h" +#include "QXmppMixInfoItem.h" +#include "QXmppMixInvitation.h" +#include "QXmppMixIq.h" +#include "QXmppPubSubEvent.h" +#include "QXmppPubSubManager.h" +#include "QXmppRosterManager.h" +#include "QXmppUtils.h" + +#include +#include + +using namespace QXmpp::Private; + +/// +/// \class QXmppMixManager +/// +/// This class manages group chat communication as specified in the following XEPs: +/// * \xep{0369, Mediated Information eXchange (MIX)} +/// * \xep{0405, Mediated Information eXchange (MIX): Participant Server Requirements} +/// * \xep{0406, Mediated Information eXchange (MIX): MIX Administration} +/// * \xep{0407, Mediated Information eXchange (MIX): Miscellaneous Capabilities} +/// +/// In order to use this manager, you need to add it to the client: +/// \code +/// auto *manager = client->addNewExtension(); +/// \endcode +/// +/// If you want to be informed about results of the methods in this class by the corresponding +/// signals, please make sure to subscribe by the relevant subsciption methods before. +/// That way is chosen to keep the structure simple and not notify two times by one signal for the +/// same result. +/// +/// In order to send a message to a MIX channel, you have to set the type QXmppMessage::GroupChat. +/// +/// Example for an unencrypted message: +/// \code +/// message->setType(QXmppMessage::GroupChat); +/// message->setTo("group@mix.example.org") +/// client->send(std::move(message)); +/// \endcode +/// +/// Example for an encrypted message decryptable by Alice and Bob: +/// \code +/// message->setType(QXmppMessage::GroupChat); +/// message->setTo("group@mix.example.org") +/// +/// QXmppSendStanzaParams params; +/// params.setEncryptionJids({ "alice@example.org", "bob@example.com" }) +/// +/// client->sendSensitive(std::move(message), params); +/// \endcode +/// +/// \ingroup Managers +/// +/// \since QXmpp 1.6 +/// + +/// +/// \property QXmppMixManager::supportedByServer +/// +/// \see QXmppMixManager::supportedByServer() +/// + +/// +/// \property QXmppMixManager::archivingSupportedByServer +/// +/// \see QXmppMixManager::archivingSupportedByServer() +/// + +/// +/// \property QXmppMixManager::services +/// +/// \see QXmppMixManager::services() +/// + +/// +/// \struct QXmppMixManager::Service +/// +/// Service providing MIX channels and corresponding nodes. +/// +/// \var QXmppMixManager::Service::jid +/// +/// JID of the service. +/// +/// \var QXmppMixManager::Service::channelsSearchable +/// +/// Whether the service can be searched for channels. +/// +/// \var QXmppMixManager::Service::channelCreationAllowed +/// +/// Whether channels can be created on the service. +/// + +/// \cond +bool QXmppMixManager::Service::operator==(const Service &other) const +{ + return jid == other.jid && + channelsSearchable == other.channelsSearchable && + channelCreationAllowed == other.channelCreationAllowed; +} +/// \endcond + +/// +/// \struct QXmppMixManager::Subscription +/// +/// Subscription to nodes of a MIX channel. +/// +/// \var QXmppMixManager::Subscription::nodesBeingSubscribedTo +/// +/// Nodes belonging to the channel that are subscribed to. +/// +/// If not all desired nodes could be subscribed, this contains only the subscribed nodes. +/// +/// \var QXmppMixManager::Subscription::nodesBeingUnsubscribedFrom +/// +/// Nodes belonging to the channel that are unsubscribed from. +/// + +/// +/// \struct QXmppMixManager::Participation +/// +/// Participation in a channel. +/// +/// \var QXmppMixManager::Participation::participantId +/// +/// ID of the user within the channel. +/// +/// \var QXmppMixManager::Participation::nickname +/// +/// Nickname of the user within the channel. +/// +/// If the server modified the desired nickname, this is the modified one. +/// +/// \var QXmppMixManager::Participation::nodesBeingSubscribedTo +/// +/// Nodes belonging to the joined channel that are subscribed to. +/// +/// If not all desired nodes could be subscribed, this contains only the subscribed nodes. +/// + +/// +/// \typedef QXmppMixManager::Jid +/// +/// JID of a user or domain. +/// + +/// +/// \typedef QXmppMixManager::ChannelJid +/// +/// JID of a MIX channel. +/// + +/// +/// \typedef QXmppMixManager::Nickname +/// +/// Nickname of the user within a MIX channel. +/// +/// If the server modified the desired nickname, this is the modified one. +/// + +/// +/// \typedef QXmppMixManager::CreationResult +/// +/// Contains the JID of the created MIX channel a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::ChannelJidResult +/// +/// Contains the JIDs of all discoverable MIX channels of a MIX service or a QXmppError if it +/// failed. +/// + +/// +/// \typedef QXmppMixManager::ChannelNodeResult +/// +/// Contains all nodes of the requested MIX channel that can be subscribed by the user or a +/// QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::ConfigurationResult +/// +/// Contains the configuration of the MIX channel or a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::InformationResult +/// +/// Contains the information of the MIX channel or a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::JoiningResult +/// +/// Contains the result of the joined MIX channel or a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::NicknameResult +/// +/// Contains the new nickname within a joined MIX channel or a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::SubscriptionResult +/// +/// Contains the result of the subscribed/unsubscribed nodes belonging to a MIX channel or a +/// QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::JidResult +/// +/// Contains the JIDs of users or domains that are allowed to participate resp. banned from +/// participating in a MIX channel or a QXmppError on failure. +/// + +/// +/// \typedef QXmppMixManager::ParticipantResult +/// +/// Contains the participants of a MIX channel or a QXmppError on failure. +/// + +constexpr auto MIX_SERVICE_DISCOVERY_NODE = "mix"; + +/// +/// Constructs a MIX manager. +/// +QXmppMixManager::QXmppMixManager() = default; + +QStringList QXmppMixManager::discoveryFeatures() const +{ + return QStringList() << ns_mix; +} + +/// +/// Returns whether the own server supports MIX clients. +/// +/// In that case, the server interacts between a client and a MIX service. +/// E.g., the server adds a MIX service to the client's roster after joining it and archives the +/// messages sent through the channel while the client is offline. +/// +/// \return whether MIX clients are supported +/// +bool QXmppMixManager::supportedByServer() const +{ + return m_supportedByServer; +} + +/// +/// \fn QXmppMixManager::supportedByServerChanged() +/// +/// Emitted when the server enabled or disabled supporting MIX clients. +/// + +/// +/// Returns whether the own server supports archiving messages via +/// \xep{0313, Message Archive Management} of MIX channels the user participates in. +/// +/// \return whether MIX messages are archived +/// +bool QXmppMixManager::archivingSupportedByServer() const +{ + return m_archivingSupportedByServer; +} + +/// +/// \fn QXmppMixManager::archivingSupportedByServerChanged() +/// +/// Emitted when the server enabled or disabled supporting archiving for MIX. +/// + +/// +/// Returns the services providing MIX on the own server. +/// +/// Such services provide MIX channels and their nodes. +/// It interacts directly with clients or with their servers. +/// +/// \return the provided MIX services +/// +QList QXmppMixManager::services() const +{ + return m_services; +} + +/// +/// \fn QXmppMixManager::servicesChanged() +/// +/// Emitted when the services providing MIX on the own server changed. +/// + +/// +/// Creates a MIX channel. +/// +/// If no channelId is passed, the channel is created with an ID provided by the MIX service. +/// Furthermore, the channel cannot be discovered by anyone. +/// A channel with the mentioned properties is called an "ad-hoc channel". +/// +/// The channel ID is the local part of the channel JID. +/// The MIX service JID is the domain part of the channel JID. +/// Example: "channel" is the channel ID and "mix.example.org" the service JID of the channel JID +/// "channel@mix.example.org". +/// +/// \param serviceJid JID of the service +/// \param channelId ID of the channel (default: provided by the server) +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::createChannel(const QString &serviceJid, const QString &channelId) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(serviceJid); + iq.setActionType(QXmppMixIq::Create); + iq.setChannelId(channelId); + + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> CreationResult { + return iq.channelJid().isEmpty() ? iq.channelId() % "@" % iq.from() : iq.channelJid(); + }); +} + +/// +/// Requests the JIDs of all discoverable MIX channels of a MIX service. +/// +/// \param serviceJid JID of the service that provides the channels +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestChannelJids(const QString &serviceJid) +{ + QXmppPromise promise; + + auto task = m_discoveryManager->requestDiscoItems(serviceJid); + + task.then(this, [promise](QXmppDiscoveryManager::ItemsResult result) mutable { + if (const auto items = std::get_if>(&result)) { + QVector jids; + + std::for_each(items->cbegin(), items->cend(), [&jids](const QXmppDiscoveryIq::Item &item) { + jids.append(item.jid()); + }); + + promise.finish(jids); + } else { + promise.finish(std::move(std::get(result))); + } + }); + + return promise.task(); +} + +/// +/// Requests all nodes of a MIX channel that can be subscribed by the user. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestChannelNodes(const QString &channelJid) +{ + QXmppPromise promise; + + auto task = m_discoveryManager->requestDiscoItems(channelJid, MIX_SERVICE_DISCOVERY_NODE); + task.then(this, [promise](QXmppDiscoveryManager::ItemsResult result) mutable { + if (const auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + const auto items = std::get>(result); + QVector nodes; + + std::for_each(items.cbegin(), items.cend(), [&nodes](const QXmppDiscoveryIq::Item &item) { + nodes.append(item.node()); + }); + + promise.finish(QXmppMixIq::listToNodes(nodes)); + } + }); + + return promise.task(); +} + +/// +/// Requests the configuration of a MIX channel. +/// +/// \param channelJid JID of the channel whose configuration is requested +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestChannelConfiguration(const QString &channelJid) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->requestItems(channelJid, ns_mix_node_config); + task.then(this, [this, promise, channelJid](QXmppPubSubManager::ItemsResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(std::get>(result).items.constFirst())); + } + }); + + return promise.task(); +} + +/// +/// Updates the configuration of a MIX channel. +/// +/// In order to use this method, retrieve the current configuration via +/// requestChannelConfiguration() first, change the desired attributes and pass the configuration to +/// this method. +/// +/// \param channelJid JID of the channel whose configuration is to be updated +/// \param configuration new configuration of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::updateChannelConfiguration(const QString &channelJid, QXmppMixConfigItem configuration) +{ + QXmppPromise promise; + + configuration.setFormType(QXmppDataForm::Submit); + + auto task = m_pubSubManager->publishItem(channelJid, ns_mix_node_config, configuration); + task.then(this, [this, promise](QXmppPubSubManager::PublishItemResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(QXmpp::Success())); + } + }); + + return promise.task(); +} + +/// +/// \fn QXmppMixManager::channelConfigurationUpdated(const QString &channelJid, const QXmppMixConfigItem &configuration) +/// +/// Emitted when the configuration of a MIX channel is updated. +/// +/// \param channelJid JID of the channel whose configuration is updated +/// \param configuration new channel configuration +/// + +/// +/// Requests the information of a MIX channel. +/// +/// \param channelJid JID of the channel whose information is requested +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestChannelInformation(const QString &channelJid) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->requestItems(channelJid, ns_mix_node_info); + task.then(this, [this, promise, channelJid](QXmppPubSubManager::ItemsResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(std::get>(result).items.constFirst())); + } + }); + + return promise.task(); +} + +/// +/// Updates the information of a MIX channel. +/// +/// In order to use this method, retrieve the current information via requestChannelInformation() +/// first, change the desired attributes and pass the information to this method. +/// +/// \param channelJid JID of the channel whose information is to be updated +/// \param information new information of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::updateChannelInformation(const QString &channelJid, QXmppMixInfoItem information) +{ + QXmppPromise promise; + + information.setFormType(QXmppDataForm::Submit); + + auto task = m_pubSubManager->publishItem(channelJid, ns_mix_node_info, information); + task.then(this, [this, promise](QXmppPubSubManager::PublishItemResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(QXmpp::Success())); + } + }); + + return promise.task(); +} + +/// +/// \fn QXmppMixManager::channelInformationUpdated(const QString &channelJid, const QXmppMixInfoItem &information) +/// +/// Emitted when the information of a MIX channel is updated. +/// +/// \param channelJid JID of the channel whose information is updated +/// \param information new channel information +/// + +/// +/// Joins a MIX channel to become a participant of it. +/// +/// \param channelJid JID of the channel being joined +/// \param nickname nickname of the user which is usually required by the server (default: no +/// nickname is set) +/// \param nodes nodes of the channel that are subscribed to for receiving their updates (default: +/// all nodes are subcribed to) +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::joinChannel(const QString &channelJid, const QString &nickname, QXmppMixConfigItem::Nodes nodes) +{ + return joinChannel(std::move(prepareJoinIq(channelJid, nickname, nodes))); +} + +/// +/// Invites a user to a MIX channel that the user is not yet allowed to participate in. +/// +/// This requests an invitation from the channel and sends it to the invitee. +/// The invitee can then use that invitation to join the channel. +/// +/// That invitation mechanism avoids storing allowed JIDs for an indefinite time if the invited user +/// never joins the channel. +/// By using this method, there is no need of allowing JIDs via allowJid() (while being permitted to +/// do so) and sending them invitations via sendInvitation() manually. +/// +/// This method can be used in the following cases: +/// * The inviter is an administrator of the channel. +/// * The inviter is a participant of the channel and the channel allows all participants to +/// invite new users. +/// +/// \param channelJid JID of the channel that the contact is invited to +/// \param inviteeJid JID of the invited user +/// \param messageBody body of the invitation message sent to the invited contact +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::invite(const QString &channelJid, const QString &inviteeJid, const QString &messageBody) +{ + QXmppPromise promise; + + QXmppMixIq iq; + iq.setType(QXmppIq::Get); + iq.setTo(channelJid); + iq.setActionType(QXmppMixIq::InvitationRequest); + iq.setInviteeJid(inviteeJid); + + auto task = client()->sendIq(std::move(iq)); + task.then(this, [this, promise, channelJid, inviteeJid, messageBody](QXmppClient::IqResult &&result) mutable { + if (const auto error = std::get_if(&result)) { + promise.finish(*error); + } else { + QXmppMixIq iq; + iq.parse(std::get(result)); + + auto task = sendInvitation(*(iq.invitation()), messageBody); + task.then(this, [this, promise, channelJid, inviteeJid, messageBody](QXmpp::SendResult result) mutable { + promise.finish(result); + }); + } + }); + + return promise.task(); +} + +/// +/// Sends a MIX channel invitation to a user without requesting an invitation from the channel. +/// +/// If you need to request an invitation from the channel, use invite() instead. +/// +/// This method can be used if the invitee is already allowed to participate in the channel but did +/// not join yet. +/// +/// This method can also be used if the channel does neither allow the invitee yet nor support +/// invitations via invite() but the inviter is permitted to allow JIDs. +/// In that case, the user's JID has to be added via allowJid() before calling this method. +/// +/// This method can be used in the following cases: +/// * Everybody is allowed to participate in the channel. +/// * The invitee is explicitly allowed to participate in the channel. +/// That is particularly relevant if the channel does not support invitations via invite() but +/// the inviter is permitted to allow JIDs via allowJid(). +/// In that case, the invitee's JID has to be allowed before calling this method. +/// +/// The sent invitation is not meant to be read by a human. +/// Instead, the receiving client needs to support it. +/// But you can add an appropriate text to the body of the invitation message to enable human users +/// of clients that do not support that feature to join the channel manually. +/// For example, you could add the JID of the channel or even an XMPP URI to the body. +/// +/// \param channelJid JID of the channel that the contact is invited to +/// \param inviteeJid JID of the invited user +/// \param messageBody body of the message sent to the invited contact +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::sendInvitation(const QString &channelJid, const QString &inviteeJid, const QString &messageBody) +{ + QXmppMixInvitation invitation; + invitation.setInviterJid(client()->configuration().jidBare()); + invitation.setInviteeJid(inviteeJid); + invitation.setChannelJid(channelJid); + + return sendInvitation(invitation, messageBody); +} + +/// +/// \fn QXmppMixManager::invited(const QXmppMixInvitation &invitation) +/// +/// Emitted when the user is invited to a MIX channel. +/// +/// \param invitation invitation used to join the channel +/// + +/// +/// Joins a MIX channel via an invitation to become a participant of it. +/// +/// \param invitation invitation to the channel +/// \param nickname nickname of the user which is usually required by the server (default: no +/// nickname is set) +/// \param nodes nodes of the channel that are subscribed to for receiving their updates (default: +/// all nodes are subcribed to) +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::acceptInvitation(const QXmppMixInvitation &invitation, const QString &nickname, QXmppMixConfigItem::Nodes nodes) +{ + auto iq = prepareJoinIq(invitation.channelJid(), nickname, nodes); + + // Submit the invitation only if it was generated by the channel and thus needed to join. + if (!invitation.token().isEmpty()) { + iq.setInvitation(invitation); + } + + return joinChannel(std::move(iq)); +} + +/// +/// Updates the nickname within a channel. +/// +/// If the update succeeded, the new nickname is returned which may differ from the requested one. +/// +/// \param channelJid JID of the channel +/// \param nickname nickname to be set +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::updateNickname(const QString &channelJid, const QString &nickname) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(channelJid); + iq.setActionType(QXmppMixIq::SetNick); + iq.setNick(nickname); + + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> NicknameResult { + return iq.nick(); + }); +} + +/// +/// Updates the subscriptions to nodes of a MIX channel. +/// +/// \param channelJid JID of the channel +/// \param nodesToSubscribeTo nodes to subscribe to +/// \param nodesToUnsubscribeFrom nodes to unsubscribe from +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::updateSubscriptions(const QString &channelJid, QXmppMixConfigItem::Nodes nodesToSubscribeTo, QXmppMixConfigItem::Nodes nodesToUnsubscribeFrom) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(channelJid); + iq.setActionType(QXmppMixIq::UpdateSubscription); + iq.setNodesBeingSubscribedTo(nodesToSubscribeTo); + iq.setNodesBeingUnsubscribedFrom(nodesToUnsubscribeFrom); + + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> SubscriptionResult { + return QXmppMixManager::Subscription { iq.nodesBeingSubscribedTo(), iq.nodesBeingUnsubscribedFrom() }; + }); +} + +/// +/// Requests all JIDs which are allowed to participate in a MIX channel. +/// +/// The JIDs can specify users (e.g., "alice@example.org") or groups of users (e.g., "example.org") +/// to let all users join which have a JID containing the specified domain. +/// This is only relevant/used for private channels having a user-specified JID. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestAllowedJids(const QString &channelJid) +{ + return requestJids(channelJid, ns_mix_node_allowed); +} + +/// +/// 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, jid); +} + +/// +/// \fn QXmppMixManager::jidAllowed(const QString &channelJid, const QString &jid) +/// +/// Emitted when a JID is allowed to participate in a MIX channel. +/// +/// That happens if allowJid() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// \param jid allowed bare JID +/// + +/// +/// \fn QXmppMixManager::allJidsAllowed(const QString &channelJid) +/// +/// Emitted when all JIDs are allowed to participate in a MIX channel. +/// +/// That happens if QXmppMixConfigItem::Node::AllowedJids is removed from a channel. +/// +/// \param channelJid JID of the channel +/// + +/// +/// Disallows a formerly allowed JID to participate in a MIX channel. +/// +/// Only allowed JIDs can be disallowed via this method. +/// In order to disallow other JIDs, use banJid(). +/// +/// \param channelJid JID of the channel +/// \param jid bare JID to be disallowed +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::disallowJid(const QString &channelJid, const QString &jid) +{ + return m_pubSubManager->retractItem(channelJid, ns_mix_node_allowed, jid); +} + +/// +/// \fn QXmppMixManager::jidDisallowed(const QString &channelJid, const QString &jid) +/// +/// Emitted when a fomerly allowed JID is disallowed to participate in a MIX channel anymore. +/// +/// That happens if disallowJid() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// \param jid disallowed bare JID +/// + +/// +/// Disallows all formerly allowed JIDs to participate in a MIX channel. +/// +/// Only allowed JIDs can be disallowed via this method. +/// In order to disallow other JIDs, use banJid(). +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::disallowAllJids(const QString &channelJid) +{ + return m_pubSubManager->purgeItems(channelJid, ns_mix_node_allowed); +} + +/// +/// \fn QXmppMixManager::allJidsDisallowed(const QString &channelJid) +/// +/// Emitted when no JID is allowed to participate in a MIX channel anymore. +/// +/// That happens if disallowAllJids() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// + +/// +/// Requests all JIDs which are not allowed to participate in a MIX channel. +/// +/// \param channelJid JID of the corresponding channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestBannedJids(const QString &channelJid) +{ + return requestJids(channelJid, ns_mix_node_banned); +} + +/// +/// Bans a JID from participating in a MIX channel. +/// +/// The JID can specify a user (e.g., "alice@example.org") or groups of users (e.g., "example.org") +/// to ban all users which have a JID containing the specified domain. +/// +/// 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, jid); +} + +/// +/// \fn QXmppMixManager::jidBanned(const QString &channelJid, const QString &jid) +/// +/// Emitted when a JID is banned from participating in a MIX channel. +/// +/// That happens if banJid() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// \param jid banned bare JID +/// + +/// +/// Unbans a formerly banned JID from participating in a MIX channel. +/// +/// \param channelJid JID of the channel +/// \param jid bare JID to be unbanned +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::unbanJid(const QString &channelJid, const QString &jid) +{ + return m_pubSubManager->retractItem(channelJid, ns_mix_node_banned, jid); +} + +/// +/// \fn QXmppMixManager::jidUnbanned(const QString &channelJid, const QString &jid) +/// +/// Emitted when a formerly banned JID is unbanned from participating in a MIX channel. +/// +/// That happens if unbanJid() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// \param jid unbanned bare JID +/// + +/// +/// Unbans all formerly banned JIDs from participating in a MIX channel. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::unbanAllJids(const QString &channelJid) +{ + return deleteNode(channelJid, ns_mix_node_banned); +} + +/// +/// \fn QXmppMixManager::allJidsUnbanned(const QString &channelJid) +/// +/// Emitted when all JIDs are unbanned from participating in a MIX channel. +/// +/// That happens if unbanAllJids() was successful or if another resource or user did that. +/// +/// \param channelJid JID of the channel +/// + +/// +/// Requests all participants of a MIX channel. +/// +/// In the case of a channel that not everybody is allowed to participate in, the participants are a +/// subset of the allowed JIDs. +/// +/// \param channelJid JID of the channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestParticipants(const QString &channelJid) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->requestItems(channelJid, ns_mix_node_participants); + task.then(this, [this, promise](QXmppPubSubManager::ItemsResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(std::get>(result).items)); + } + }); + + return promise.task(); +} + +/// +/// \fn QXmppMixManager::userJoinedOrParticipantModified(const QString &channelJid, const QXmppMixParticipantItem &participantItem) +/// +/// Emitted when a user joined a MIX channel or a participant changed the nick. +/// +/// \param channelJid JID of the channel that is joined by the user or for which the participant changed the nick +/// \param participantItem item for the new or modified participant +/// + +/// +/// \fn QXmppMixManager::participantLeft(const QString &channelJid, const QString &participantId) +/// +/// Emitted when a participant left the MIX channel. +/// +/// \param channelJid JID of the channel that is left by the participant +/// \param participantId ID of the left participant +/// + +/// +/// Leaves a MIX channel. +/// +/// \param channelJid JID of the channel to be left +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::leaveChannel(const QString &channelJid) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(client()->configuration().jidBare()); + iq.setActionType(QXmppMixIq::ClientLeave); + iq.setChannelJid(channelJid); + + return client()->sendGenericIq(std::move(iq)); +} + +/// +/// Deletes a MIX channel. +/// +/// \param channelJid JID of the channel to be deleted +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::deleteChannel(const QString &channelJid) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(QXmppUtils::jidToDomain(channelJid)); + iq.setActionType(QXmppMixIq::Destroy); + iq.setChannelId(QXmppUtils::jidToUser(channelJid)); + + return client()->sendGenericIq(std::move(iq)); +} + +/// +/// \fn QXmppMixManager::channelDeleted(const QString &channelJid) +/// +/// Emitted when a MIX channel is deleted. +/// +/// \param channelJid JID of the deleted channel +/// + +/// \cond +void QXmppMixManager::setClient(QXmppClient *client) +{ + QXmppClientExtension::setClient(client); + + // Reset cached information after the client disconnected from the server. + connect(client, &QXmppClient::disconnected, this, [this]() { + setSupportedByServer(false); + setArchivingSupportedByServer(false); + removeServices(); + }); + + if (!(m_discoveryManager = client->findExtension())) { + m_discoveryManager = client->addNewExtension(); + } + + connect(m_discoveryManager, &QXmppDiscoveryManager::infoReceived, this, &QXmppMixManager::handleDiscoInfo); + + if (!(m_pubSubManager = client->findExtension())) { + m_pubSubManager = client->addNewExtension(); + } +} + +bool QXmppMixManager::handleMessage(const QXmppMessage &message) +{ + if (const auto invitation = message.mixInvitation()) { + Q_EMIT invited(*invitation); + return true; + } + + return false; +} + +bool QXmppMixManager::handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName) +{ + if (nodeName == ns_mix_node_allowed && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + // Items have been published. + case QXmppPubSubEventBase::Items: { + const auto items = event.items(); + for (const auto &item : items) { + Q_EMIT jidAllowed(pubSubService, item.id()); + } + break; + } + // Specific items are deleted. + case QXmppPubSubEventBase::Retract: { + const auto ids = event.retractIds(); + for (const auto &id : ids) { + Q_EMIT jidDisallowed(pubSubService, id); + } + break; + } + // All items are deleted. + case QXmppPubSubEventBase::Purge: + Q_EMIT allJidsDisallowed(pubSubService); + break; + // The whole node is deleted. + case QXmppPubSubEventBase::Delete: + Q_EMIT allJidsAllowed(pubSubService); + break; + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } else if (nodeName == ns_mix_node_banned && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + // Items have been published. + case QXmppPubSubEventBase::Items: { + const auto items = event.items(); + for (const auto &item : items) { + Q_EMIT jidBanned(pubSubService, item.id()); + } + break; + } + // Specific items are deleted. + case QXmppPubSubEventBase::Retract: { + const auto ids = event.retractIds(); + for (const auto &id : ids) { + Q_EMIT jidUnbanned(pubSubService, id); + } + break; + } + // All items are deleted. + case QXmppPubSubEventBase::Purge: + // The whole node is deleted. + case QXmppPubSubEventBase::Delete: + Q_EMIT allJidsUnbanned(pubSubService); + break; + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } else if (nodeName == ns_mix_node_config && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + case QXmppPubSubEventBase::Items: { + const auto item = event.items().constFirst(); + Q_EMIT channelConfigurationUpdated(pubSubService, item); + break; + } + case QXmppPubSubEventBase::Retract: + case QXmppPubSubEventBase::Purge: + case QXmppPubSubEventBase::Delete: + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } else if (nodeName == ns_mix_node_info && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + case QXmppPubSubEventBase::Items: { + const auto item = event.items().constFirst(); + Q_EMIT channelInformationUpdated(pubSubService, item); + break; + } + case QXmppPubSubEventBase::Retract: + case QXmppPubSubEventBase::Purge: + case QXmppPubSubEventBase::Delete: + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } else if (nodeName == ns_mix_node_participants && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + // Items have been published. + case QXmppPubSubEventBase::Items: { + const auto items = event.items(); + for (const auto &item : items) { + Q_EMIT userJoinedOrParticipantModified(pubSubService, item); + } + break; + } + // Specific items are deleted. + case QXmppPubSubEventBase::Retract: { + const auto ids = event.retractIds(); + for (const auto &id : ids) { + Q_EMIT participantLeft(pubSubService, id); + } + break; + } + // All items are deleted. + case QXmppPubSubEventBase::Purge: + // The whole node is deleted. + case QXmppPubSubEventBase::Delete: + Q_EMIT channelDeleted(pubSubService); + break; + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } + + return false; +} + +/// +/// Pepares an IQ stanza for joining a MIX channel. +/// +/// \param channelJid JID of the channel being joined +/// \param nickname nickname of the user which is usually required by the server (default: no +/// nickname is set) +/// \param nodes nodes of the channel that are subscribed to for receiving their updates (default: +/// all nodes are subcribed to) +/// +/// \return the prepared MIX join IQ stanza +/// +QXmppMixIq QXmppMixManager::prepareJoinIq(const QString &channelJid, const QString &nickname, QXmppMixConfigItem::Nodes nodes) +{ + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(client()->configuration().jidBare()); + iq.setActionType(QXmppMixIq::ClientJoin); + iq.setChannelJid(channelJid); + iq.setNick(nickname); + iq.setNodesBeingSubscribedTo(nodes); + + return iq; +} +/// \endcond + +/// +/// Joins a MIX channel. +/// +/// \param iq IQ stanza for joining a channel +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::joinChannel(QXmppMixIq &&iq) +{ + return chainIq(client()->sendIq(std::move(iq)), this, [](QXmppMixIq &&iq) -> JoiningResult { + return Participation { iq.participantId(), iq.nick(), iq.nodesBeingSubscribedTo() }; + }); +} + +/// +/// Sends a MIX channel invitation to a user. +/// +/// \param invitation invitation to the channel +/// \param messageBody body of the message sent to the invited contact +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::sendInvitation(const QXmppMixInvitation &invitation, const QString &messageBody) +{ + QXmppMessage message; + message.setTo(invitation.inviteeJid()); + message.setMixInvitation(invitation); + + // A message having no body would neither be delivered to all clients via Message Carbons nor + // delivered to clients which are currently offline. + // To enforce that behavior, set a corresponding message type and message processing hint. + if (messageBody.isEmpty()) { + message.setType(QXmppMessage::Chat); + message.addHint(QXmppMessage::Store); + } else { + message.setBody(messageBody); + } + + return client()->sendSensitive(std::move(message)); +} + +QXmppTask QXmppMixManager::deleteNode(const QString &channelJid, const QString &node) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->deleteNode(channelJid, node); + task.then(this, [this, promise, channelJid](QXmppClient::EmptyResult result) mutable { + if (auto error = std::get_if(&result)) { + // If the node could not be deleted because it did not exist, consider the deletion as + // succeeded. + // If there is another error, return it directly. + if (const auto stanzaError = error->value(); + stanzaError && + stanzaError->type() == QXmppStanza::Error::Cancel && + stanzaError->condition() == QXmppStanza::Error::ItemNotFound) { + promise.finish(std::move(QXmpp::Success())); + } else { + promise.finish(std::move(*error)); + } + } else { + promise.finish(std::move(QXmpp::Success())); + } + }); + + return promise.task(); +} + +/// +/// Requests all JIDs of a node belonging to a MIX. +/// +/// This is only used for nodes storing items with IDs representing JIDs. +/// +/// \param channelJid JID of the channel +/// \param node node to be queried +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::requestJids(const QString &channelJid, const QString &node) +{ + QXmppPromise promise; + + auto task = m_pubSubManager->requestItems(channelJid, node); + task.then(this, [this, promise](QXmppPubSubManager::ItemsResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + const auto items = std::get>(result).items; + QVector jids; + + std::for_each(items.cbegin(), items.cend(), [&jids](const QXmppPubSubBaseItem &item) mutable { + jids.append(item.id()); + }); + + promise.finish(std::move(jids)); + } + }); + + return promise.task(); +} + +/// +/// Adds a JID to a node of a MIX channel. +/// +/// This is only used for nodes storing items with IDs representing JIDs. +/// +/// \param channelJid JID of the channel +/// \param node node to which the JID is added +/// \param jid JID to be added +/// +/// \return the result of the action +/// +QXmppTask QXmppMixManager::addJidToNode(const QString &channelJid, const QString &node, const QString &jid) +{ + QXmppPromise promise; + + const QXmppPubSubBaseItem item { jid }; + + auto task = m_pubSubManager->publishItem(channelJid, node, item); + task.then(this, [this, promise, channelJid, node, item](QXmppPubSubManager::PublishItemResult result) mutable { + if (auto error = std::get_if(&result)) { + promise.finish(std::move(*error)); + } else { + promise.finish(std::move(QXmpp::Success())); + } + }); + + return promise.task(); +} + +/// +/// Handles incoming service infos specified by \xep{0030, Service Discovery} +/// +/// \param iq received Service Discovery IQ stanza +/// +void QXmppMixManager::handleDiscoInfo(const QXmppDiscoveryIq &iq) +{ + // Check the server's functionality to support MIX clients. + if (iq.from().isEmpty() || iq.from() == client()->configuration().domain()) { + // Check whether MIX is supported. + if (iq.features().contains(ns_mix_pam)) { + setSupportedByServer(true); + + // Check whether MIX archiving is supported. + if (iq.features().contains(ns_mix_pam_archiving)) { + setArchivingSupportedByServer(true); + } + } else { + setSupportedByServer(false); + setArchivingSupportedByServer(false); + } + } + + const auto jid = iq.from().isEmpty() ? client()->configuration().domain() : iq.from(); + + // Search for a MIX service and check what it supports. + // if none can be found, remove them from the cache. + if (!iq.features().contains(ns_mix)) { + removeService(jid); + return; + } + + const auto identities = iq.identities(); + + for (const QXmppDiscoveryIq::Identity &identity : identities) { + // ' || identity.type() == "text"' is a workaround for older ejabberd versions. + if (identity.category() == "conference" && (identity.type() == MIX_SERVICE_DISCOVERY_NODE || identity.type() == "text")) { + Service service; + service.jid = iq.from().isEmpty() ? client()->configuration().domain() : iq.from(); + service.channelsSearchable = iq.features().contains(ns_mix_searchable); + service.channelCreationAllowed = iq.features().contains(ns_mix_create_channel); + + addService(service); + return; + } + } + + removeService(jid); +} + +/// +/// Sets whether the own server supports MIX. +/// +/// \param supportedByServer whether MIX is supported by the own server +/// +void QXmppMixManager::setSupportedByServer(bool supportedByServer) +{ + if (m_supportedByServer != supportedByServer) { + m_supportedByServer = supportedByServer; + Q_EMIT supportedByServerChanged(); + } +} + +/// +/// Sets whether the own server supports archiving messages via +/// \xep{0313, Message Archive Management} of MIX channels the user participates in. +/// +/// \param archivingSupportedByServer whether MIX messages are archived by the own server +/// +void QXmppMixManager::setArchivingSupportedByServer(bool archivingSupportedByServer) +{ + if (m_archivingSupportedByServer != archivingSupportedByServer) { + m_archivingSupportedByServer = archivingSupportedByServer; + Q_EMIT archivingSupportedByServerChanged(); + } +} + +/// +/// Adds a MIX service. +/// +/// \param service MIX service +/// +void QXmppMixManager::addService(const Service &service) +{ + auto itr = std::find_if(m_services.begin(), m_services.end(), [&jid = service.jid](const Service &service) { + return service.jid == jid; + }); + + if (itr == m_services.end()) { + m_services.append(service); + } else if (*itr == service) { + return; + } else { + *itr = service; + } + + Q_EMIT servicesChanged(); +} + +/// +/// Removes a MIX service. +/// +/// \param jid JID of the MIX service +/// +void QXmppMixManager::removeService(const QString &jid) +{ + auto itr = std::find_if(m_services.begin(), m_services.end(), [&jid](const Service &service) { + return service.jid == jid; + }); + + if (itr == m_services.end()) { + return; + } else { + m_services.erase(itr); + } + + Q_EMIT servicesChanged(); +} + +/// +/// Removes all MIX services. +/// +void QXmppMixManager::removeServices() +{ + if (!m_services.isEmpty()) { + m_services.clear(); + Q_EMIT servicesChanged(); + } +} diff --git a/src/client/QXmppMixManager.h b/src/client/QXmppMixManager.h new file mode 100644 index 000000000..70bcc9af4 --- /dev/null +++ b/src/client/QXmppMixManager.h @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2023 Linus Jahn +// SPDX-FileCopyrightText: 2023 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "QXmppClient.h" +#include "QXmppClientExtension.h" +#include "QXmppDiscoveryManager.h" +#include "QXmppMessageHandler.h" +#include "QXmppMixInfoItem.h" +#include "QXmppMixIq.h" +#include "QXmppMixParticipantItem.h" +#include "QXmppPubSubEventHandler.h" + +class QXMPP_EXPORT QXmppMixManager : public QXmppClientExtension, public QXmppMessageHandler, public QXmppPubSubEventHandler +{ + Q_OBJECT + Q_PROPERTY(bool supportedByServer READ supportedByServer NOTIFY supportedByServerChanged) + Q_PROPERTY(bool archivingSupportedByServer READ archivingSupportedByServer NOTIFY archivingSupportedByServerChanged) + Q_PROPERTY(QList services READ services NOTIFY servicesChanged) + +public: + struct Service + { + QString jid; + bool channelsSearchable = false; + bool channelCreationAllowed = false; + + /// \cond + bool operator==(const Service &other) const; + /// \endcond + }; + + struct Subscription + { + QXmppMixConfigItem::Nodes nodesBeingSubscribedTo; + QXmppMixConfigItem::Nodes nodesBeingUnsubscribedFrom; + }; + + struct Participation + { + QString participantId; + QString nickname; + QXmppMixConfigItem::Nodes nodesBeingSubscribedTo; + }; + + using Jid = QString; + using ChannelJid = QString; + using Nickname = QString; + + using CreationResult = std::variant; + using ChannelJidResult = std::variant, QXmppError>; + using ChannelNodeResult = std::variant; + using ConfigurationResult = std::variant; + using InformationResult = std::variant; + using JoiningResult = std::variant; + using NicknameResult = std::variant; + using SubscriptionResult = std::variant; + using JidResult = std::variant, QXmppError>; + using ParticipantResult = std::variant, QXmppError>; + + QXmppMixManager(); + + QStringList discoveryFeatures() const override; + + bool supportedByServer() const; + Q_SIGNAL void supportedByServerChanged(); + + bool archivingSupportedByServer() const; + Q_SIGNAL void archivingSupportedByServerChanged(); + + QList services() const; + Q_SIGNAL void servicesChanged(); + + QXmppTask createChannel(const QString &serviceJid, const QString &channelId = {}); + + QXmppTask requestChannelJids(const QString &serviceJid); + QXmppTask requestChannelNodes(const QString &channelJid); + + QXmppTask requestChannelConfiguration(const QString &channelJid); + QXmppTask updateChannelConfiguration(const QString &channelJid, QXmppMixConfigItem configuration); + Q_SIGNAL void channelConfigurationUpdated(const QString &channelJid, const QXmppMixConfigItem &configuration); + + QXmppTask requestChannelInformation(const QString &channelJid); + QXmppTask updateChannelInformation(const QString &channelJid, QXmppMixInfoItem information); + Q_SIGNAL void channelInformationUpdated(const QString &channelJid, const QXmppMixInfoItem &information); + + QXmppTask joinChannel(const QString &channelJid, const QString &nickname = {}, QXmppMixConfigItem::Nodes nodes = ~QXmppMixConfigItem::Nodes()); + + QXmppTask invite(const QString &channelJid, const QString &inviteeJid, const QString &messageBody = {}); + QXmppTask sendInvitation(const QString &channelJid, const QString &inviteeJid, const QString &messageBody = {}); + Q_SIGNAL void invited(const QXmppMixInvitation &invitation); + QXmppTask acceptInvitation(const QXmppMixInvitation &invitation, const QString &nickname = {}, QXmppMixConfigItem::Nodes nodes = ~QXmppMixConfigItem::Nodes()); + + QXmppTask updateNickname(const QString &channelJid, const QString &nickname); + QXmppTask updateSubscriptions(const QString &channelJid, QXmppMixConfigItem::Nodes nodesToSubscribeTo = ~QXmppMixConfigItem::Nodes(), QXmppMixConfigItem::Nodes nodesToUnsubscribeFrom = ~QXmppMixConfigItem::Nodes()); + + QXmppTask requestAllowedJids(const QString &channelJid); + QXmppTask allowJid(const QString &channelJid, const QString &jid); + Q_SIGNAL void jidAllowed(const QString &channelJid, const QString &jid); + Q_SIGNAL void allJidsAllowed(const QString &channelJid); + + QXmppTask disallowJid(const QString &channelJid, const QString &jid); + Q_SIGNAL void jidDisallowed(const QString &channelJid, const QString &jid); + QXmppTask disallowAllJids(const QString &channelJid); + Q_SIGNAL void allJidsDisallowed(const QString &channelJid); + + QXmppTask requestBannedJids(const QString &channelJid); + QXmppTask banJid(const QString &channelJid, const QString &jid); + Q_SIGNAL void jidBanned(const QString &channelJid, const QString &jid); + + QXmppTask unbanJid(const QString &channelJid, const QString &jid); + Q_SIGNAL void jidUnbanned(const QString &channelJid, const QString &jid); + QXmppTask unbanAllJids(const QString &channelJid); + Q_SIGNAL void allJidsUnbanned(const QString &channelJid); + + QXmppTask requestParticipants(const QString &channelJid); + Q_SIGNAL void userJoinedOrParticipantModified(const QString &channelJid, const QXmppMixParticipantItem &participantItem); + Q_SIGNAL void participantLeft(const QString &channelJid, const QString &participantId); + + QXmppTask leaveChannel(const QString &channelJid); + + QXmppTask deleteChannel(const QString &channelJid); + Q_SIGNAL void channelDeleted(const QString &channelJid); + +protected: + /// \cond + void setClient(QXmppClient *client) override; + bool handleMessage(const QXmppMessage &message) override; + bool handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName) override; + /// \endcond + +private: + friend class tst_QXmppMixManager; + + QXmppMixIq prepareJoinIq(const QString &channelJid, const QString &nickname, QXmppMixConfigItem::Nodes nodes); + QXmppTask joinChannel(QXmppMixIq &&iq); + QXmppTask sendInvitation(const QXmppMixInvitation &invitation, const QString &messageBody); + QXmppTask deleteNode(const QString &channelJid, const QString &node); + QXmppTask requestJids(const QString &channelJid, const QString &node); + QXmppTask addJidToNode(const QString &channelJid, const QString &node, const QString &jid); + + void handleDiscoInfo(const QXmppDiscoveryIq &iq); + + void setSupportedByServer(bool supportedByServer); + void setArchivingSupportedByServer(bool archivingSupportedByServer); + void addService(const Service &service); + void removeService(const QString &jid); + void removeServices(); + + QXmppPubSubManager *m_pubSubManager; + QXmppDiscoveryManager *m_discoveryManager; + bool m_supportedByServer = false; + bool m_archivingSupportedByServer = false; + QList m_services; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0213aeb0d..bce6ff117 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,6 +48,7 @@ add_simple_test(qxmppjinglemessageinitiationmanager) add_simple_test(qxmppmammanager) add_simple_test(qxmppmixinvitation) add_simple_test(qxmppmixitems) +add_simple_test(qxmppmixmanager TestClient.h) add_simple_test(qxmppmessage) add_simple_test(qxmppmessagereaction) add_simple_test(qxmppmessagereceiptmanager) diff --git a/tests/TestClient.h b/tests/TestClient.h index 9da12a972..6b939a4bd 100644 --- a/tests/TestClient.h +++ b/tests/TestClient.h @@ -46,6 +46,8 @@ class TestClient : public QXmppClient void expect(QString &&packet) { QVERIFY2(!m_sentPackets.empty(), "No packet was sent!"); + qDebug() << m_sentPackets.first(); + qDebug() << packet.replace(u'\'', u'"'); QCOMPARE(m_sentPackets.takeFirst(), packet.replace(u'\'', u'"')); resetIdCount(); } diff --git a/tests/qxmppmixiq/tst_qxmppmixiq.cpp b/tests/qxmppmixiq/tst_qxmppmixiq.cpp index 3ddf7ef77..0a29d4ea0 100644 --- a/tests/qxmppmixiq/tst_qxmppmixiq.cpp +++ b/tests/qxmppmixiq/tst_qxmppmixiq.cpp @@ -9,6 +9,7 @@ #include Q_DECLARE_METATYPE(QXmppIq::Type) +Q_DECLARE_METATYPE(QXmppMixConfigItem::Nodes) Q_DECLARE_METATYPE(QXmppMixIq::Type) class tst_QXmppMixIq : public QObject @@ -54,12 +55,10 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example\" " "from=\"hag66@shakespeare.example/UUID-a1j/7533\" " "type=\"set\">" - "" + "" "" - "" - "" - "" "" + "" "third witch" "" "hag66@shakespeare.example" @@ -76,10 +75,8 @@ void tst_QXmppMixIq::testBase_data() "from=\"hag66@shakespeare.example\" " "type=\"set\">" "" - "" - "" - "" "" + "" "stpeter" "" "hag66@shakespeare.example" @@ -94,11 +91,9 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example\" " "from=\"coven@mix.shakespeare.example\" " "type=\"result\">" - "" - "" - "" - "" + "" "" + "" "third witch" "" ""); @@ -107,13 +102,11 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example/UUID-a1j/7533\" " "from=\"hag66@shakespeare.example\" " "type=\"result\">" - "" + "" "" - "" - "" - "" + "id=\"123456\">" "" + "" "" "" ""); @@ -122,7 +115,7 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example\" " "from=\"hag66@shakespeare.example/UUID-a1j/7533\" " "type=\"set\">" - "" + "" "" "" ""); @@ -145,7 +138,7 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example/UUID-a1j/7533\" " "from=\"hag66@shakespeare.example\" " "type=\"result\">" - "" + "" "" "" ""); @@ -156,6 +149,7 @@ void tst_QXmppMixIq::testBase_data() "type=\"set\">" "" "" + "" "" ""); QByteArray updateSubscriptionResultXml( @@ -163,8 +157,9 @@ void tst_QXmppMixIq::testBase_data() "to=\"hag66@shakespeare.example/UUID-a1j/7533\" " "from=\"hag66@shakespeare.example\" " "type=\"result\">" - "" + "" "" + "" "" ""); QByteArray setNickSetXml( @@ -192,7 +187,7 @@ void tst_QXmppMixIq::testBase_data() "type=\"set\">" "" ""); - QByteArray createWithoutNameXml( + QByteArray createWithoutIdXml( ""); - QStringList emptyNodes; - QStringList defaultNodes; - defaultNodes << "urn:xmpp:mix:nodes:messages" - << "urn:xmpp:mix:nodes:presence" - << "urn:xmpp:mix:nodes:participants" - << "urn:xmpp:mix:nodes:info"; + QStringList emptyNodeList; + QStringList nodeList = { "urn:xmpp:mix:nodes:info", "urn:xmpp:mix:nodes:messages" }; + QXmppMixConfigItem::Nodes nodesBeingSubscribedTo = { QXmppMixConfigItem::Node::Information | QXmppMixConfigItem::Node::Messages }; + QXmppMixConfigItem::Nodes noNodes; + QXmppMixConfigItem::Nodes nodesBeingUnsubscribedFrom = { QXmppMixConfigItem::Node::Information | QXmppMixConfigItem::Node::Configuration }; QTest::addColumn("xml"); QTest::addColumn("type"); QTest::addColumn("actionType"); QTest::addColumn("jid"); QTest::addColumn("channelName"); + QTest::addColumn("participantId"); + QTest::addColumn("channelId"); + QTest::addColumn("channelJid"); QTest::addColumn("nodes"); + QTest::addColumn("nodesBeingSubscribedTo"); + QTest::addColumn("nodesBeingUnsubscribedFrom"); QTest::addColumn("nick"); QTest::addColumn("inviteeJid"); QTest::addColumn("invitationToken"); @@ -235,7 +234,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::InvitationRequest << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << QStringLiteral("cat@shakespeare.example") << ""; @@ -245,7 +249,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::InvitationResponse << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << QStringLiteral("ABCDEF"); @@ -255,7 +264,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::ClientJoin << "coven@mix.shakespeare.example" << "" - << defaultNodes + << "" + << "" + << "coven@mix.shakespeare.example" + << nodeList + << nodesBeingSubscribedTo + << noNodes << "third witch" << "" << QStringLiteral("ABCDEF"); @@ -265,7 +279,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Join << "" << "" - << defaultNodes + << "" + << "" + << "" + << nodeList + << nodesBeingSubscribedTo + << noNodes << "stpeter" << "" << QStringLiteral("ABCDEF"); @@ -273,9 +292,14 @@ void tst_QXmppMixIq::testBase_data() << joinS2sResultXml << QXmppIq::Result << QXmppMixIq::Join - << "123456#coven@mix.shakespeare.example" << "" - << defaultNodes + << "" + << "123456" + << "" + << "" + << nodeList + << nodesBeingSubscribedTo + << noNodes << "third witch" << "" << ""; @@ -283,9 +307,14 @@ void tst_QXmppMixIq::testBase_data() << joinC2sResultXml << QXmppIq::Result << QXmppMixIq::ClientJoin - << "123456#coven@mix.shakespeare.example" << "" - << defaultNodes + << "" + << "123456" + << "" + << "" + << nodeList + << nodesBeingSubscribedTo + << noNodes << "" << "" << ""; @@ -295,7 +324,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::ClientLeave << "coven@mix.shakespeare.example" << "" - << emptyNodes + << "" + << "" + << "coven@mix.shakespeare.example" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -305,7 +339,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Leave << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -315,7 +354,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Leave << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -325,7 +369,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::ClientLeave << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -335,7 +384,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::UpdateSubscription << "" << "" - << (QStringList() << "urn:xmpp:mix:nodes:messages") + << "" + << "" + << "" + << QStringList { "urn:xmpp:mix:nodes:messages" } + << QXmppMixConfigItem::Nodes { QXmppMixConfigItem::Node::Messages } + << QXmppMixConfigItem::Nodes { QXmppMixConfigItem::Node::Configuration } << "" << "" << ""; @@ -343,9 +397,14 @@ void tst_QXmppMixIq::testBase_data() << updateSubscriptionResultXml << QXmppIq::Result << QXmppMixIq::UpdateSubscription - << "hag66@shakespeare.example" << "" - << (QStringList() << "urn:xmpp:mix:nodes:messages") + << "" + << "" + << "" + << "" + << QStringList { "urn:xmpp:mix:nodes:messages" } + << QXmppMixConfigItem::Nodes { QXmppMixConfigItem::Node::Messages } + << QXmppMixConfigItem::Nodes { QXmppMixConfigItem::Node::Configuration } << "" << "" << ""; @@ -355,7 +414,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::SetNick << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "thirdwitch" << "" << ""; @@ -365,7 +429,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::SetNick << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "thirdwitch" << "" << ""; @@ -375,17 +444,27 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Create << "" << "coven" - << emptyNodes + << "" + << "coven" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; - QTest::newRow("create-without-name") - << createWithoutNameXml + QTest::newRow("create-without-id") + << createWithoutIdXml << QXmppIq::Set << QXmppMixIq::Create << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -395,7 +474,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::Destroy << "" << "coven" - << emptyNodes + << "" + << "coven" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -405,7 +489,12 @@ void tst_QXmppMixIq::testBase_data() << QXmppMixIq::None << "" << "" - << emptyNodes + << "" + << "" + << "" + << emptyNodeList + << noNodes + << noNodes << "" << "" << ""; @@ -418,7 +507,12 @@ void tst_QXmppMixIq::testBase() QFETCH(QXmppMixIq::Type, actionType); QFETCH(QString, jid); QFETCH(QString, channelName); + QFETCH(QString, participantId); + QFETCH(QString, channelId); + QFETCH(QString, channelJid); QFETCH(QStringList, nodes); + QFETCH(QXmppMixConfigItem::Nodes, nodesBeingSubscribedTo); + QFETCH(QXmppMixConfigItem::Nodes, nodesBeingUnsubscribedFrom); QFETCH(QString, nick); QFETCH(QString, inviteeJid); QFETCH(QString, invitationToken); @@ -429,7 +523,12 @@ void tst_QXmppMixIq::testBase() QCOMPARE(iq.actionType(), actionType); QCOMPARE(iq.jid(), jid); QCOMPARE(iq.channelName(), channelName); + QCOMPARE(iq.participantId(), participantId); + QCOMPARE(iq.channelId(), channelId); + QCOMPARE(iq.channelJid(), channelJid); QCOMPARE(iq.nodes(), nodes); + QCOMPARE(iq.nodesBeingSubscribedTo(), nodesBeingSubscribedTo); + QCOMPARE(iq.nodesBeingUnsubscribedFrom(), nodesBeingUnsubscribedFrom); QCOMPARE(iq.nick(), nick); QCOMPARE(iq.inviteeJid(), inviteeJid); QCOMPARE(iq.invitation().has_value(), !invitationToken.isEmpty()); @@ -445,7 +544,12 @@ void tst_QXmppMixIq::testDefaults() QCOMPARE(iq.actionType(), QXmppMixIq::None); QCOMPARE(iq.jid(), QString()); QCOMPARE(iq.channelName(), QString()); + QCOMPARE(iq.participantId(), QString()); + QCOMPARE(iq.channelId(), QString()); + QCOMPARE(iq.channelJid(), QString()); QCOMPARE(iq.nodes(), QStringList()); + QCOMPARE(iq.nodesBeingSubscribedTo(), QXmppMixConfigItem::Nodes {}); + QCOMPARE(iq.nodesBeingUnsubscribedFrom(), QXmppMixConfigItem::Nodes {}); QCOMPARE(iq.nick(), QString()); QVERIFY(iq.inviteeJid().isEmpty()); QVERIFY(!iq.invitation()); @@ -454,14 +558,34 @@ void tst_QXmppMixIq::testDefaults() void tst_QXmppMixIq::testSetters() { QXmppMixIq iq; + iq.setActionType(QXmppMixIq::Join); QCOMPARE(iq.actionType(), QXmppMixIq::Join); + iq.setJid("interestingnews@mix.example.com"); QCOMPARE(iq.jid(), QString("interestingnews@mix.example.com")); + iq.setChannelName("interestingnews"); QCOMPARE(iq.channelName(), QString("interestingnews")); - iq.setNodes(QStringList() << "com:example:mix:node:custom"); - QCOMPARE(iq.nodes(), QStringList() << "com:example:mix:node:custom"); + + iq.setParticipantId("123456"); + QCOMPARE(iq.participantId(), "123456"); + + iq.setChannelId("coven"); + QCOMPARE(iq.channelId(), "coven"); + + iq.setChannelJid("coven@mix.shakespeare.example"); + QCOMPARE(iq.channelJid(), "coven@mix.shakespeare.example"); + + iq.setNodes(QStringList() << "urn:xmpp:mix:nodes:info"); + QCOMPARE(iq.nodes(), QStringList() << "urn:xmpp:mix:nodes:info"); + + iq.setNodesBeingSubscribedTo(QXmppMixConfigItem::Node::AllowedJids | QXmppMixConfigItem::Node::BannedJids); + QCOMPARE(iq.nodesBeingSubscribedTo(), QXmppMixConfigItem::Node::AllowedJids | QXmppMixConfigItem::Node::BannedJids); + + iq.setNodesBeingUnsubscribedFrom(QXmppMixConfigItem::Node::Information | QXmppMixConfigItem::Node::Configuration); + QCOMPARE(iq.nodesBeingUnsubscribedFrom(), QXmppMixConfigItem::Node::Information | QXmppMixConfigItem::Node::Configuration); + iq.setNick("SMUDO"); QCOMPARE(iq.nick(), QString("SMUDO")); @@ -501,7 +625,7 @@ void tst_QXmppMixIq::testIsMixIq() "to=\"hag66@shakespeare.example\" " "from=\"hag66@shakespeare.example/UUID-a1j/7533\" " "type=\"set\">" - "" + "" "" "" ""); diff --git a/tests/qxmppmixitems/tst_qxmppmixitems.cpp b/tests/qxmppmixitems/tst_qxmppmixitems.cpp index 529dfedcc..8c0b0ccf1 100644 --- a/tests/qxmppmixitems/tst_qxmppmixitems.cpp +++ b/tests/qxmppmixitems/tst_qxmppmixitems.cpp @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: LGPL-2.1-or-later +#include "QXmppMixConfigItem.h" #include "QXmppMixInfoItem.h" #include "QXmppMixParticipantItem.h" @@ -12,12 +13,225 @@ class tst_QXmppMixItem : public QObject Q_OBJECT private: + Q_SLOT void testConfig(); + Q_SLOT void testIsConfigItem(); Q_SLOT void testInfo(); Q_SLOT void testIsInfoItem(); Q_SLOT void testParticipant(); Q_SLOT void testIsParticipantItem(); }; +void tst_QXmppMixItem::testConfig() +{ + const QByteArray xml( + "" + "" + "" + "urn:xmpp:mix:admin:0" + "" + "" + "greymalkin@shakespeare.example" + "" + "" + "hecate@shakespeare.example" + "greymalkin@shakespeare.example" + "" + "" + "juliet@shakespeare.example" + "romeo@shakespeare.example" + "" + "" + "2023-12-31T12:30:00Z" + "" + "" + "allowed" + "information" + "" + "" + "allowed" + "" + "" + "nobody" + "" + "" + "allowed" + "" + "" + "admins" + "" + "" + "anyone" + "" + "" + "owners" + "" + "" + "allowed" + "" + "" + "allowed" + "" + "" + "allowed" + "" + "" + "participants" + "" + "" + "false" + "" + "" + "true" + "" + "" + "false" + "" + "" + "true" + "" + "" + "true" + "" + "" + "false" + "" + "" + ""); + + QXmppMixConfigItem item1; + const QDateTime channelDeletion { { 2023, 12, 31 }, { 12, 30 } }; + + QCOMPARE(item1.formType(), QXmppDataForm::None); + QVERIFY(item1.lastEditorJid().isEmpty()); + QVERIFY(item1.ownerJids().isEmpty()); + QVERIFY(item1.administratorJids().isEmpty()); + QVERIFY(!item1.channelDeletion().isValid()); + QVERIFY(!item1.nicknameRequired()); + QVERIFY(!item1.presenceRequired()); + QVERIFY(!item1.onlyParticipantsPermittedToSubmitPresence()); + QVERIFY(!item1.ownMessageRetractionPermitted()); + QVERIFY(!item1.invitationsPermitted()); + QVERIFY(!item1.privateMessagesPermitted()); + + parsePacket(item1, xml); + QCOMPARE(item1.formType(), QXmppDataForm::Result); + QCOMPARE(item1.lastEditorJid(), QStringLiteral("greymalkin@shakespeare.example")); + QCOMPARE(item1.ownerJids(), QStringList({ QStringLiteral("hecate@shakespeare.example"), QStringLiteral("greymalkin@shakespeare.example") })); + QCOMPARE(item1.administratorJids(), QStringList({ QStringLiteral("juliet@shakespeare.example"), QStringLiteral("romeo@shakespeare.example") })); + QCOMPARE(item1.channelDeletion(), QDateTime({ { 2023, 12, 31 }, { 12, 30 }, Qt::UTC })); + QCOMPARE(item1.nodes(), QXmppMixConfigItem::Node::AllowedJids | QXmppMixConfigItem::Node::Information); + QCOMPARE(item1.messagesSubscribeRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item1.messagesRetractRole(), QXmppMixConfigItem::Role::Nobody); + QCOMPARE(item1.presenceSubscribeRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item1.participantsSubscribeRole(), QXmppMixConfigItem::Role::Administrator); + QCOMPARE(item1.informationSubscribeRole(), QXmppMixConfigItem::Role::Anyone); + QCOMPARE(item1.informationUpdateRole(), QXmppMixConfigItem::Role::Owner); + QCOMPARE(item1.allowedJidsSubscribeRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item1.bannedJidsSubscribeRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item1.configurationReadRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item1.avatarUpdateRole(), QXmppMixConfigItem::Role::Participant); + QVERIFY(item1.nicknameRequired()); + QVERIFY(!*item1.nicknameRequired()); + QVERIFY(item1.presenceRequired()); + QVERIFY(*item1.presenceRequired()); + QVERIFY(item1.onlyParticipantsPermittedToSubmitPresence()); + QVERIFY(!*item1.onlyParticipantsPermittedToSubmitPresence()); + QVERIFY(item1.ownMessageRetractionPermitted()); + QVERIFY(*item1.ownMessageRetractionPermitted()); + QVERIFY(item1.invitationsPermitted()); + QVERIFY(*item1.invitationsPermitted()); + QVERIFY(item1.privateMessagesPermitted()); + QVERIFY(!*item1.privateMessagesPermitted()); + serializePacket(item1, xml); + + QXmppMixConfigItem item2; + item2.setId(QStringLiteral("2016-05-30T09:00:00")); + item2.setFormType(QXmppDataForm::Result); + item2.setLastEditorJid(QStringLiteral("greymalkin@shakespeare.example")); + item2.setOwnerJids(QStringList({ QStringLiteral("hecate@shakespeare.example"), + QStringLiteral("greymalkin@shakespeare.example") })); + item2.setAdministratorJids(QStringList({ QStringLiteral("juliet@shakespeare.example"), + QStringLiteral("romeo@shakespeare.example") })); + item2.setChannelDeletion({ { 2023, 12, 31 }, { 12, 30 }, Qt::UTC }); + item2.setNodes(QXmppMixConfigItem::Node::AllowedJids | QXmppMixConfigItem::Node::Information); + item2.setMessagesSubscribeRole(QXmppMixConfigItem::Role::Allowed); + item2.setMessagesRetractRole(QXmppMixConfigItem::Role::Nobody); + item2.setPresenceSubscribeRole(QXmppMixConfigItem::Role::Allowed); + item2.setParticipantsSubscribeRole(QXmppMixConfigItem::Role::Administrator); + item2.setInformationSubscribeRole(QXmppMixConfigItem::Role::Anyone); + item2.setInformationUpdateRole(QXmppMixConfigItem::Role::Owner); + item2.setAllowedJidsSubscribeRole(QXmppMixConfigItem::Role::Allowed); + item2.setBannedJidsSubscribeRole(QXmppMixConfigItem::Role::Allowed); + item2.setConfigurationReadRole(QXmppMixConfigItem::Role::Allowed); + item2.setAvatarUpdateRole(QXmppMixConfigItem::Role::Participant); + item2.setNicknameRequired(false); + item2.setPresenceRequired(true); + item2.setOnlyParticipantsPermittedToSubmitPresence(false); + item2.setOwnMessageRetractionPermitted(true); + item2.setInvitationsPermitted(true); + item2.setPrivateMessagesPermitted(false); + + QCOMPARE(item2.formType(), QXmppDataForm::Result); + QCOMPARE(item2.lastEditorJid(), QStringLiteral("greymalkin@shakespeare.example")); + QCOMPARE(item2.ownerJids(), QStringList({ QStringLiteral("hecate@shakespeare.example"), QStringLiteral("greymalkin@shakespeare.example") })); + QCOMPARE(item2.administratorJids(), QStringList({ QStringLiteral("juliet@shakespeare.example"), QStringLiteral("romeo@shakespeare.example") })); + QCOMPARE(item2.channelDeletion(), QDateTime({ { 2023, 12, 31 }, { 12, 30 }, Qt::UTC })); + QCOMPARE(item2.nodes(), QXmppMixConfigItem::Node::AllowedJids | QXmppMixConfigItem::Node::Information); + QCOMPARE(item2.messagesSubscribeRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item2.messagesRetractRole(), QXmppMixConfigItem::Role::Nobody); + QCOMPARE(item2.presenceSubscribeRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item2.participantsSubscribeRole(), QXmppMixConfigItem::Role::Administrator); + QCOMPARE(item2.informationSubscribeRole(), QXmppMixConfigItem::Role::Anyone); + QCOMPARE(item2.informationUpdateRole(), QXmppMixConfigItem::Role::Owner); + QCOMPARE(item2.allowedJidsSubscribeRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item2.bannedJidsSubscribeRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item2.configurationReadRole(), QXmppMixConfigItem::Role::Allowed); + QCOMPARE(item2.avatarUpdateRole(), QXmppMixConfigItem::Role::Participant); + QVERIFY(item2.nicknameRequired()); + QVERIFY(!*item2.nicknameRequired()); + QVERIFY(item2.presenceRequired()); + QVERIFY(*item2.presenceRequired()); + QVERIFY(item2.onlyParticipantsPermittedToSubmitPresence()); + QVERIFY(!*item2.onlyParticipantsPermittedToSubmitPresence()); + QVERIFY(item2.ownMessageRetractionPermitted()); + QVERIFY(*item2.ownMessageRetractionPermitted()); + QVERIFY(item2.invitationsPermitted()); + QVERIFY(*item2.invitationsPermitted()); + QVERIFY(item2.privateMessagesPermitted()); + QVERIFY(!*item2.privateMessagesPermitted()); + serializePacket(item2, xml); +} + +void tst_QXmppMixItem::testIsConfigItem() +{ + QDomDocument doc; + QDomElement element; + + const QByteArray xmlCorrect( + "" + "" + "" + "urn:xmpp:mix:admin:0" + "" + "" + ""); + QVERIFY(doc.setContent(xmlCorrect, true)); + element = doc.documentElement(); + QVERIFY(QXmppMixConfigItem::isItem(element)); + + const QByteArray xmlWrong( + "" + "" + "" + "other:namespace" + "" + "" + ""); + QVERIFY(doc.setContent(xmlWrong, true)); + element = doc.documentElement(); + QVERIFY(!QXmppMixConfigItem::isItem(element)); +} + void tst_QXmppMixItem::testInfo() { const QByteArray xml( @@ -41,21 +255,26 @@ void tst_QXmppMixItem::testInfo() ""); QXmppMixInfoItem item; - parsePacket(item, xml); + QVERIFY(item.name().isEmpty()); + QVERIFY(item.description().isEmpty()); + QVERIFY(item.contactJids().isEmpty()); - QCOMPARE(item.name(), QString("Witches Coven")); - QCOMPARE(item.description(), QString("A location not far from the blasted " - "heath where the three witches meet")); + parsePacket(item, xml); + QCOMPARE(item.formType(), QXmppDataForm::Result); + QCOMPARE(item.name(), QStringLiteral("Witches Coven")); + QCOMPARE(item.description(), QStringLiteral("A location not far from the blasted " + "heath where the three witches meet")); QCOMPARE(item.contactJids(), QStringList() << "greymalkin@shakespeare.example" << "joan@shakespeare.example"); - serializePacket(item, xml); // test setters + item.setFormType(QXmppDataForm::Submit); + QCOMPARE(item.formType(), QXmppDataForm::Submit); item.setName("Skynet Development"); - QCOMPARE(item.name(), QString("Skynet Development")); + QCOMPARE(item.name(), QStringLiteral("Skynet Development")); item.setDescription("Very cool development group."); - QCOMPARE(item.description(), QString("Very cool development group.")); + QCOMPARE(item.description(), QStringLiteral("Very cool development group.")); item.setContactJids(QStringList() << "somebody@example.org"); QCOMPARE(item.contactJids(), QStringList() << "somebody@example.org"); } @@ -101,18 +320,19 @@ void tst_QXmppMixItem::testParticipant() ""); QXmppMixParticipantItem item; - parsePacket(item, xml); - - QCOMPARE(item.nick(), QString("thirdwitch")); - QCOMPARE(item.jid(), QString("hag66@shakespeare.example")); + QVERIFY(item.nick().isEmpty()); + QVERIFY(item.jid().isEmpty()); + parsePacket(item, xml); + QCOMPARE(item.nick(), QStringLiteral("thirdwitch")); + QCOMPARE(item.jid(), QStringLiteral("hag66@shakespeare.example")); serializePacket(item, xml); // test setters item.setNick("thomasd"); - QCOMPARE(item.nick(), QString("thomasd")); + QCOMPARE(item.nick(), QStringLiteral("thomasd")); item.setJid("thomas@d.example"); - QCOMPARE(item.jid(), QString("thomas@d.example")); + QCOMPARE(item.jid(), QStringLiteral("thomas@d.example")); } void tst_QXmppMixItem::testIsParticipantItem() diff --git a/tests/qxmppmixmanager/tst_qxmppmixmanager.cpp b/tests/qxmppmixmanager/tst_qxmppmixmanager.cpp new file mode 100644 index 000000000..6626db4a9 --- /dev/null +++ b/tests/qxmppmixmanager/tst_qxmppmixmanager.cpp @@ -0,0 +1,1721 @@ +// SPDX-FileCopyrightText: 2023 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppMixInfoItem.h" +#include "QXmppMixInvitation.h" +#include "QXmppMixManager.h" +#include "QXmppPubSubEvent.h" +#include "QXmppPubSubManager.h" + +#include "TestClient.h" + +struct Tester +{ + Tester() { } + + Tester(const QString &jid) + { + client.configuration().setJid(jid); + } + + TestClient client; + QXmppMixManager *manager = client.addNewExtension(); +}; + +struct MessageTester +{ + MessageTester() + { + client.logger()->setLoggingType(QXmppLogger::SignalLogging); + } + + MessageTester(const QString &jid) + : MessageTester() + { + client.configuration().setJid(jid); + } + + QXmppClient client; + QXmppMixManager *manager = client.addNewExtension(); +}; + +class tst_QXmppMixManager : public QObject +{ + Q_OBJECT + +private: + Q_SLOT void testDiscoveryFeatures(); + Q_SLOT void testSupportedByServer(); + Q_SLOT void testArchivingSupportedByServer(); + Q_SLOT void testService(); + Q_SLOT void testServices(); + Q_SLOT void testHandleDiscoInfo(); + Q_SLOT void testAddJidToNode(); + Q_SLOT void testRequestJids(); + Q_SLOT void testDeleteNode(); + Q_SLOT void testSendInvitationPrivate(); + Q_SLOT void testJoinChannelPrivate(); + Q_SLOT void testPrepareJoinIq(); + Q_SLOT void testHandlePubSubEvent(); + Q_SLOT void testHandleMessage(); + Q_SLOT void testSetClient(); + Q_SLOT void testCreateChannel(); + Q_SLOT void testCreateChannelWithId(); + Q_SLOT void testRequestChannelJids(); + Q_SLOT void testRequestChannelNodes(); + Q_SLOT void testRequestChannelConfiguration(); + Q_SLOT void testUpdateChannelConfiguration(); + Q_SLOT void testRequestChannelInformation(); + Q_SLOT void testUpdateChannelInformation(); + Q_SLOT void testJoinChannel(); + Q_SLOT void testJoinChannelWithNickname(); + Q_SLOT void testJoinChannelWithNodes(); + Q_SLOT void testSendInvitationPrivateWithBody(); + Q_SLOT void testSendInvitation(); + Q_SLOT void testSendInvitationWithBody(); + Q_SLOT void testInvite(); + Q_SLOT void testAcceptInvitation(); + Q_SLOT void testAcceptInvitationWithNickname(); + Q_SLOT void testAcceptInvitationWithNodes(); + Q_SLOT void testUpdateNickname(); + Q_SLOT void testUpdateSubscriptions(); + Q_SLOT void testRequestAllowedJids(); + Q_SLOT void testAllowJid(); + Q_SLOT void testDisallowJid(); + Q_SLOT void testDisallowAllJids(); + Q_SLOT void testRequestBannedJids(); + Q_SLOT void testBanJid(); + Q_SLOT void testUnbanJid(); + Q_SLOT void testUnbanAllJids(); + Q_SLOT void testRequestParticipants(); + Q_SLOT void testLeaveChannel(); + Q_SLOT void testDeleteChannel(); + + template + void testErrorFromChannel(QXmppTask &task, TestClient &client); + template + void testErrorFromChannel(QXmppTask &task, TestClient &client, const QString &id); + template + void testError(QXmppTask &task, TestClient &client, const QString &id, const QString &from); +}; + +void tst_QXmppMixManager::testDiscoveryFeatures() +{ + QXmppMixManager manager; + QCOMPARE(manager.discoveryFeatures(), QStringList { "urn:xmpp:mix:core:1" }); +} + +void tst_QXmppMixManager::testSupportedByServer() +{ + QXmppMixManager manager; + QSignalSpy spy(&manager, &QXmppMixManager::supportedByServerChanged); + + QVERIFY(!manager.supportedByServer()); + manager.setSupportedByServer(true); + QVERIFY(manager.supportedByServer()); + QCOMPARE(spy.size(), 1); +} + +void tst_QXmppMixManager::testArchivingSupportedByServer() +{ + QXmppMixManager manager; + QSignalSpy spy(&manager, &QXmppMixManager::archivingSupportedByServerChanged); + + QVERIFY(!manager.archivingSupportedByServer()); + manager.setArchivingSupportedByServer(true); + QVERIFY(manager.archivingSupportedByServer()); + QCOMPARE(spy.size(), 1); +} + +void tst_QXmppMixManager::testService() +{ + QXmppMixManager::Service service1; + + QVERIFY(service1.jid.isEmpty()); + QVERIFY(!service1.channelsSearchable); + QVERIFY(!service1.channelCreationAllowed); + + service1.jid = QStringLiteral("mix.shakespeare.example"); + service1.channelsSearchable = true; + service1.channelCreationAllowed = false; + + QXmppMixManager::Service service2; + service2.jid = QStringLiteral("mix.shakespeare.example"); + service2.channelsSearchable = true; + service2.channelCreationAllowed = false; + + QCOMPARE(service1, service2); + + QXmppMixManager::Service service3; + service3.jid = QStringLiteral("mix.shakespeare.example"); + service3.channelsSearchable = true; + service3.channelCreationAllowed = true; + + QVERIFY(!(service1 == service3)); +} + +void tst_QXmppMixManager::testServices() +{ + QXmppMixManager manager; + QSignalSpy spy(&manager, &QXmppMixManager::servicesChanged); + + QXmppMixManager::Service service; + service.jid = QStringLiteral("mix.shakespeare.example"); + + QVERIFY(manager.services().isEmpty()); + + manager.addService(service); + QCOMPARE(manager.services().size(), 1); + QCOMPARE(manager.services().at(0).jid, service.jid); + manager.addService(service); + QCOMPARE(spy.size(), 1); + + manager.removeService(QStringLiteral("mix1.shakespeare.example")); + QCOMPARE(manager.services().size(), 1); + QCOMPARE(spy.size(), 1); + + manager.removeService(service.jid); + QVERIFY(manager.services().isEmpty()); + QCOMPARE(spy.size(), 2); + + manager.addService(service); + service.channelsSearchable = true; + manager.addService(service); + QCOMPARE(manager.services().size(), 1); + QCOMPARE(manager.services().at(0).jid, service.jid); + QCOMPARE(manager.services().at(0).channelsSearchable, service.channelsSearchable); + QCOMPARE(spy.size(), 4); + + service.jid = QStringLiteral("mix1.shakespeare.example"); + manager.addService(service); + manager.removeServices(); + QVERIFY(manager.services().isEmpty()); + QCOMPARE(spy.size(), 6); +} + +void tst_QXmppMixManager::testHandleDiscoInfo() +{ + auto [client, manager] = MessageTester(QStringLiteral("hag66@shakespeare.example")); + + QXmppDiscoveryIq::Identity identity; + identity.setCategory(QStringLiteral("conference")); + identity.setType(QStringLiteral("mix")); + + QXmppDiscoveryIq iq; + iq.setFeatures({ QStringLiteral("urn:xmpp:mix:pam:2"), + QStringLiteral("urn:xmpp:mix:pam:2#archive"), + QStringLiteral("urn:xmpp:mix:core:1"), + QStringLiteral("urn:xmpp:mix:core:1#searchable"), + QStringLiteral("urn:xmpp:mix:core:1#create-channel") }); + iq.setIdentities({ identity }); + + manager->handleDiscoInfo(iq); + + QVERIFY(manager->supportedByServer()); + QVERIFY(manager->archivingSupportedByServer()); + QCOMPARE(manager->services().at(0).jid, QStringLiteral("shakespeare.example")); + QVERIFY(manager->services().at(0).channelsSearchable); + QVERIFY(manager->services().at(0).channelCreationAllowed); + + iq.setFeatures({}); + iq.setIdentities({}); + + manager->handleDiscoInfo(iq); + + QVERIFY(!manager->supportedByServer()); + QVERIFY(!manager->archivingSupportedByServer()); + QVERIFY(manager->services().isEmpty()); +} + +void tst_QXmppMixManager::testAddJidToNode() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->addJidToNode(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("urn:xmpp:mix:nodes:allowed"), QStringLiteral("alice@wonderland.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); + + // First all injects and then all expects + // TODO: Fix following test cases ("expect(QStringLiteral("qxmpp3"))" results in a stanza with a UUID as its ID instead of "qxmpp3") +} + +void tst_QXmppMixManager::testRequestJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestJids(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("urn:xmpp:mix:nodes:allowed")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto jids = expectFutureVariant>(task); + QCOMPARE(jids.at(0), QStringLiteral("shakespeare.example")); + QCOMPARE(jids.at(1), QStringLiteral("alice@wonderland.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testDeleteNode() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->deleteNode(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("urn:xmpp:mix:nodes:allowed")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + task = call(); + + client.ignore(); + client.inject(QStringLiteral("" + "" + "" + "" + "")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testSendInvitationPrivate() +{ + auto [client, manager] = MessageTester(); + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + *messageSent = true; + + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + QCOMPARE(message.to(), QStringLiteral("cat@shakespeare.example")); + QCOMPARE(message.type(), QXmppMessage::Chat); + QVERIFY(message.hasHint(QXmppMessage::Store)); + QVERIFY(message.body().isEmpty()); + QVERIFY(message.mixInvitation()); + QCOMPARE(message.mixInvitation()->inviteeJid(), QStringLiteral("cat@shakespeare.example")); + } + }); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + manager->sendInvitation(invitation, {}); + + QVERIFY(messageSent); +} + +void tst_QXmppMixManager::testJoinChannelPrivate() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + QXmppMixIq iq; + iq.setType(QXmppIq::Set); + iq.setTo(QStringLiteral("hag66@shakespeare.example")); + iq.setActionType(QXmppMixIq::ClientJoin); + iq.setChannelJid(invitation.channelJid()); + iq.setNick(QStringLiteral("third witch")); + iq.setNodesBeingSubscribedTo(QXmppMixConfigItem::Node::AllowedJids | QXmppMixConfigItem::Node::BannedJids); + iq.setInvitation(invitation); + + return manager->joinChannel(std::move(iq)); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "third witch" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "third witch 2" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123456")); + QCOMPARE(result.nickname, QStringLiteral("third witch 2")); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::AllowedJids); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("hag66@shakespeare.example")); +} + +void tst_QXmppMixManager::testPrepareJoinIq() +{ + auto [client, manager] = Tester(QStringLiteral("hag66@shakespeare.example")); + auto iq = manager->prepareJoinIq(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("third witch"), QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); + + QCOMPARE(iq.type(), QXmppIq::Set); + QCOMPARE(iq.to(), QStringLiteral("hag66@shakespeare.example")); + QCOMPARE(iq.actionType(), QXmppMixIq::ClientJoin); + QCOMPARE(iq.channelJid(), QStringLiteral("coven@mix.shakespeare.example")); + QCOMPARE(iq.nick(), QStringLiteral("third witch")); + QCOMPARE(iq.nodesBeingSubscribedTo(), QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); +} + +void tst_QXmppMixManager::testHandlePubSubEvent() +{ + QXmppMixManager manager; + QSignalSpy jidAllowedSpy(&manager, &QXmppMixManager::jidAllowed); + QSignalSpy allJidsAllowedSpy(&manager, &QXmppMixManager::allJidsAllowed); + QSignalSpy jidDisallowedSpy(&manager, &QXmppMixManager::jidDisallowed); + QSignalSpy allJidsDisallowedSpy(&manager, &QXmppMixManager::allJidsDisallowed); + QSignalSpy jidBannedSpy(&manager, &QXmppMixManager::jidBanned); + QSignalSpy jidUnbannedSpy(&manager, &QXmppMixManager::jidUnbanned); + QSignalSpy allJidsUnbannedSpy(&manager, &QXmppMixManager::allJidsUnbanned); + + QSignalSpy channelConfigurationUpdatedSpy(&manager, &QXmppMixManager::channelConfigurationUpdated); + QSignalSpy channelInformationUpdatedSpy(&manager, &QXmppMixManager::channelInformationUpdated); + QSignalSpy userJoinedOrParticipantModifiedSpy(&manager, &QXmppMixManager::userJoinedOrParticipantModified); + QSignalSpy participantLeftSpy(&manager, &QXmppMixManager::participantLeft); + QSignalSpy channelDeletedSpy(&manager, &QXmppMixManager::channelDeleted); + + const auto channelJid = QStringLiteral("coven@mix.shakespeare.example"); + const auto channelName = QStringLiteral("The Coven"); + const QStringList nodes = { QStringLiteral("urn:xmpp:mix:nodes:allowed"), QStringLiteral("urn:xmpp:mix:nodes:banned") }; + const auto configurationNode = QStringLiteral("urn:xmpp:mix:nodes:config"); + const auto informationNode = QStringLiteral("urn:xmpp:mix:nodes:info"); + const auto participantNode = QStringLiteral("urn:xmpp:mix:nodes:participants"); + const QStringList jids = { QStringLiteral("hag66@shakespeare.example"), QStringLiteral("cat@shakespeare.example") }; + + const auto eventTypes = QVector { QXmppPubSubEventBase::EventType::Configuration, + QXmppPubSubEventBase::EventType::Delete, + QXmppPubSubEventBase::EventType::Items, + QXmppPubSubEventBase::EventType::Retract, + QXmppPubSubEventBase::EventType::Purge, + QXmppPubSubEventBase::EventType::Subscription }; + + QXmppPubSubBaseItem allowedOrBannedJidsItem1; + allowedOrBannedJidsItem1.setId(jids.at(0)); + + QXmppPubSubBaseItem allowedOrBannedJidsItem2; + allowedOrBannedJidsItem2.setId(jids.at(1)); + + QXmppPubSubEvent allowedOrBannedJidsEvent; + allowedOrBannedJidsEvent.setItems({ allowedOrBannedJidsItem1, allowedOrBannedJidsItem2 }); + allowedOrBannedJidsEvent.setRetractIds(jids); + + QXmppMixParticipantItem participantItem1; + participantItem1.setJid(jids.at(0)); + + QXmppMixParticipantItem participantItem2; + participantItem2.setJid(jids.at(1)); + + QXmppPubSubEvent participantEvent; + participantEvent.setItems({ participantItem1, participantItem2 }); + participantEvent.setRetractIds(jids); + + QXmppMixConfigItem configurationItem; + configurationItem.setFormType(QXmppDataForm::Result); + configurationItem.setOwnerJids(jids); + + QXmppPubSubEvent configurationEvent; + configurationEvent.setItems({ configurationItem }); + configurationEvent.setRetractIds(jids); + + QXmppMixInfoItem informationItem; + informationItem.setFormType(QXmppDataForm::Result); + informationItem.setName(channelName); + + QXmppPubSubEvent informationEvent; + informationEvent.setItems({ informationItem }); + informationEvent.setRetractIds(jids); + + for (const auto &node : nodes) { + for (auto eventType : eventTypes) { + allowedOrBannedJidsEvent.setEventType(eventType); + manager.handlePubSubEvent(writePacketToDom(allowedOrBannedJidsEvent), channelJid, node); + } + } + + for (auto eventType : eventTypes) { + participantEvent.setEventType(eventType); + manager.handlePubSubEvent(writePacketToDom(participantEvent), channelJid, participantNode); + } + + for (auto eventType : eventTypes) { + configurationEvent.setEventType(eventType); + manager.handlePubSubEvent(writePacketToDom(configurationEvent), channelJid, configurationNode); + } + + for (auto eventType : eventTypes) { + informationEvent.setEventType(eventType); + manager.handlePubSubEvent(writePacketToDom(informationEvent), channelJid, informationNode); + } + + for (const auto &spy : { &jidAllowedSpy, &jidDisallowedSpy, &jidBannedSpy, &jidUnbannedSpy, &participantLeftSpy }) { + QCOMPARE(spy->size(), 2); + + for (auto i = 0; i < spy->size(); i++) { + const auto &arguments = spy->at(i); + QCOMPARE(arguments.at(0).toString(), channelJid); + QCOMPARE(arguments.at(1).toString(), jids.at(i)); + } + } + + for (const auto &spy : { &allJidsAllowedSpy, &allJidsDisallowedSpy }) { + QCOMPARE(spy->size(), 1); + auto arguments = spy->constFirst(); + QCOMPARE(arguments.at(0).toString(), channelJid); + } + + for (const auto &spy : { &allJidsUnbannedSpy, &channelDeletedSpy }) { + QCOMPARE(spy->size(), 2); + for (const auto &arguments : *spy) { + QCOMPARE(arguments.at(0).toString(), channelJid); + } + } + + QCOMPARE(userJoinedOrParticipantModifiedSpy.size(), 2); + for (auto i = 0; i < userJoinedOrParticipantModifiedSpy.size(); i++) { + const auto &arguments = userJoinedOrParticipantModifiedSpy.at(i); + QCOMPARE(arguments.at(0).toString(), channelJid); + QCOMPARE(arguments.at(1).value().jid(), participantEvent.items().at(i).jid()); + } + + for (const auto &spy : { &channelConfigurationUpdatedSpy, &channelInformationUpdatedSpy }) { + QCOMPARE(spy->size(), 1); + auto arguments = spy->constFirst(); + QCOMPARE(arguments.at(0).toString(), channelJid); + } + + QCOMPARE(channelConfigurationUpdatedSpy.first().at(1).value().ownerJids(), jids); + QCOMPARE(channelInformationUpdatedSpy.first().at(1).value().name(), channelName); +} + +void tst_QXmppMixManager::testHandleMessage() +{ + QXmppMixManager manager; + + const QObject context; + auto invitationEmitted = std::make_shared(false); + + connect(&manager, &QXmppMixManager::invited, &context, [invitationEmitted](const QXmppMixInvitation &invitation) { + *invitationEmitted = true; + + QCOMPARE(invitation.inviterJid(), QStringLiteral("hag66@shakespeare.example")); + QCOMPARE(invitation.inviteeJid(), QStringLiteral("cat@shakespeare.example")); + QCOMPARE(invitation.channelJid(), QStringLiteral("coven@mix.shakespeare.example")); + QCOMPARE(invitation.token(), QStringLiteral("ABCDEF")); + }); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + QXmppMessage message; + QVERIFY(!manager.handleMessage(message)); + + message.setMixInvitation(invitation); + QVERIFY(manager.handleMessage(message)); + QVERIFY(*invitationEmitted); +} + +void tst_QXmppMixManager::testSetClient() +{ + QXmppClient client; + QXmppMixManager manager; + + client.configuration().setJid(QStringLiteral("hag66@shakespeare.example")); + manager.setClient(&client); + + QXmppMixManager::Service service; + service.jid = QStringLiteral("mix.shakespeare.example"); + service.channelsSearchable = true; + service.channelCreationAllowed = false; + + manager.setSupportedByServer(true); + manager.setArchivingSupportedByServer(true); + manager.addService(service); + + Q_EMIT client.disconnected(); + QVERIFY(!manager.supportedByServer()); + QVERIFY(!manager.archivingSupportedByServer()); + QVERIFY(manager.services().isEmpty()); + + QVERIFY(client.findExtension()); + QVERIFY(manager.m_discoveryManager); + + QXmppDiscoveryIq iq; + iq.setFeatures({ QStringLiteral("urn:xmpp:mix:pam:2") }); + Q_EMIT manager.m_discoveryManager->infoReceived(iq); + QVERIFY(manager.supportedByServer()); + + QVERIFY(client.findExtension()); + QVERIFY(manager.m_pubSubManager); +} + +void tst_QXmppMixManager::testCreateChannel() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->createChannel(QStringLiteral("mix.shakespeare.example")); + }; + + auto task = call(); + + client.inject(QStringLiteral("" + "" + "")); + client.expect(QStringLiteral("" + "" + "")); + + auto channelJid = expectFutureVariant(task); + QCOMPARE(channelJid, QStringLiteral("A1B2C345@mix.shakespeare.example")); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("mix.shakespeare.example")); +} + +void tst_QXmppMixManager::testCreateChannelWithId() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->createChannel(QStringLiteral("mix.shakespeare.example"), QStringLiteral("coven")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("" + "" + "")); + + auto channelJid = expectFutureVariant(task); + QCOMPARE(channelJid, QStringLiteral("coven@mix.shakespeare.example")); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("mix.shakespeare.example")); +} + +void tst_QXmppMixManager::testRequestChannelJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestChannelJids(QStringLiteral("mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + + auto jids = expectFutureVariant>(task); + QCOMPARE(jids.size(), 3); + QCOMPARE(jids.at(0), QStringLiteral("coven@mix.shakespeare.example")); + QCOMPARE(jids.at(1), QStringLiteral("spells@mix.shakespeare.example")); + QCOMPARE(jids.at(2), QStringLiteral("wizards@mix.shakespeare.example")); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("mix.shakespeare.example")); +} + +void tst_QXmppMixManager::testRequestChannelNodes() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestChannelNodes(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "")); + + auto nodes = expectFutureVariant(task); + QCOMPARE(nodes, QXmppMixConfigItem::Node::AllowedJids | QXmppMixConfigItem::Node::Presence); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestChannelConfiguration() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->requestChannelConfiguration(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "urn:xmpp:mix:admin:0" + "" + "" + "greymalkin@shakespeare.example" + "" + "" + "" + "" + "" + "")); + + auto configuration = expectFutureVariant(task); + QCOMPARE(configuration.lastEditorJid(), QStringLiteral("greymalkin@shakespeare.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUpdateChannelConfiguration() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + QXmppMixConfigItem configuration; + configuration.setId(QStringLiteral("2016-05-30T09:00:00")); + configuration.setOwnerJids({ QStringLiteral("greymalkin@shakespeare.example") }); + + auto call = [manager, configuration]() { + return manager->updateChannelConfiguration(QStringLiteral("coven@mix.shakespeare.example"), configuration); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "urn:xmpp:mix:admin:0" + "" + "" + "greymalkin@shakespeare.example" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestChannelInformation() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->requestChannelInformation(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "urn:xmpp:mix:core:1" + "" + "" + "Witches Coven" + "" + "" + "" + "" + "" + "")); + + auto information = expectFutureVariant(task); + QCOMPARE(information.name(), QStringLiteral("Witches Coven")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUpdateChannelInformation() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + QXmppMixInfoItem information; + information.setId(QStringLiteral("2016-05-30T09:00:00")); + information.setName(QStringLiteral("The Coven")); + + auto call = [manager, information]() { + return manager->updateChannelInformation(QStringLiteral("coven@mix.shakespeare.example"), information); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "urn:xmpp:mix:core:1" + "" + "" + "The Coven" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testJoinChannel() +{ + auto tester = Tester(QStringLiteral("hag66@shakespeare.example")); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + return manager->joinChannel(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123456")); + QVERIFY(result.nickname.isEmpty()); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("hag66@shakespeare.example")); +} + +void tst_QXmppMixManager::testJoinChannelWithNickname() +{ + auto [client, manager] = Tester(QStringLiteral("hag66@shakespeare.example")); + + auto task = manager->joinChannel(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("third witch")); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "third witch" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "third witch" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123456")); + QCOMPARE(result.nickname, QStringLiteral("third witch")); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); +} + +void tst_QXmppMixManager::testJoinChannelWithNodes() +{ + auto [client, manager] = Tester(QStringLiteral("hag66@shakespeare.example")); + + auto task = manager->joinChannel(QStringLiteral("coven@mix.shakespeare.example"), {}, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, "123456"); + QVERIFY(result.nickname.isEmpty()); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); +} + +void tst_QXmppMixManager::testSendInvitationPrivateWithBody() +{ + auto [client, manager] = MessageTester(QStringLiteral("hag66@shakespeare.example")); + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + *messageSent = true; + + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + QCOMPARE(message.to(), QStringLiteral("cat@shakespeare.example")); + QVERIFY(!message.hasHint(QXmppMessage::Store)); + QCOMPARE(message.body(), QStringLiteral("Would you like to join the coven?")); + QVERIFY(message.mixInvitation()); + QCOMPARE(message.mixInvitation()->token(), QStringLiteral("ABCDEF")); + } + }); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + manager->sendInvitation(invitation, QStringLiteral("Would you like to join the coven?")); + + QVERIFY(messageSent); +} + +void tst_QXmppMixManager::testSendInvitation() +{ + auto [client, manager] = MessageTester(QStringLiteral("hag66@shakespeare.example")); + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + *messageSent = true; + + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + QVERIFY(message.body().isEmpty()); + QVERIFY(message.mixInvitation()); + QCOMPARE(message.mixInvitation()->inviterJid(), QStringLiteral("hag66@shakespeare.example")); + QCOMPARE(message.mixInvitation()->inviteeJid(), QStringLiteral("cat@shakespeare.example")); + QCOMPARE(message.mixInvitation()->channelJid(), QStringLiteral("coven@mix.shakespeare.example")); + QVERIFY(message.mixInvitation()->token().isEmpty()); + } + }); + + manager->sendInvitation(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("cat@shakespeare.example")); + + QVERIFY(messageSent); +} + +void tst_QXmppMixManager::testSendInvitationWithBody() +{ + auto [client, manager] = MessageTester(QStringLiteral("hag66@shakespeare.example")); + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + *messageSent = true; + + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + QCOMPARE(message.body(), QStringLiteral("Would you like to join the coven?")); + } + }); + + manager->sendInvitation(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("cat@shakespeare.example"), QStringLiteral("Would you like to join the coven?")); + + QVERIFY(messageSent); +} + +void tst_QXmppMixManager::testInvite() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + auto logger = client.logger(); + + const QObject context; + auto messageSent = std::make_shared(false); + + auto call = [&client, manager]() { + return manager->invite(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("cat@shakespeare.example")); + }; + + connect(logger, &QXmppLogger::message, &context, [messageSent](QXmppLogger::MessageType type, const QString &text) { + if (type == QXmppLogger::SentMessage) { + QXmppMessage message; + parsePacket(message, text.toUtf8()); + + // Ignore stream management stanzas enabled by Tester. + if (message.mixInvitation()) { + *messageSent = true; + + QCOMPARE(message.to(), QStringLiteral("cat@shakespeare.example")); + QCOMPARE(message.type(), QXmppMessage::Chat); + QVERIFY(message.hasHint(QXmppMessage::Store)); + QVERIFY(message.body().isEmpty()); + QVERIFY(message.mixInvitation()); + QCOMPARE(message.mixInvitation()->token(), QStringLiteral("ABCDEF")); + } + } + }); + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "cat@shakespeare.example" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "")); + + QVERIFY(*messageSent); + + // TODO: Find a way such that the following line succeeds. + // expectFutureVariant(task); + + // TODO: Fix error parsing in QXmppStream::handleIqResponse() to make sendIq() return QXmppError instead of returning stanza as QDomElement + // testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testAcceptInvitation() +{ + auto tester = Tester(QStringLiteral("cat@shakespeare.example")); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [manager]() { + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + return manager->acceptInvitation(invitation); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123457")); + QVERIFY(result.nickname.isEmpty()); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("cat@shakespeare.example")); +} + +void tst_QXmppMixManager::testAcceptInvitationWithNickname() +{ + auto [client, manager] = Tester(QStringLiteral("cat@shakespeare.example")); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + auto task = manager->acceptInvitation(invitation, QStringLiteral("fourth witch")); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "fourth witch" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "fourth witch" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, QStringLiteral("123457")); + QCOMPARE(result.nickname, QStringLiteral("fourth witch")); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); +} + +void tst_QXmppMixManager::testAcceptInvitationWithNodes() +{ + auto [client, manager] = Tester(QStringLiteral("cat@shakespeare.example")); + + QXmppMixInvitation invitation; + invitation.setInviterJid(QStringLiteral("hag66@shakespeare.example")); + invitation.setInviteeJid(QStringLiteral("cat@shakespeare.example")); + invitation.setChannelJid(QStringLiteral("coven@mix.shakespeare.example")); + invitation.setToken(QStringLiteral("ABCDEF")); + + auto task = manager->acceptInvitation(invitation, {}, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "hag66@shakespeare.example" + "cat@shakespeare.example" + "coven@mix.shakespeare.example" + "ABCDEF" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.participantId, "123457"); + QVERIFY(result.nickname.isEmpty()); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); +} + +void tst_QXmppMixManager::testUpdateNickname() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->updateNickname(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("third witch")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "third witch" + "" + "")); + client.inject(QStringLiteral("" + "" + "third witch 2" + "" + "")); + + auto nickname = expectFutureVariant(task); + QCOMPARE(nickname, "third witch 2"); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUpdateSubscriptions() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->updateSubscriptions(QStringLiteral("coven@mix.shakespeare.example"), QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence, QXmppMixConfigItem::Node::Configuration | QXmppMixConfigItem::Node::Information); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto result = expectFutureVariant(task); + QCOMPARE(result.nodesBeingSubscribedTo, QXmppMixConfigItem::Node::Messages | QXmppMixConfigItem::Node::Presence); + QCOMPARE(result.nodesBeingUnsubscribedFrom, QXmppMixConfigItem::Node::Configuration | QXmppMixConfigItem::Node::Information); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestAllowedJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestAllowedJids(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto allowedJids = expectFutureVariant>(task); + QCOMPARE(allowedJids.at(0), QStringLiteral("shakespeare.example")); + QCOMPARE(allowedJids.at(1), QStringLiteral("alice@wonderland.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testAllowJid() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->allowJid(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("alice@wonderland.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testDisallowJid() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->disallowJid(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("alice@wonderland.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testDisallowAllJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->disallowAllJids(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestBannedJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestBannedJids(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "" + "" + "")); + + auto allowedJids = expectFutureVariant>(task); + QCOMPARE(allowedJids.at(0), QStringLiteral("lear@shakespeare.example")); + QCOMPARE(allowedJids.at(1), QStringLiteral("macbeth@shakespeare.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testBanJid() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->banJid(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("macbeth@shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUnbanJid() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->unbanJid(QStringLiteral("coven@mix.shakespeare.example"), QStringLiteral("macbeth@shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testUnbanAllJids() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->unbanAllJids(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testRequestParticipants() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->requestParticipants(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "" + "thirdwitch" + "hag66@shakespeare.example" + "" + "" + "" + "" + "fourthwitch" + "hag67@shakespeare.example" + "" + "" + "" + "" + "")); + + auto participants = expectFutureVariant>(task); + QCOMPARE(participants.at(0).jid(), QStringLiteral("hag66@shakespeare.example")); + QCOMPARE(participants.at(1).jid(), QStringLiteral("hag67@shakespeare.example")); + + testErrorFromChannel(task = call(), client); +} + +void tst_QXmppMixManager::testLeaveChannel() +{ + auto tester = Tester(QStringLiteral("hag66@shakespeare.example")); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->leaveChannel(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "" + "" + "")); + client.inject(QStringLiteral("" + "" + "" + "" + "")); + + expectFutureVariant(task); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("hag66@shakespeare.example")); +} + +void tst_QXmppMixManager::testDeleteChannel() +{ + auto tester = Tester(); + auto &client = tester.client; + auto manager = tester.manager; + + auto call = [&client, manager]() { + return manager->deleteChannel(QStringLiteral("coven@mix.shakespeare.example")); + }; + + auto task = call(); + + client.expect(QStringLiteral("" + "" + "")); + client.inject(QStringLiteral("")); + + expectFutureVariant(task); + + testError(task = call(), client, QStringLiteral("qxmpp1"), QStringLiteral("mix.shakespeare.example")); +} + +template +void tst_QXmppMixManager::testErrorFromChannel(QXmppTask &task, TestClient &client) +{ + testErrorFromChannel(task, client, QStringLiteral("qxmpp1")); +} + +template +void tst_QXmppMixManager::testErrorFromChannel(QXmppTask &task, TestClient &client, const QString &id) +{ + testError(task, client, id, QStringLiteral("coven@mix.shakespeare.example")); +} + +template +void tst_QXmppMixManager::testError(QXmppTask &task, TestClient &client, const QString &id, const QString &from) +{ + client.ignore(); + client.inject(QStringLiteral("" + "" + "" + "" + "") + .arg(id, from)); + + expectFutureVariant(task); +} + +QTEST_MAIN(tst_QXmppMixManager) +#include "tst_qxmppmixmanager.moc"