Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AccountMigration: Improve and fix migration of MIX channels #657

Merged
merged 9 commits into from
Nov 16, 2024
30 changes: 30 additions & 0 deletions src/base/Algorithms.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@

namespace QXmpp::Private {

template<typename T>
constexpr bool HasShrinkToFit = requires(const T &t) {
t.shrink_to_fit();
};

template<typename T>
constexpr bool HasSqueeze = requires(const T &t) {
t.squeeze();
};

template<typename OutputVector, typename InputVector, typename Converter>
auto transform(const InputVector &input, Converter convert)
{
Expand All @@ -24,6 +34,26 @@ auto transform(const InputVector &input, Converter convert)
return output;
}

template<typename OutputVector, typename InputVector, typename Converter>
auto transformFilter(const InputVector &input, Converter convert)
{
OutputVector output;
if constexpr (std::ranges::sized_range<InputVector>) {
output.reserve(input.size());
}
for (const auto &value : input) {
if (const std::optional<std::decay_t<decltype(value)>> result = std::invoke(convert, value)) {
output.push_back(*result);
}
}
if constexpr (HasShrinkToFit<InputVector>) {
output.shrink_to_fit();
} else if constexpr (HasSqueeze<InputVector>) {
output.squeeze();
}
return output;
}

template<typename Vec, typename T>
auto contains(const Vec &vec, const T &value)
{
Expand Down
51 changes: 48 additions & 3 deletions src/client/QXmppAccountMigrationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "QXmppTask.h"
#include "QXmppUtils_p.h"

#include "Algorithms.h"
#include "StringLiterals.h"

#include <QDomElement>
Expand All @@ -30,6 +31,16 @@ struct XmlElementId {
{
return tagName == other.tagName && xmlns == other.xmlns;
}

bool operator<(const XmlElementId &other) const {
const auto result = xmlns.compare(other.xmlns);

if (result == 0) {
return tagName.compare(other.tagName) < 0;
}

return result < 0;
}
};

#ifndef QXMPP_DOC
Expand All @@ -47,6 +58,12 @@ struct std::hash<XmlElementId> {
using AnyParser = QXmppExportData::ExtensionParser<std::any>;
using AnySerializer = QXmppExportData::ExtensionSerializer<std::any>;

static std::unordered_map<std::type_index, XmlElementId> &accountDataMapping()
{
thread_local static std::unordered_map<std::type_index, XmlElementId> registry;
return registry;
}

static std::unordered_map<XmlElementId, AnyParser> &accountDataParsers()
{
thread_local static std::unordered_map<XmlElementId, AnyParser> registry;
Expand Down Expand Up @@ -102,18 +119,36 @@ std::variant<QXmppExportData, QXmppError> QXmppExportData::fromDom(const QDomEle

void QXmppExportData::toXml(QXmlStreamWriter *writer) const
{
// We need to generate the xml file with nodes always in the same order.
// This is needed for our unit tests which are based on xml generation.
const auto sortedExtensionsKeys = [this]() {
auto keys = transform<std::vector<std::pair<std::type_index, XmlElementId>>>(accountDataMapping(), [](auto pair) {
return pair;
});
std::ranges::stable_sort(keys, [](const auto &left, const auto &right) {
return left.second < right.second;
});
return keys;
}();

writer->writeStartDocument();
writer->writeStartElement(QSL65("account-data"));
writer->writeDefaultNamespace(toString65(ns_qxmpp_export));
writer->writeAttribute(QSL65("jid"), d->accountJid);

const auto &serializers = accountDataSerializers();
for (const auto &[typeIndex, extension] : std::as_const(d->extensions)) {
for (const auto &sortedKey : sortedExtensionsKeys) {
const auto &typeIndex = sortedKey.first;

if (!d->extensions.contains(typeIndex)) {
continue;
}

const auto serializer = serializers.find(typeIndex);
if (serializer != serializers.end()) {
const auto &[_, serialize] = *serializer;

serialize(extension, *writer);
serialize(d->extensions.at(typeIndex), *writer);
}
}

Expand Down Expand Up @@ -143,7 +178,9 @@ void QXmppExportData::setExtension(std::any value)

void QXmppExportData::registerExtensionInternal(std::type_index type, ExtensionParser<std::any> parse, ExtensionSerializer<std::any> serialize, QStringView tagName, QStringView xmlns)
{
accountDataParsers().emplace(XmlElementId { tagName.toString(), xmlns.toString() }, parse);
const auto id = XmlElementId { tagName.toString(), xmlns.toString() };
accountDataMapping().emplace(type, id);
accountDataParsers().emplace(id, parse);
accountDataSerializers().emplace(type, serialize);
}

Expand Down Expand Up @@ -189,6 +226,14 @@ struct QXmppAccountMigrationManagerPrivate {
/// Contains T or QXmppError.
///

///
/// \fn QXmppAccountMigrationManager::errorOccurred(const QXmppError &error)
///
/// Emitted when an error occured during export or import.
///
/// \param error The occured error
///

///
/// \fn QXmppAccountMigrationManager::registerExportData(ImportFunc importFunc, ExportFunc exportFunc)
///
Expand Down
2 changes: 2 additions & 0 deletions src/client/QXmppAccountMigrationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ class QXMPP_EXPORT QXmppAccountMigrationManager : public QXmppClientExtension
template<typename DataType>
void unregisterExportData();

Q_SIGNAL void errorOccurred(const QXmppError &error);

private:
void registerMigrationDataInternal(std::type_index dataType, std::function<QXmppTask<Result<>>(std::any)>, std::function<QXmppTask<Result<std::any>>()>);
void unregisterMigrationDataInternal(std::type_index dataType);
Expand Down
161 changes: 161 additions & 0 deletions src/client/QXmppMixManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

#include "QXmppMixManager.h"

#include "QXmppAccountMigrationManager.h"
#include "QXmppClient.h"
#include "QXmppConstants_p.h"
#include "QXmppDiscoveryIq.h"
#include "QXmppDiscoveryManager.h"
#include "QXmppGlobal.h"
#include "QXmppMessage.h"
#include "QXmppMixInfoItem.h"
#include "QXmppMixInvitation.h"
Expand All @@ -17,11 +19,14 @@
#include "QXmppPubSubManager.h"
#include "QXmppRosterManager.h"
#include "QXmppUtils.h"
#include "QXmppUtils_p.h"

#include "Algorithms.h"
#include "StringLiterals.h"

#include <QDomElement>

using namespace QXmpp;
using namespace QXmpp::Private;

class QXmppMixManagerPrivate
Expand All @@ -34,6 +39,64 @@ class QXmppMixManagerPrivate
QList<QXmppMixManager::Service> services;
};

namespace QXmpp::Private {

struct MixData {
struct Item {
QString jid;
QString nick;

void parse(const QDomElement &element) {
jid = element.attribute(u"jid"_s);
nick = element.attribute(u"nick"_s);
}

void toXml(QXmlStreamWriter *writer) const {
writer->writeStartElement(QSL65("item"));
writeOptionalXmlAttribute(writer, u"jid", jid);
writeOptionalXmlAttribute(writer, u"nick", nick);
writer->writeEndElement();
}
};

using Items = QList<Item>;

Items items;

static std::variant<MixData, QXmppError> fromDom(const QDomElement &el)
{
if (el.tagName() != u"mix" || el.namespaceURI() != ns_qxmpp_export) {
return QXmppError { u"Invalid element."_s, {} };
}

MixData d;

for (const auto &itemEl : iterChildElements(el, u"item")) {
Item item;
item.parse(itemEl);
d.items.push_back(std::move(item));
}

return d;
}

void toXml(QXmlStreamWriter &writer) const
{
writer.writeStartElement(QSL65("mix"));
for (const auto &item : items) {
item.toXml(&writer);
}
writer.writeEndElement();
}
};

static void serializeMixData(const MixData &d, QXmlStreamWriter &writer)
{
d.toXml(writer);
}

} // namespace QXmpp::Private

///
/// \class QXmppMixManager
///
Expand Down Expand Up @@ -366,6 +429,7 @@ constexpr QStringView MIX_SERVICE_DISCOVERY_NODE = u"mix";
QXmppMixManager::QXmppMixManager()
: d(new QXmppMixManagerPrivate())
{
QXmppExportData::registerExtension<MixData, MixData::fromDom, serializeMixData>(u"mix", ns_qxmpp_export);
}

QXmppMixManager::~QXmppMixManager() = default;
Expand Down Expand Up @@ -988,13 +1052,110 @@ void QXmppMixManager::onRegistered(QXmppClient *client)

d->pubSubManager = client->findExtension<QXmppPubSubManager>();
Q_ASSERT_X(d->pubSubManager, "QXmppMixManager", "QXmppPubSubManager is missing");

// data import/export
if (auto manager = client->findExtension<QXmppAccountMigrationManager>()) {
auto rosterManager = client->findExtension<QXmppRosterManager>();
Q_ASSERT_X(rosterManager, "QXmppMixManager", "QXmppRosterManager is missing");

using ImportResult = std::variant<Success, QXmppError>;
auto importData = [this, client, manager](const MixData &data) -> QXmppTask<ImportResult> {
if (data.items.isEmpty()) {
return makeReadyTask<ImportResult>(Success());
}

const auto defaultNick = client->configuration().user();
QXmppPromise<ImportResult> promise;
auto counter = std::make_shared<int>(data.items.size());

for (const auto &item : std::as_const(data.items)) {
const auto nick = item.nick.isEmpty() ? defaultNick : item.nick;

joinChannel(item.jid, nick).then(this, [manager, promise, counter](auto &&result) mutable {
if (promise.task().isFinished()) {
return;
}

// We do not break import/export on mix errors, we only notify about it
if (auto error = std::get_if<QXmppError>(&result); error) {
Q_EMIT manager->errorOccurred(std::move(*error));
}

if ((--(*counter)) == 0) {
return promise.finish(Success());
}
});
}

return promise.task();
};

using ExportResult = std::variant<MixData, QXmppError>;
auto exportData = [this, client, manager, rosterManager]() -> QXmppTask<ExportResult> {
QXmppPromise<ExportResult> promise;

rosterManager->requestRoster().then(this, [this, manager, promise](auto &&rosterResult) mutable {
if (auto error = std::get_if<QXmppError>(&rosterResult); error) {
return promise.finish(std::move(*error));
}

const auto iq = std::move(std::get<QXmppRosterIq>(rosterResult));
const auto iqItems = transformFilter<QList<QXmppRosterIq::Item>>(iq.items(), [](const auto &item) -> std::optional<QXmppRosterIq::Item> {
if (item.isMixChannel()) {
return item;
}

return {};
});

auto result = std::make_shared<MixData>();
auto counter = std::make_shared<int>(iqItems.size());

result->items.reserve(*counter);

for (const auto &item : std::as_const(iqItems)) {
requestParticipants(item.bareJid()).then(this, [manager, result, promise, counter, channelId = item.bareJid(), participantId = item.mixParticipantId()](auto &&participantsResult) mutable {
if (promise.task().isFinished()) {
return;
}

// We do not break import/export on mix errors, we only notify about it
if (auto error = std::get_if<QXmppError>(&participantsResult); error) {
Q_EMIT manager->errorOccurred(std::move(*error));
} else {
const auto participants = std::get<QVector<QXmppMixParticipantItem>>(participantsResult);

for (const QXmppMixParticipantItem &participant: participants) {
if (participant.id() == participantId) {
result->items.append({ channelId, participant.nick() });
break;
}
}
}

if ((--(*counter)) == 0) {
return promise.finish(*result.get());
}
});
}
});

return promise.task();
};

manager->registerExportData<MixData>(importData, exportData);
}
}

void QXmppMixManager::onUnregistered(QXmppClient *client)
{
disconnect(d->discoveryManager, &QXmppDiscoveryManager::infoReceived, this, &QXmppMixManager::handleDiscoInfo);
resetCachedData();
disconnect(client, &QXmppClient::connected, this, nullptr);

if (auto manager = client->findExtension<QXmppAccountMigrationManager>()) {
manager->unregisterExportData<MixData>();
}
}

bool QXmppMixManager::handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName)
Expand Down
Loading
Loading