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