Merge branch '1.5'
This commit is contained in:
commit
55362b2e36
|
@ -49,6 +49,18 @@ jobs:
|
|||
- uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v2
|
||||
- name: Run tests
|
||||
run: |
|
||||
${env:PATH} += ";D:/a/qxmpp/qxmpp/src/Debug"
|
||||
cmake . && cmake --build .
|
||||
# ctest
|
||||
# ctest --rerun-failed --output-on-failure
|
||||
xmllint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
|
@ -1659,7 +1659,7 @@ void QXmppMessage::serializeExtensions(QXmlStreamWriter *writer, QXmpp::SceMode
|
|||
writer->writeStartElement(QStringLiteral("encryption"));
|
||||
writer->writeDefaultNamespace(ns_eme);
|
||||
writer->writeAttribute(QStringLiteral("namespace"), d->encryptionMethod);
|
||||
helperToXmlAddAttribute(writer, QStringLiteral("name"), d->encryptionName);
|
||||
helperToXmlAddAttribute(writer, QStringLiteral("name"), encryptionName());
|
||||
writer->writeEndElement();
|
||||
}
|
||||
|
||||
|
|
|
@ -197,7 +197,7 @@ QXmppTask<QXmpp::SendResult> QXmppStream::send(QXmppPacket &&packet, bool &writt
|
|||
///
|
||||
/// \since QXmpp 1.5
|
||||
///
|
||||
QXmppTask<QXmppStream::IqResult> QXmppStream::sendIq(QXmppIq &&iq)
|
||||
QXmppTask<QXmppStream::IqResult> QXmppStream::sendIq(QXmppIq &&iq, const QString &to)
|
||||
{
|
||||
using namespace QXmpp;
|
||||
|
||||
|
@ -212,7 +212,7 @@ QXmppTask<QXmppStream::IqResult> QXmppStream::sendIq(QXmppIq &&iq)
|
|||
iq.setId(QXmppUtils::generateStanzaUuid());
|
||||
}
|
||||
|
||||
return sendIq(QXmppPacket(iq), iq.id(), iq.to());
|
||||
return sendIq(QXmppPacket(iq), iq.id(), to);
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -466,10 +466,18 @@ bool QXmppStream::handleIqResponse(const QDomElement &stanza)
|
|||
return false;
|
||||
}
|
||||
|
||||
if (auto itr = d->runningIqs.find(stanza.attribute(QStringLiteral("id")));
|
||||
const auto id = stanza.attribute(QStringLiteral("id"));
|
||||
if (auto itr = d->runningIqs.find(id);
|
||||
itr != d->runningIqs.end()) {
|
||||
if (stanza.attribute("from") != itr.value().jid) {
|
||||
warning(QStringLiteral("Received IQ response to one of our requests from wrong sender. Ignoring."));
|
||||
const auto expectedFrom = itr.value().jid;
|
||||
// Check that the sender of the response matches the recipient of the request.
|
||||
// Stanzas coming from the server on behalf of the user's account must have no "from"
|
||||
// attribute or have it set to the user's bare JID.
|
||||
// If 'from' is empty, the IQ has been sent by the server. In this case we don't need to
|
||||
// do the check as we trust the server anyways.
|
||||
if (const auto from = stanza.attribute("from"); !from.isEmpty() && from != expectedFrom) {
|
||||
warning(QStringLiteral("Ignored received IQ response to request '%1' because of wrong sender '%2' instead of expected sender '%3'")
|
||||
.arg(id, from, expectedFrom));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ public:
|
|||
QXmppTask<QXmpp::SendResult> send(QXmppPacket &&);
|
||||
|
||||
using IqResult = std::variant<QDomElement, QXmppError>;
|
||||
QXmppTask<IqResult> sendIq(QXmppIq &&);
|
||||
QXmppTask<IqResult> sendIq(QXmppIq &&, const QString &to);
|
||||
QXmppTask<IqResult> sendIq(QXmppPacket &&, const QString &id, const QString &to);
|
||||
void cancelOngoingIqs();
|
||||
bool hasIqId(const QString &id) const;
|
||||
|
|
|
@ -18,10 +18,9 @@ using namespace QXmpp::Private;
|
|||
class CarbonEnableIq : public QXmppIq
|
||||
{
|
||||
public:
|
||||
CarbonEnableIq(const QString &jid)
|
||||
CarbonEnableIq()
|
||||
: QXmppIq()
|
||||
{
|
||||
setTo(jid);
|
||||
setType(QXmppIq::Set);
|
||||
}
|
||||
|
||||
|
@ -165,7 +164,7 @@ void QXmppCarbonManagerV2::enableCarbons()
|
|||
return;
|
||||
}
|
||||
|
||||
client()->sendIq(CarbonEnableIq(client()->configuration().jidBare())).then(this, [this](QXmppClient::IqResult domResult) {
|
||||
client()->sendIq(CarbonEnableIq()).then(this, [this](QXmppClient::IqResult domResult) {
|
||||
if (auto err = parseIq(std::move(domResult))) {
|
||||
warning("Could not enable message carbons: " % err->description);
|
||||
} else {
|
||||
|
|
|
@ -95,12 +95,10 @@ namespace Private {
|
|||
iq.parse(element);
|
||||
iq.setE2eeMetadata(e2eeMetadata);
|
||||
|
||||
processHandleIqResult(
|
||||
client,
|
||||
iq.id(),
|
||||
iq.from(),
|
||||
e2eeMetadata,
|
||||
invokeIqHandler(std::forward<Handler>(handler), std::move(iq)));
|
||||
auto id = iq.id(), from = iq.from();
|
||||
|
||||
processHandleIqResult(client, id, from, e2eeMetadata,
|
||||
invokeIqHandler(std::forward<Handler>(handler), std::move(iq)));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -9,29 +9,98 @@
|
|||
#include "QXmppConstants_p.h"
|
||||
#include "QXmppDataForm.h"
|
||||
#include "QXmppE2eeExtension.h"
|
||||
#include "QXmppFutureUtils_p.h"
|
||||
#include "QXmppMamIq.h"
|
||||
#include "QXmppMessage.h"
|
||||
#include "QXmppPromise.h"
|
||||
#include "QXmppUtils.h"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include <QDomElement>
|
||||
|
||||
using namespace QXmpp;
|
||||
using namespace QXmpp::Private;
|
||||
|
||||
template<typename T, typename Converter>
|
||||
auto transform(const T &input, Converter convert)
|
||||
{
|
||||
using Output = std::decay_t<decltype(convert(*input.begin()))>;
|
||||
QVector<Output> output;
|
||||
output.reserve(input.size());
|
||||
std::transform(input.begin(), input.end(), std::back_inserter(output), std::move(convert));
|
||||
return output;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto sum(const T &c)
|
||||
{
|
||||
return std::accumulate(c.begin(), c.end(), 0);
|
||||
}
|
||||
|
||||
struct MamMessage
|
||||
{
|
||||
QDomElement element;
|
||||
std::optional<QDateTime> delay;
|
||||
};
|
||||
|
||||
enum EncryptedType { Unencrypted,
|
||||
Encrypted };
|
||||
|
||||
QXmppMessage parseMamMessage(const MamMessage &mamMessage, EncryptedType encrypted)
|
||||
{
|
||||
QXmppMessage m;
|
||||
m.parse(mamMessage.element, encrypted == Encrypted ? ScePublic : SceAll);
|
||||
if (mamMessage.delay) {
|
||||
m.setStamp(*mamMessage.delay);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
std::optional<std::tuple<MamMessage, QString>> parseMamMessageResult(const QDomElement &messageEl)
|
||||
{
|
||||
auto resultElement = messageEl.firstChildElement("result");
|
||||
if (resultElement.isNull() || resultElement.namespaceURI() != ns_mam) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto forwardedElement = resultElement.firstChildElement("forwarded");
|
||||
if (forwardedElement.isNull() || forwardedElement.namespaceURI() != ns_forwarding) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto queryId = resultElement.attribute("queryid");
|
||||
|
||||
auto messageElement = forwardedElement.firstChildElement("message");
|
||||
if (messageElement.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto parseDelay = [](const auto &forwardedEl) -> std::optional<QDateTime> {
|
||||
auto delayEl = forwardedEl.firstChildElement("delay");
|
||||
if (!delayEl.isNull() && delayEl.namespaceURI() == ns_delayed_delivery) {
|
||||
return QXmppUtils::datetimeFromString(delayEl.attribute("stamp"));
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
return { { MamMessage { messageElement, parseDelay(forwardedElement) }, queryId } };
|
||||
}
|
||||
|
||||
struct RetrieveRequestState
|
||||
{
|
||||
QXmppPromise<QXmppMamManager::RetrieveResult> promise;
|
||||
QXmppMamResultIq iq;
|
||||
QVector<QXmppMessage> messages;
|
||||
QVector<MamMessage> messages;
|
||||
QVector<QXmppMessage> processedMessages;
|
||||
uint runningDecryptionJobs = 0;
|
||||
|
||||
void finish()
|
||||
{
|
||||
Q_ASSERT(messages.count() == processedMessages.count());
|
||||
promise.finish(
|
||||
QXmppMamManager::RetrievedMessages {
|
||||
std::move(iq),
|
||||
std::move(messages) });
|
||||
std::move(processedMessages) });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -87,28 +156,8 @@ QStringList QXmppMamManager::discoveryFeatures() const
|
|||
bool QXmppMamManager::handleStanza(const QDomElement &element)
|
||||
{
|
||||
if (element.tagName() == "message") {
|
||||
QDomElement resultElement = element.firstChildElement("result");
|
||||
if (!resultElement.isNull() && resultElement.namespaceURI() == ns_mam) {
|
||||
QDomElement forwardedElement = resultElement.firstChildElement("forwarded");
|
||||
QString queryId = resultElement.attribute("queryid");
|
||||
|
||||
if (forwardedElement.isNull() || forwardedElement.namespaceURI() != ns_forwarding) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto messageElement = forwardedElement.firstChildElement("message");
|
||||
auto delayElement = forwardedElement.firstChildElement("delay");
|
||||
|
||||
if (messageElement.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QXmppMessage message;
|
||||
message.parse(messageElement);
|
||||
if (!delayElement.isNull() && delayElement.namespaceURI() == ns_delayed_delivery) {
|
||||
const QString stamp = delayElement.attribute("stamp");
|
||||
message.setStamp(QXmppUtils::datetimeFromString(stamp));
|
||||
}
|
||||
if (auto result = parseMamMessageResult(element)) {
|
||||
auto &[message, queryId] = *result;
|
||||
|
||||
auto itr = d->ongoingRequests.find(queryId.toStdString());
|
||||
if (itr != d->ongoingRequests.end()) {
|
||||
|
@ -116,7 +165,7 @@ bool QXmppMamManager::handleStanza(const QDomElement &element)
|
|||
itr->second.messages.append(std::move(message));
|
||||
} else {
|
||||
// signal-based API
|
||||
Q_EMIT archivedMessageReceived(queryId, message);
|
||||
Q_EMIT archivedMessageReceived(queryId, parseMamMessage(message, Unencrypted));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -240,74 +289,97 @@ QString QXmppMamManager::retrieveArchivedMessages(const QString &to,
|
|||
QXmppTask<QXmppMamManager::RetrieveResult> QXmppMamManager::retrieveMessages(const QString &to, const QString &node, const QString &jid, const QDateTime &start, const QDateTime &end, const QXmppResultSetQuery &resultSetQuery)
|
||||
{
|
||||
auto queryIq = buildRequest(to, node, jid, start, end, resultSetQuery);
|
||||
auto queryId = queryIq.queryId();
|
||||
|
||||
auto [itr, _] = d->ongoingRequests.insert({ queryIq.queryId().toStdString(), RetrieveRequestState() });
|
||||
auto [itr, inserted] = d->ongoingRequests.insert({ queryIq.queryId().toStdString(), RetrieveRequestState() });
|
||||
Q_ASSERT(inserted);
|
||||
|
||||
// create task here; promise could finish immediately after client()->sendIq()
|
||||
auto task = itr->second.promise.task();
|
||||
|
||||
// retrieve messages
|
||||
client()->sendIq(std::move(queryIq)).then(this, [this, queryId = queryIq.queryId()](QXmppClient::IqResult result) {
|
||||
client()->sendIq(std::move(queryIq)).then(this, [this, queryId](QXmppClient::IqResult result) {
|
||||
auto itr = d->ongoingRequests.find(queryId.toStdString());
|
||||
if (itr == d->ongoingRequests.end()) {
|
||||
return;
|
||||
}
|
||||
auto &state = itr->second;
|
||||
|
||||
if (std::holds_alternative<QDomElement>(result)) {
|
||||
auto &iq = itr->second.iq;
|
||||
iq.parse(std::get<QDomElement>(result));
|
||||
// handle IQ sending errors
|
||||
if (std::holds_alternative<QXmppError>(result)) {
|
||||
state.promise.finish(std::get<QXmppError>(result));
|
||||
d->ongoingRequests.erase(itr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (iq.type() == QXmppIq::Error) {
|
||||
itr->second.promise.finish(QXmppError { iq.error().text(), iq.error() });
|
||||
d->ongoingRequests.erase(itr);
|
||||
// parse IQ
|
||||
auto &iq = state.iq;
|
||||
iq.parse(std::get<QDomElement>(result));
|
||||
|
||||
// handle MAM error result IQ
|
||||
if (iq.type() == QXmppIq::Error) {
|
||||
state.promise.finish(QXmppError { iq.error().text(), iq.error() });
|
||||
d->ongoingRequests.erase(itr);
|
||||
return;
|
||||
}
|
||||
|
||||
// decrypt encrypted messages
|
||||
if (auto *e2eeExt = client()->encryptionExtension()) {
|
||||
// initialize processed messages (we need random access because
|
||||
// decryptMessage() may finish in random order)
|
||||
state.processedMessages.resize(state.messages.size());
|
||||
|
||||
// check for encrypted messages (once)
|
||||
auto messagesEncrypted = transform(state.messages, [&](const auto &m) {
|
||||
return e2eeExt->isEncrypted(m.element);
|
||||
});
|
||||
auto encryptedCount = sum(messagesEncrypted);
|
||||
|
||||
// We can't do this on the fly (with ++ and --) in the for loop
|
||||
// because some decryptMessage() jobs could finish instantly
|
||||
state.runningDecryptionJobs = encryptedCount;
|
||||
|
||||
for (auto i = 0; i < state.messages.size(); i++) {
|
||||
if (!messagesEncrypted[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
e2eeExt->decryptMessage(parseMamMessage(state.messages.at(i), Encrypted)).then(this, [this, i, queryId](auto result) {
|
||||
auto itr = d->ongoingRequests.find(queryId.toStdString());
|
||||
Q_ASSERT(itr != d->ongoingRequests.end());
|
||||
|
||||
auto &state = itr->second;
|
||||
|
||||
// store decrypted message, fallback to encrypted message
|
||||
if (std::holds_alternative<QXmppMessage>(result)) {
|
||||
state.processedMessages[i] = std::get<QXmppMessage>(std::move(result));
|
||||
} else {
|
||||
warning(QStringLiteral("Error decrypting message."));
|
||||
state.processedMessages[i] = parseMamMessage(state.messages[i], Unencrypted);
|
||||
}
|
||||
|
||||
// finish promise if this was the last job
|
||||
state.runningDecryptionJobs--;
|
||||
if (state.runningDecryptionJobs == 0) {
|
||||
state.finish();
|
||||
d->ongoingRequests.erase(itr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// finishing the promise is done after decryptMessage()
|
||||
if (encryptedCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// decrypt encrypted messages
|
||||
if (auto *e2eeExt = client()->encryptionExtension()) {
|
||||
auto &messages = itr->second.messages;
|
||||
auto running = std::make_shared<uint>(0);
|
||||
// handle case when no message is encrypted
|
||||
auto hasEncryptedMessages = false;
|
||||
|
||||
for (auto i = 0; i < messages.size(); i++) {
|
||||
if (!e2eeExt->isEncrypted(messages.at(i))) {
|
||||
continue;
|
||||
}
|
||||
hasEncryptedMessages = true;
|
||||
|
||||
auto message = messages.at(i);
|
||||
(*running)++;
|
||||
e2eeExt->decryptMessage(std::move(message)).then(this, [this, i, running, queryId](auto result) {
|
||||
(*running)--;
|
||||
auto itr = d->ongoingRequests.find(queryId.toStdString());
|
||||
if (itr == d->ongoingRequests.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::holds_alternative<QXmppMessage>(result)) {
|
||||
itr->second.messages[i] = std::get<QXmppMessage>(std::move(result));
|
||||
} else {
|
||||
warning(QStringLiteral("Error decrypting message."));
|
||||
}
|
||||
if (*running == 0) {
|
||||
itr->second.finish();
|
||||
d->ongoingRequests.erase(itr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasEncryptedMessages) {
|
||||
// finish here, no decryptMessage callback will do it
|
||||
itr->second.finish();
|
||||
d->ongoingRequests.erase(itr);
|
||||
}
|
||||
} else {
|
||||
itr->second.finish();
|
||||
d->ongoingRequests.erase(itr);
|
||||
}
|
||||
} else {
|
||||
itr->second.promise.finish(std::get<QXmppError>(result));
|
||||
d->ongoingRequests.erase(itr);
|
||||
}
|
||||
|
||||
// for the case without decryption, finish here
|
||||
state.processedMessages = transform(state.messages, [](const auto &m) {
|
||||
return parseMamMessage(m, Unencrypted);
|
||||
});
|
||||
state.finish();
|
||||
d->ongoingRequests.erase(itr);
|
||||
});
|
||||
|
||||
return itr->second.promise.task();
|
||||
return task;
|
||||
}
|
||||
|
|
|
@ -324,11 +324,9 @@ bool QXmppOutgoingClient::isStreamResumed() const
|
|||
///
|
||||
QXmppTask<QXmppStream::IqResult> QXmppOutgoingClient::sendIq(QXmppIq &&iq)
|
||||
{
|
||||
// always set a to address (the QXmppStream needs this for matching)
|
||||
if (iq.to().isEmpty()) {
|
||||
iq.setTo(d->config.domain());
|
||||
}
|
||||
return QXmppStream::sendIq(std::move(iq));
|
||||
// If 'to' is empty the user's bare JID is meant implicitly (see RFC6120, section 10.3.3.).
|
||||
auto to = iq.to();
|
||||
return QXmppStream::sendIq(std::move(iq), to.isEmpty() ? d->config.jidBare() : to);
|
||||
}
|
||||
|
||||
void QXmppOutgoingClient::_q_socketDisconnected()
|
||||
|
|
|
@ -340,7 +340,7 @@ QXmppOmemoManager::~QXmppOmemoManager() = default;
|
|||
///
|
||||
/// This should be called after starting the client and before the login.
|
||||
/// It must only be called after \c setUp() has been called once for the user
|
||||
/// during one of the past login session.
|
||||
/// during one of the past login sessions.
|
||||
/// It does not need to be called if setUp() has been called during the current
|
||||
/// login session.
|
||||
///
|
||||
|
@ -1267,29 +1267,33 @@ bool Manager::handlePubSubEvent(const QDomElement &element, const QString &pubSu
|
|||
switch (event.eventType()) {
|
||||
// Items have been published.
|
||||
case QXmppPubSubEventBase::Items: {
|
||||
const auto items = event.items();
|
||||
|
||||
// Only process items if the event notification contains one.
|
||||
// That is necessary because PubSub allows publishing without
|
||||
// items leading to notification-only events.
|
||||
if (!items.isEmpty()) {
|
||||
const auto &deviceListItem = items.constFirst();
|
||||
if (deviceListItem.id() == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) {
|
||||
d->updateDevices(pubSubService, event.items().constFirst());
|
||||
// That is necessary because PubSub allows publishing without items leading to
|
||||
// notification-only events.
|
||||
if (const auto &items = event.items(); !items.isEmpty()) {
|
||||
// Since the usage of the item ID \c QXmppPubSubManager::Current is only RECOMMENDED
|
||||
// by \xep{0060, Publish-Subscribe} (PubSub) but not obligatory, an appropriate
|
||||
// contact device list is determined.
|
||||
// In case of the own device list node, it is sctrictly processed as a recommended
|
||||
// singleton item and changed to fit that if needed.
|
||||
const auto isOwnDeviceListNode = d->ownBareJid() == pubSubService;
|
||||
if (isOwnDeviceListNode) {
|
||||
const auto &deviceListItem = items.constFirst();
|
||||
if (deviceListItem.id() == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) {
|
||||
d->updateDevices(pubSubService, event.items().constFirst());
|
||||
} else {
|
||||
d->handleIrregularDeviceListChanges(pubSubService);
|
||||
}
|
||||
} else {
|
||||
d->handleIrregularDeviceListChanges(pubSubService);
|
||||
d->updateContactDevices(pubSubService, items);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// Items have been retracted.
|
||||
// Specific items are deleted.
|
||||
case QXmppPubSubEventBase::Retract: {
|
||||
// Specific items are deleted.
|
||||
const auto &retractedItem = event.retractIds().constFirst();
|
||||
if (retractedItem == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) {
|
||||
d->handleIrregularDeviceListChanges(pubSubService);
|
||||
}
|
||||
d->handleIrregularDeviceListChanges(pubSubService);
|
||||
}
|
||||
// All items are deleted.
|
||||
case QXmppPubSubEventBase::Purge:
|
||||
|
|
|
@ -920,7 +920,7 @@ bool ManagerPrivate::updatePreKeyPairs(uint32_t count)
|
|||
deviceBundle.addPublicPreKey(preKeyId, serializedPublicPreKey);
|
||||
}
|
||||
|
||||
this->preKeyPairs.insert(serializedPreKeyPairs);
|
||||
preKeyPairs.insert(serializedPreKeyPairs);
|
||||
omemoStorage->addPreKeyPairs(serializedPreKeyPairs);
|
||||
ownDevice.latestPreKeyId = latestPreKeyId - 1 + count;
|
||||
|
||||
|
@ -2647,6 +2647,36 @@ void ManagerPrivate::updateOwnDevicesLocally(bool isDeviceListNodeExistent, Func
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Updates all locally stored devices of a contact.
|
||||
//
|
||||
// \param deviceOwnerJid bare JID of the devices' owner
|
||||
// \param deviceListItems PEP items that may contain a device list
|
||||
//
|
||||
// \returns a found device list item
|
||||
//
|
||||
std::optional<QXmppOmemoDeviceListItem> QXmppOmemoManagerPrivate::updateContactDevices(const QString &deviceOwnerJid, const QVector<QXmppOmemoDeviceListItem> &deviceListItems)
|
||||
{
|
||||
if (deviceListItems.size() > 1) {
|
||||
const auto itr = std::find_if(deviceListItems.cbegin(), deviceListItems.cend(), [=](const QXmppOmemoDeviceListItem &item) {
|
||||
return item.id() == QXmppPubSubManager::Current;
|
||||
});
|
||||
|
||||
if (itr != deviceListItems.cend()) {
|
||||
updateDevices(deviceOwnerJid, *itr);
|
||||
return *itr;
|
||||
} else {
|
||||
warning("Device list for JID '" % deviceOwnerJid % "' could not be updated because the node contains more than one item but none with the singleton node's specific ID '" % QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current) % "'");
|
||||
handleIrregularDeviceListChanges(deviceOwnerJid);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const auto &item = deviceListItems.constFirst();
|
||||
updateDevices(deviceOwnerJid, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
//
|
||||
// Updates all locally stored devices by a passed device list item.
|
||||
//
|
||||
|
@ -2783,7 +2813,7 @@ void ManagerPrivate::updateDevices(const QString &deviceOwnerJid, const QXmppOme
|
|||
// Publish an own correct device list if the PEP service's one is incorrect
|
||||
// and the devices are already set up locally.
|
||||
if (isOwnDeviceListIncorrect) {
|
||||
if (!this->devices.isEmpty()) {
|
||||
if (!devices.isEmpty()) {
|
||||
publishDeviceListItem(true, [=](bool isPublished) {
|
||||
if (!isPublished) {
|
||||
warning("Own device list item could not be published in order to correct the PEP service's one");
|
||||
|
@ -2795,7 +2825,7 @@ void ManagerPrivate::updateDevices(const QString &deviceOwnerJid, const QXmppOme
|
|||
|
||||
//
|
||||
// Corrects the own device list on the PEP service by the locally stored
|
||||
// devices or set a contact device to be removed locally in the future.
|
||||
// devices or sets a contact device to be removed locally in the future.
|
||||
//
|
||||
// \param deviceOwnerJid bare JID of the devices' owner
|
||||
//
|
||||
|
@ -2810,7 +2840,7 @@ void ManagerPrivate::handleIrregularDeviceListChanges(const QString &deviceOwner
|
|||
auto future = pubSubManager->deleteOwnPepNode(ns_omemo_2_devices);
|
||||
future.then(q, [=](QXmppPubSubManager::Result result) {
|
||||
if (const auto error = std::get_if<QXmppError>(&result)) {
|
||||
warning("Node '" % QString(ns_omemo_2_devices) % "' of JID '" % deviceOwnerJid %
|
||||
warning("Node '" % QString(ns_omemo_2_devices) % "' of JID '" % deviceOwnerJid %
|
||||
"' could not be deleted in order to recover from an inconsistent node: " %
|
||||
errorToString(*error));
|
||||
} else {
|
||||
|
@ -2845,7 +2875,7 @@ void ManagerPrivate::handleIrregularDeviceListChanges(const QString &deviceOwner
|
|||
}
|
||||
});
|
||||
} else {
|
||||
auto &ownerDevices = this->devices[deviceOwnerJid];
|
||||
auto &ownerDevices = devices[deviceOwnerJid];
|
||||
|
||||
// Set a timestamp for locally stored contact devices being removed
|
||||
// later if their device list item is removed, if their device list node
|
||||
|
@ -3055,16 +3085,27 @@ QXmppTask<bool> ManagerPrivate::changeDeviceLabel(const QString &deviceLabel)
|
|||
//
|
||||
QXmppTask<QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem>> ManagerPrivate::requestDeviceList(const QString &jid)
|
||||
{
|
||||
auto future = pubSubManager->requestItem<QXmppOmemoDeviceListItem>(jid, ns_omemo_2_devices, QXmppPubSubManager::Current);
|
||||
future.then(q, [this, jid](QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem> result) mutable {
|
||||
QXmppPromise<QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem>> interface;
|
||||
|
||||
// Since the usage of the item ID \c QXmppPubSubManager::Current is only RECOMMENDED by
|
||||
// \xep{0060, Publish-Subscribe} (PubSub) but not obligatory, all items are requested even if
|
||||
// the node should contain only one item.
|
||||
auto future = pubSubManager->requestItems<QXmppOmemoDeviceListItem>(jid, ns_omemo_2_devices);
|
||||
future.then(q, [this, interface, jid](QXmppPubSubManager::ItemsResult<QXmppOmemoDeviceListItem> result) mutable {
|
||||
if (const auto error = std::get_if<QXmppError>(&result)) {
|
||||
warning("Device list for JID '" % jid % "' could not be retrieved: " % errorToString(*error));
|
||||
interface.finish(*error);
|
||||
} else if (const auto &items = std::get<QXmppPubSubManager::Items<QXmppOmemoDeviceListItem>>(result).items; items.isEmpty()) {
|
||||
const auto errorMessage = "Device list for JID '" % jid % "' could not be retrieved because the node does not contain any item";
|
||||
warning(errorMessage);
|
||||
interface.finish(QXmppError { errorMessage });
|
||||
} else if (const auto item = updateContactDevices(jid, items); item) {
|
||||
interface.finish(*item);
|
||||
} else {
|
||||
const auto &item = std::get<QXmppOmemoDeviceListItem>(result);
|
||||
updateDevices(jid, item);
|
||||
interface.finish(QXmppError { "Device list for JID '" % jid % "' could not be retrieved because the node does not contain an appropriate item" });
|
||||
}
|
||||
});
|
||||
return future;
|
||||
return interface.task();
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -290,6 +290,7 @@ public:
|
|||
QXmppOmemoDeviceListItem deviceListItem(bool addOwnDevice = true);
|
||||
template<typename Function>
|
||||
void updateOwnDevicesLocally(bool isDeviceListNodeExistent, Function continuation);
|
||||
std::optional<QXmppOmemoDeviceListItem> updateContactDevices(const QString &deviceOwnerJid, const QVector<QXmppOmemoDeviceListItem> &deviceListItems);
|
||||
void updateDevices(const QString &deviceOwnerJid, const QXmppOmemoDeviceListItem &deviceListItem);
|
||||
void handleIrregularDeviceListChanges(const QString &deviceOwnerJid);
|
||||
template<typename Function>
|
||||
|
|
|
@ -35,6 +35,7 @@ add_simple_test(qxmppdataform)
|
|||
add_simple_test(qxmppdiscoveryiq)
|
||||
add_simple_test(qxmppdiscoverymanager TestClient.h)
|
||||
add_simple_test(qxmppentitytimeiq)
|
||||
add_simple_test(qxmppentitytimemanager TestClient.h)
|
||||
add_simple_test(qxmppexternalservicediscoveryiq)
|
||||
add_simple_test(qxmppexternalservicediscoverymanager TestClient.h)
|
||||
add_simple_test(qxmpphttpuploadiq)
|
||||
|
@ -79,6 +80,7 @@ add_simple_test(qxmppusertunemanager TestClient.h)
|
|||
add_simple_test(qxmppvcardiq)
|
||||
add_simple_test(qxmppvcardmanager)
|
||||
add_simple_test(qxmppversioniq)
|
||||
add_simple_test(qxmppversionmanager TestClient.h)
|
||||
|
||||
if(WITH_QCA)
|
||||
add_simple_test(qxmppfileencryption)
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
// SPDX-FileCopyrightText: 2023 Linus Jahn <lnj@kaidan.im>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include "QXmppEntityTimeIq.h"
|
||||
#include "QXmppEntityTimeManager.h"
|
||||
|
||||
#include "TestClient.h"
|
||||
|
||||
Q_DECLARE_METATYPE(QXmppEntityTimeIq)
|
||||
|
||||
class tst_QXmppEntityTimeManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_SLOT void initTestCase();
|
||||
Q_SLOT void testSendRequest();
|
||||
Q_SLOT void testHandleRequest();
|
||||
};
|
||||
|
||||
void tst_QXmppEntityTimeManager::initTestCase()
|
||||
{
|
||||
qRegisterMetaType<QXmppEntityTimeIq>();
|
||||
}
|
||||
|
||||
void tst_QXmppEntityTimeManager::testSendRequest()
|
||||
{
|
||||
TestClient test;
|
||||
auto *manager = test.addNewExtension<QXmppEntityTimeManager>();
|
||||
|
||||
QSignalSpy spy(manager, &QXmppEntityTimeManager::timeReceived);
|
||||
|
||||
manager->requestTime("juliet@capulet.com/balcony");
|
||||
test.expect("<iq id='qxmpp1' to='juliet@capulet.com/balcony' type='get'><time xmlns='urn:xmpp:time'/></iq>");
|
||||
manager->handleStanza(xmlToDom(R"(<iq id='qxmpp1' to='romeo@montague.net/orchard' from='juliet@capulet.com/balcony' type='result'>
|
||||
<time xmlns='urn:xmpp:time'>
|
||||
<tzo>-06:00</tzo>
|
||||
<utc>2006-12-19T17:58:35Z</utc>
|
||||
</time>
|
||||
</iq>)"));
|
||||
|
||||
QCOMPARE(spy.size(), 1);
|
||||
auto time = spy.at(0).at(0).value<QXmppEntityTimeIq>();
|
||||
QCOMPARE(time.utc(), QDateTime({2006, 12, 19}, {17, 58, 35}, Qt::UTC));
|
||||
QCOMPARE(time.tzo(), -6 * 60 * 60);
|
||||
}
|
||||
|
||||
void tst_QXmppEntityTimeManager::testHandleRequest()
|
||||
{
|
||||
TestClient test;
|
||||
test.configuration().setJid("juliet@capulet.com/balcony");
|
||||
|
||||
auto *manager = test.addNewExtension<QXmppEntityTimeManager>();
|
||||
|
||||
manager->handleStanza(xmlToDom(R"(<iq type='get' from='romeo@montague.net/orchard' to='juliet@capulet.com/balcony' id='time_1'>
|
||||
<time xmlns='urn:xmpp:time'/>
|
||||
</iq>)"));
|
||||
|
||||
auto packet = xmlToDom(test.takePacket());
|
||||
QVERIFY(QXmppEntityTimeIq::isEntityTimeIq(packet));
|
||||
QXmppEntityTimeIq resp;
|
||||
resp.parse(packet);
|
||||
|
||||
QCOMPARE(resp.id(), QStringLiteral("time_1"));
|
||||
QCOMPARE(resp.type(), QXmppIq::Result);
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_QXmppEntityTimeManager)
|
||||
#include "tst_qxmppentitytimemanager.moc"
|
|
@ -783,7 +783,7 @@ void tst_QXmppMessage::testEme()
|
|||
// test standard encryption: OMEMO
|
||||
const QByteArray xmlOmemo(
|
||||
"<message to=\"foo@example.com/QXmpp\" from=\"bar@example.com/QXmpp\" type=\"normal\">"
|
||||
"<encryption xmlns=\"urn:xmpp:eme:0\" namespace=\"eu.siacs.conversations.axolotl\"/>"
|
||||
"<encryption xmlns=\"urn:xmpp:eme:0\" namespace=\"eu.siacs.conversations.axolotl\" name=\"OMEMO\"/>"
|
||||
"<body>This message is encrypted with OMEMO, but your client doesn't seem to support that.</body>"
|
||||
"</message>");
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
// SPDX-FileCopyrightText: 2023 Linus Jahn <lnj@kaidan.im>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include "QXmppVersionManager.h"
|
||||
#include "QXmppVersionIq.h"
|
||||
|
||||
#include "TestClient.h"
|
||||
|
||||
Q_DECLARE_METATYPE(QXmppVersionIq);
|
||||
|
||||
class tst_QXmppVersionManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_SLOT void initTestCase();
|
||||
Q_SLOT void testSendRequest();
|
||||
Q_SLOT void testHandleRequest();
|
||||
};
|
||||
|
||||
void tst_QXmppVersionManager::initTestCase()
|
||||
{
|
||||
qRegisterMetaType<QXmppVersionIq>();
|
||||
}
|
||||
|
||||
void tst_QXmppVersionManager::testSendRequest()
|
||||
{
|
||||
TestClient test;
|
||||
auto *verManager = test.addNewExtension<QXmppVersionManager>();
|
||||
|
||||
QSignalSpy spy(verManager, &QXmppVersionManager::versionReceived);
|
||||
|
||||
auto id = verManager->requestVersion("juliet@capulet.com/balcony");
|
||||
test.expect("<iq id='qxmpp1' to='juliet@capulet.com/balcony' type='get'><query xmlns='jabber:iq:version'/></iq>");
|
||||
verManager->handleStanza(xmlToDom(R"(<iq type='result' from='juliet@capulet.com/balcony' id='qxmpp1'>
|
||||
<query xmlns='jabber:iq:version'>
|
||||
<name>Exodus</name>
|
||||
<version>0.7.0.4</version>
|
||||
<os>Windows-XP 5.01.2600</os>
|
||||
</query>
|
||||
</iq>)"));
|
||||
|
||||
QCOMPARE(spy.size(), 1);
|
||||
auto version = spy.at(0).at(0).value<QXmppVersionIq>();
|
||||
QCOMPARE(version.name(), QStringLiteral("Exodus"));
|
||||
QCOMPARE(version.version(), QStringLiteral("0.7.0.4"));
|
||||
QCOMPARE(version.os(), QStringLiteral("Windows-XP 5.01.2600"));
|
||||
}
|
||||
|
||||
void tst_QXmppVersionManager::testHandleRequest()
|
||||
{
|
||||
TestClient test;
|
||||
test.configuration().setJid("juliet@capulet.com/balcony");
|
||||
|
||||
auto *verManager = test.addNewExtension<QXmppVersionManager>();
|
||||
verManager->setClientName("Exodus");
|
||||
verManager->setClientVersion("0.7.0.4");
|
||||
verManager->setClientOs("Windows-XP 5.01.2600");
|
||||
|
||||
verManager->handleStanza(xmlToDom(R"(<iq type='get' from='romeo@montague.net/orchard' to='juliet@capulet.com/balcony' id='version_1'>
|
||||
<query xmlns='jabber:iq:version'/>
|
||||
</iq>)"));
|
||||
test.expect(R"(<iq id='version_1' to='romeo@montague.net/orchard' type='result'>)"
|
||||
"<query xmlns='jabber:iq:version'><name>Exodus</name><os>Windows-XP 5.01.2600</os><version>0.7.0.4</version>"
|
||||
"</query></iq>");
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_QXmppVersionManager)
|
||||
#include "tst_qxmppversionmanager.moc"
|
Loading…
Reference in New Issue