<commit_before>/******************************************************************************
 * Copyright (C) 2015 <NAME> <<EMAIL>>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "connection.h"
#include "connectiondata.h"
#include "user.h"
#include "events/event.h"
#include "events/directchatevent.h"
#include "room.h"
#include "settings.h"
#include "converters.h"
#include "csapi/login.h"
#include "csapi/logout.h"
#include "csapi/receipts.h"
#include "csapi/leaving.h"
#include "csapi/account-data.h"
#include "csapi/joining.h"
#include "csapi/to_device.h"
#include "jobs/sendeventjob.h"
#include "jobs/syncjob.h"
#include "jobs/mediathumbnailjob.h"
#include "jobs/downloadfilejob.h"

#include <QtNetwork/QDnsLookup>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QStandardPaths>
#include <QtCore/QStringBuilder>
#include <QtCore/QElapsedTimer>
#include <QtCore/QRegularExpression>
#include <QtCore/QCoreApplication>

using namespace QMatrixClient;

// This is very much Qt-specific; STL iterators don't have key() and value()
template <typename HashT, typename Pred>
HashT erase_if(HashT& hashMap, Pred pred)
{
    HashT removals;
    for (auto it = hashMap.begin(); it != hashMap.end();)
    {
        if (pred(it))
        {
            removals.insert(it.key(), it.value());
            it = hashMap.erase(it);
        } else
            ++it;
    }
    return removals;
}

class Connection::Private
{
    public:
        explicit Private(std::unique_ptr<ConnectionData>&& connection)
            : data(move(connection))
        { }
        Q_DISABLE_COPY(Private)
        Private(Private&&) = delete;
        Private operator=(Private&&) = delete;

        Connection* q = nullptr;
        std::unique_ptr<ConnectionData> data;
        // A complex key below is a pair of room name and whether its
        // state is Invited. The spec mandates to keep Invited room state
        // separately so we should, e.g., keep objects for Invite and
        // Leave state of the same room.
        QHash<QPair<QString, bool>, Room*> roomMap;
        QVector<QString> roomIdsToForget;
        QVector<Room*> firstTimeRooms;
        QMap<QString, User*> userMap;
        DirectChatsMap directChats;
        QHash<QString, AccountDataMap> accountData;
        QString userId;

        SyncJob* syncJob = nullptr;

        bool cacheState = true;
        bool cacheToBinary = SettingsGroup("libqmatrixclient")
                             .value("cache_type").toString() != "json";

        void connectWithToken(const QString& user, const QString& accessToken,
                              const QString& deviceId);
        void broadcastDirectChatUpdates(const DirectChatsMap& additions,
                                        const DirectChatsMap& removals);
};

Connection::Connection(const QUrl& server, QObject* parent)
    : QObject(parent)
    , d(std::make_unique<Private>(std::make_unique<ConnectionData>(server)))
{
    d->q = this; // All d initialization should occur before this line
}

Connection::Connection(QObject* parent)
    : Connection({}, parent)
{ }

Connection::~Connection()
{
    qCDebug(MAIN) << "deconstructing connection object for" << d->userId;
    stopSync();
}

void Connection::resolveServer(const QString& mxidOrDomain)
{
    // At this point we may have something as complex as
    // @username:[IPv6:address]:port, or as simple as a plain domain name.

    // Try to parse as an FQID; if there's no @ part, assume it's a domain name.
    QRegularExpression parser(
        "^(@.+?:)?" // Optional username (allow everything for compatibility)
        "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address
        "(:\\d{1,5})?$", // Optional port
        QRegularExpression::UseUnicodePropertiesOption); // Because asian digits
    auto match = parser.match(mxidOrDomain);

    QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2));
    maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http"
    if (!match.hasMatch() || !maybeBaseUrl.isValid())
    {
        emit resolveError(
            tr("%1 is not a valid homeserver address")
                    .arg(maybeBaseUrl.toString()));
        return;
    }

    setHomeserver(maybeBaseUrl);
    emit resolved();
    return;

    // FIXME, #178: The below code is incorrect and is no more executed. The
    // correct server resolution should be done from .well-known/matrix/client
    auto domain = maybeBaseUrl.host();
    qCDebug(MAIN) << "Finding the server" << domain;
    // Check if the Matrix server has a dedicated service record.
    auto* dns = new QDnsLookup();
    dns->setType(QDnsLookup::SRV);
    dns->setName("_matrix._tcp." + domain);

    connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() {
        QUrl baseUrl { maybeBaseUrl };
        if (dns->error() == QDnsLookup::NoError &&
                dns->serviceRecords().isEmpty())
        {
            auto record = dns->serviceRecords().front();
            baseUrl.setHost(record.target());
            baseUrl.setPort(record.port());
            qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host()
                          << "is" << baseUrl.authority();
        } else {
            qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record"
                          << dns->name() << "- using the hostname as is";
        }
        setHomeserver(baseUrl);
        emit resolved();
        dns->deleteLater();
    });
    dns->lookup();
}

void Connection::connectToServer(const QString& user, const QString& password,
                                 const QString& initialDeviceName,
                                 const QString& deviceId)
{
    checkAndConnect(user,
        [=] {
            doConnectToServer(user, password, initialDeviceName, deviceId);
        });
}
void Connection::doConnectToServer(const QString& user, const QString& password,
                                   const QString& initialDeviceName,
                                   const QString& deviceId)
{
    auto loginJob = callApi<LoginJob>(QStringLiteral("m.login.password"),
            user, /*medium*/ "", /*address*/ "", password, /*token*/ "",
            deviceId, initialDeviceName);
    connect(loginJob, &BaseJob::success, this,
        [this, loginJob] {
            d->connectWithToken(loginJob->userId(), loginJob->accessToken(),
                                loginJob->deviceId());
        });
    connect(loginJob, &BaseJob::failure, this,
        [this, loginJob] {
            emit loginError(loginJob->errorString(), loginJob->rawData());
        });
}

void Connection::connectWithToken(const QString& userId,
                                  const QString& accessToken,
                                  const QString& deviceId)
{
    checkAndConnect(userId,
        [=] { d->connectWithToken(userId, accessToken, deviceId); });
}

void Connection::Private::connectWithToken(const QString& user,
                                           const QString& accessToken,
                                           const QString& deviceId)
{
    userId = user;
    data->setToken(accessToken.toLatin1());
    data->setDeviceId(deviceId);
    qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
                  << "by user" << userId << "from device" << deviceId;
    emit q->connected();

}

void Connection::checkAndConnect(const QString& userId,
                                 std::function<void()> connectFn)
{
    if (d->data->baseUrl().isValid())
    {
        connectFn();
        return;
    }
    // Not good to go, try to fix the homeserver URL.
    if (userId.startsWith('@') && userId.indexOf(':') != -1)
    {
        // The below construct makes a single-shot connection that triggers
        // on the signal and then self-disconnects.
        // NB: doResolveServer can emit resolveError, so this is a part of
        // checkAndConnect function contract.
        QMetaObject::Connection connection;
        connection = connect(this, &Connection::homeserverChanged,
                        this, [=] { connectFn(); disconnect(connection); });
        resolveServer(userId);
    } else
        emit resolveError(
            tr("%1 is an invalid homeserver URL")
                .arg(d->data->baseUrl().toString()));
}

void Connection::logout()
{
    auto job = callApi<LogoutJob>();
    connect( job, &LogoutJob::success, this, [this] {
        stopSync();
        emit loggedOut();
    });
}

void Connection::sync(int timeout)
{
    if (d->syncJob)
        return;

    // Raw string: http://en.cppreference.com/w/cpp/language/string_literal
    const QString filter { R"({"room": { "timeline": { "limit": 100 } } })" };
    auto job = d->syncJob = callApi<SyncJob>(BackgroundRequest,
                                         d->data->lastEvent(), filter, timeout);
    connect( job, &SyncJob::success, this, [this, job] {
        onSyncSuccess(job->takeData());
        d->syncJob = nullptr;
        emit syncDone();
    });
    connect( job, &SyncJob::retryScheduled, this,
        [this,job] (int retriesTaken, int nextInMilliseconds)
        {
            emit networkError(job->errorString(), job->rawData(),
                              retriesTaken, nextInMilliseconds);
        });
    connect( job, &SyncJob::failure, this, [this, job] {
        d->syncJob = nullptr;
        if (job->error() == BaseJob::ContentAccessError)
        {
            qCWarning(SYNCJOB)
                << "Sync job failed with ContentAccessError - login expired?";
            emit loginError(job->errorString(), job->rawData());
        }
        else
            emit syncError(job->errorString(), job->rawData());
    });
}

void Connection::onSyncSuccess(SyncData &&data) {
    d->data->setLastEvent(data.nextBatch());
    for (auto&& roomData: data.takeRoomData())
    {
        const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId);
        if (forgetIdx != -1)
        {
            d->roomIdsToForget.removeAt(forgetIdx);
            if (roomData.joinState == JoinState::Leave)
            {
                qDebug(MAIN) << "Room" << roomData.roomId
                    << "has been forgotten, ignoring /sync response for it";
                continue;
            }
            qWarning(MAIN) << "Room" << roomData.roomId
                 << "has just been forgotten but /sync returned it in"
                 << toCString(roomData.joinState)
                 << "state - suspiciously fast turnaround";
        }
        if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) )
        {
            r->updateData(std::move(roomData));
            if (d->firstTimeRooms.removeOne(r))
                emit loadedRoomState(r);
        }
        QCoreApplication::processEvents();
    }
    for (auto&& accountEvent: data.takeAccountData())
    {
        if (accountEvent->type() == EventType::DirectChat)
        {
            const auto usersToDCs = ptrCast<DirectChatEvent>(move(accountEvent))
                                        ->usersToDirectChats();
            DirectChatsMap removals =
                erase_if(d->directChats, [&usersToDCs] (auto it) {
                    return !usersToDCs.contains(it.key()->id(), it.value());
                });
            if (MAIN().isDebugEnabled())
                for (auto it = removals.begin(); it != removals.end(); ++it)
                    qCDebug(MAIN) << it.value()
                        << "is no more a direct chat with" << it.key()->id();

            DirectChatsMap additions;
            for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it)
            {
                const auto* u = user(it.key());
                if (!d->directChats.contains(u, it.value()))
                {
                    additions.insert(u, it.value());
                    d->directChats.insert(u, it.value());
                    qCDebug(MAIN) << "Marked room" << it.value()
                                  << "as a direct chat with" << u->id();
                }
            }
            if (!additions.isEmpty() || !removals.isEmpty())
                emit directChatsListChanged(additions, removals);

            continue;
        }
        d->accountData[accountEvent->jsonType()] =
                fromJson<AccountDataMap>(accountEvent->contentJson());
        emit accountDataChanged(accountEvent->jsonType());
    }
}

void Connection::stopSync()
{
    if (d->syncJob)
    {
        d->syncJob->abandon();
        d->syncJob = nullptr;
    }
}

void Connection::postMessage(Room* room, const QString& type, const QString& message) const
{
    callApi<SendEventJob>(room->id(), type, message);
}

PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const
{
    return callApi<PostReceiptJob>(room->id(), "m.read", event->id());
}

JoinRoomJob* Connection::joinRoom(const QString& roomAlias)
{
    auto job = callApi<JoinRoomJob>(roomAlias);
    connect(job, &JoinRoomJob::success,
            this, [this, job] { provideRoom(job->roomId(), JoinState::Join); });
    return job;
}

void Connection::leaveRoom(Room* room)
{
    callApi<LeaveRoomJob>(room->id());
}

inline auto splitMediaId(const QString& mediaId)
{
    auto idParts = mediaId.split('/');
    Q_ASSERT_X(idParts.size() == 2, __FUNCTION__,
               ("'" + mediaId +
                "' doesn't look like 'serverName/localMediaId'").toLatin1());
    return idParts;
}

MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId,
    QSize requestedSize, RunningPolicy policy) const
{
    auto idParts = splitMediaId(mediaId);
    return callApi<MediaThumbnailJob>(policy,
        idParts.front(), idParts.back(), requestedSize);
}

MediaThumbnailJob* Connection::getThumbnail(const QUrl& url,
    QSize requestedSize, RunningPolicy policy) const
{
    return getThumbnail(url.authority() + url.path(), requestedSize, policy);
}

MediaThumbnailJob* Connection::getThumbnail(const QUrl& url,
    int requestedWidth, int requestedHeight, RunningPolicy policy) const
{
    return getThumbnail(url, QSize(requestedWidth, requestedHeight), policy);
}

UploadContentJob* Connection::uploadContent(QIODevice* contentSource,
        const QString& filename, const QString& contentType) const
{
    return callApi<UploadContentJob>(contentSource, filename, contentType);
}

UploadContentJob* Connection::uploadFile(const QString& fileName,
                                         const QString& contentType)
{
    auto sourceFile = new QFile(fileName);
    if (sourceFile->open(QIODevice::ReadOnly))
    {
        qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName()
                        << "for reading";
        return nullptr;
    }
    return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(),
                         contentType);
}

GetContentJob* Connection::getContent(const QString& mediaId) const
{
    auto idParts = splitMediaId(mediaId);
    return callApi<GetContentJob>(idParts.front(), idParts.back());
}

GetContentJob* Connection::getContent(const QUrl& url) const
{
    return getContent(url.authority() + url.path());
}

DownloadFileJob* Connection::downloadFile(const QUrl& url,
                                          const QString& localFilename) const
{
    auto mediaId = url.authority() + url.path();
    auto idParts = splitMediaId(mediaId);
    auto* job = callApi<DownloadFileJob>(idParts.front(), idParts.back(),
                                         localFilename);
    return job;
}

CreateRoomJob* Connection::createRoom(RoomVisibility visibility,
    const QString& alias, const QString& name, const QString& topic,
    const QStringList& invites, const QString& presetName,
    bool isDirect, bool guestsCanJoin,
    const QVector<CreateRoomJob::StateEvent>& initialState,
    const QVector<CreateRoomJob::Invite3pid>& invite3pids,
    const QJsonObject& creationContent)
{
    // FIXME: switch to using QStringList instead of const QStringList& in 0.4
    auto filteredInvites = invites;
    filteredInvites.removeOne(d->userId); // The creator is by definition in the room
    auto job = callApi<CreateRoomJob>(
            visibility == PublishRoom ? "public" : "private", alias, name,
            topic, filteredInvites, invite3pids, creationContent, initialState,
            presetName, isDirect, guestsCanJoin);
    connect(job, &BaseJob::success, this, [this,job] {
        emit createdRoom(provideRoom(job->roomId(), JoinState::Join));
    });
    return job;
}

void Connection::requestDirectChat(const QString& userId)
{
    doInDirectChat(userId, [this] (Room* r) { emit directChatAvailable(r); });
}

void Connection::doInDirectChat(const QString& userId,
                                std::function<void (Room*)> operation)
{
    // There can be more than one DC; find the first valid, and delete invalid
    // (left/forgotten) ones along the way.
    const auto* u = user(userId);
    DirectChatsMap removals;
    for (auto it = d->directChats.find(u);
         it != d->directChats.end() && it.key() == u; ++it)
    {
        const auto& roomId = *it;
        if (auto r = room(roomId, JoinState::Join))
        {
            Q_ASSERT(r->id() == roomId);
            qCDebug(MAIN) << "Requested direct chat with" << userId
                          << "is already available as" << r->id();
            operation(r);
            return;
        }
        if (auto ir = invitation(roomId))<commit_msg>Connection::doInDirectChat: "direct chat with self" feature<commit_after>/******************************************************************************
 * Copyright (C) 2015 <NAME> <<EMAIL>>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "connection.h"
#include "connectiondata.h"
#include "user.h"
#include "events/event.h"
#include "events/directchatevent.h"
#include "room.h"
#include "settings.h"
#include "converters.h"
#include "csapi/login.h"
#include "csapi/logout.h"
#include "csapi/receipts.h"
#include "csapi/leaving.h"
#include "csapi/account-data.h"
#include "csapi/joining.h"
#include "csapi/to_device.h"
#include "jobs/sendeventjob.h"
#include "jobs/syncjob.h"
#include "jobs/mediathumbnailjob.h"
#include "jobs/downloadfilejob.h"

#include <QtNetwork/QDnsLookup>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QStandardPaths>
#include <QtCore/QStringBuilder>
#include <QtCore/QElapsedTimer>
#include <QtCore/QRegularExpression>
#include <QtCore/QCoreApplication>

using namespace QMatrixClient;

// This is very much Qt-specific; STL iterators don't have key() and value()
template <typename HashT, typename Pred>
HashT erase_if(HashT& hashMap, Pred pred)
{
    HashT removals;
    for (auto it = hashMap.begin(); it != hashMap.end();)
    {
        if (pred(it))
        {
            removals.insert(it.key(), it.value());
            it = hashMap.erase(it);
        } else
            ++it;
    }
    return removals;
}

class Connection::Private
{
    public:
        explicit Private(std::unique_ptr<ConnectionData>&& connection)
            : data(move(connection))
        { }
        Q_DISABLE_COPY(Private)
        Private(Private&&) = delete;
        Private operator=(Private&&) = delete;

        Connection* q = nullptr;
        std::unique_ptr<ConnectionData> data;
        // A complex key below is a pair of room name and whether its
        // state is Invited. The spec mandates to keep Invited room state
        // separately so we should, e.g., keep objects for Invite and
        // Leave state of the same room.
        QHash<QPair<QString, bool>, Room*> roomMap;
        QVector<QString> roomIdsToForget;
        QVector<Room*> firstTimeRooms;
        QMap<QString, User*> userMap;
        DirectChatsMap directChats;
        QHash<QString, AccountDataMap> accountData;
        QString userId;

        SyncJob* syncJob = nullptr;

        bool cacheState = true;
        bool cacheToBinary = SettingsGroup("libqmatrixclient")
                             .value("cache_type").toString() != "json";

        void connectWithToken(const QString& user, const QString& accessToken,
                              const QString& deviceId);
        void broadcastDirectChatUpdates(const DirectChatsMap& additions,
                                        const DirectChatsMap& removals);
};

Connection::Connection(const QUrl& server, QObject* parent)
    : QObject(parent)
    , d(std::make_unique<Private>(std::make_unique<ConnectionData>(server)))
{
    d->q = this; // All d initialization should occur before this line
}

Connection::Connection(QObject* parent)
    : Connection({}, parent)
{ }

Connection::~Connection()
{
    qCDebug(MAIN) << "deconstructing connection object for" << d->userId;
    stopSync();
}

void Connection::resolveServer(const QString& mxidOrDomain)
{
    // At this point we may have something as complex as
    // @username:[IPv6:address]:port, or as simple as a plain domain name.

    // Try to parse as an FQID; if there's no @ part, assume it's a domain name.
    QRegularExpression parser(
        "^(@.+?:)?" // Optional username (allow everything for compatibility)
        "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address
        "(:\\d{1,5})?$", // Optional port
        QRegularExpression::UseUnicodePropertiesOption); // Because asian digits
    auto match = parser.match(mxidOrDomain);

    QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2));
    maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http"
    if (!match.hasMatch() || !maybeBaseUrl.isValid())
    {
        emit resolveError(
            tr("%1 is not a valid homeserver address")
                    .arg(maybeBaseUrl.toString()));
        return;
    }

    setHomeserver(maybeBaseUrl);
    emit resolved();
    return;

    // FIXME, #178: The below code is incorrect and is no more executed. The
    // correct server resolution should be done from .well-known/matrix/client
    auto domain = maybeBaseUrl.host();
    qCDebug(MAIN) << "Finding the server" << domain;
    // Check if the Matrix server has a dedicated service record.
    auto* dns = new QDnsLookup();
    dns->setType(QDnsLookup::SRV);
    dns->setName("_matrix._tcp." + domain);

    connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() {
        QUrl baseUrl { maybeBaseUrl };
        if (dns->error() == QDnsLookup::NoError &&
                dns->serviceRecords().isEmpty())
        {
            auto record = dns->serviceRecords().front();
            baseUrl.setHost(record.target());
            baseUrl.setPort(record.port());
            qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host()
                          << "is" << baseUrl.authority();
        } else {
            qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record"
                          << dns->name() << "- using the hostname as is";
        }
        setHomeserver(baseUrl);
        emit resolved();
        dns->deleteLater();
    });
    dns->lookup();
}

void Connection::connectToServer(const QString& user, const QString& password,
                                 const QString& initialDeviceName,
                                 const QString& deviceId)
{
    checkAndConnect(user,
        [=] {
            doConnectToServer(user, password, initialDeviceName, deviceId);
        });
}
void Connection::doConnectToServer(const QString& user, const QString& password,
                                   const QString& initialDeviceName,
                                   const QString& deviceId)
{
    auto loginJob = callApi<LoginJob>(QStringLiteral("m.login.password"),
            user, /*medium*/ "", /*address*/ "", password, /*token*/ "",
            deviceId, initialDeviceName);
    connect(loginJob, &BaseJob::success, this,
        [this, loginJob] {
            d->connectWithToken(loginJob->userId(), loginJob->accessToken(),
                                loginJob->deviceId());
        });
    connect(loginJob, &BaseJob::failure, this,
        [this, loginJob] {
            emit loginError(loginJob->errorString(), loginJob->rawData());
        });
}

void Connection::connectWithToken(const QString& userId,
                                  const QString& accessToken,
                                  const QString& deviceId)
{
    checkAndConnect(userId,
        [=] { d->connectWithToken(userId, accessToken, deviceId); });
}

void Connection::Private::connectWithToken(const QString& user,
                                           const QString& accessToken,
                                           const QString& deviceId)
{
    userId = user;
    data->setToken(accessToken.toLatin1());
    data->setDeviceId(deviceId);
    qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
                  << "by user" << userId << "from device" << deviceId;
    emit q->connected();

}

void Connection::checkAndConnect(const QString& userId,
                                 std::function<void()> connectFn)
{
    if (d->data->baseUrl().isValid())
    {
        connectFn();
        return;
    }
    // Not good to go, try to fix the homeserver URL.
    if (userId.startsWith('@') && userId.indexOf(':') != -1)
    {
        // The below construct makes a single-shot connection that triggers
        // on the signal and then self-disconnects.
        // NB: doResolveServer can emit resolveError, so this is a part of
        // checkAndConnect function contract.
        QMetaObject::Connection connection;
        connection = connect(this, &Connection::homeserverChanged,
                        this, [=] { connectFn(); disconnect(connection); });
        resolveServer(userId);
    } else
        emit resolveError(
            tr("%1 is an invalid homeserver URL")
                .arg(d->data->baseUrl().toString()));
}

void Connection::logout()
{
    auto job = callApi<LogoutJob>();
    connect( job, &LogoutJob::success, this, [this] {
        stopSync();
        emit loggedOut();
    });
}

void Connection::sync(int timeout)
{
    if (d->syncJob)
        return;

    // Raw string: http://en.cppreference.com/w/cpp/language/string_literal
    const QString filter { R"({"room": { "timeline": { "limit": 100 } } })" };
    auto job = d->syncJob = callApi<SyncJob>(BackgroundRequest,
                                         d->data->lastEvent(), filter, timeout);
    connect( job, &SyncJob::success, this, [this, job] {
        onSyncSuccess(job->takeData());
        d->syncJob = nullptr;
        emit syncDone();
    });
    connect( job, &SyncJob::retryScheduled, this,
        [this,job] (int retriesTaken, int nextInMilliseconds)
        {
            emit networkError(job->errorString(), job->rawData(),
                              retriesTaken, nextInMilliseconds);
        });
    connect( job, &SyncJob::failure, this, [this, job] {
        d->syncJob = nullptr;
        if (job->error() == BaseJob::ContentAccessError)
        {
            qCWarning(SYNCJOB)
                << "Sync job failed with ContentAccessError - login expired?";
            emit loginError(job->errorString(), job->rawData());
        }
        else
            emit syncError(job->errorString(), job->rawData());
    });
}

void Connection::onSyncSuccess(SyncData &&data) {
    d->data->setLastEvent(data.nextBatch());
    for (auto&& roomData: data.takeRoomData())
    {
        const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId);
        if (forgetIdx != -1)
        {
            d->roomIdsToForget.removeAt(forgetIdx);
            if (roomData.joinState == JoinState::Leave)
            {
                qDebug(MAIN) << "Room" << roomData.roomId
                    << "has been forgotten, ignoring /sync response for it";
                continue;
            }
            qWarning(MAIN) << "Room" << roomData.roomId
                 << "has just been forgotten but /sync returned it in"
                 << toCString(roomData.joinState)
                 << "state - suspiciously fast turnaround";
        }
        if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) )
        {
            r->updateData(std::move(roomData));
            if (d->firstTimeRooms.removeOne(r))
                emit loadedRoomState(r);
        }
        QCoreApplication::processEvents();
    }
    for (auto&& accountEvent: data.takeAccountData())
    {
        if (accountEvent->type() == EventType::DirectChat)
        {
            const auto usersToDCs = ptrCast<DirectChatEvent>(move(accountEvent))
                                        ->usersToDirectChats();
            DirectChatsMap removals =
                erase_if(d->directChats, [&usersToDCs] (auto it) {
                    return !usersToDCs.contains(it.key()->id(), it.value());
                });
            if (MAIN().isDebugEnabled())
                for (auto it = removals.begin(); it != removals.end(); ++it)
                    qCDebug(MAIN) << it.value()
                        << "is no more a direct chat with" << it.key()->id();

            DirectChatsMap additions;
            for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it)
            {
                const auto* u = user(it.key());
                if (!d->directChats.contains(u, it.value()))
                {
                    additions.insert(u, it.value());
                    d->directChats.insert(u, it.value());
                    qCDebug(MAIN) << "Marked room" << it.value()
                                  << "as a direct chat with" << u->id();
                }
            }
            if (!additions.isEmpty() || !removals.isEmpty())
                emit directChatsListChanged(additions, removals);

            continue;
        }
        d->accountData[accountEvent->jsonType()] =
                fromJson<AccountDataMap>(accountEvent->contentJson());
        emit accountDataChanged(accountEvent->jsonType());
    }
}

void Connection::stopSync()
{
    if (d->syncJob)
    {
        d->syncJob->abandon();
        d->syncJob = nullptr;
    }
}

void Connection::postMessage(Room* room, const QString& type, const QString& message) const
{
    callApi<SendEventJob>(room->id(), type, message);
}

PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const
{
    return callApi<PostReceiptJob>(room->id(), "m.read", event->id());
}

JoinRoomJob* Connection::joinRoom(const QString& roomAlias)
{
    auto job = callApi<JoinRoomJob>(roomAlias);
    connect(job, &JoinRoomJob::success,
            this, [this, job] { provideRoom(job->roomId(), JoinState::Join); });
    return job;
}

void Connection::leaveRoom(Room* room)
{
    callApi<LeaveRoomJob>(room->id());
}

inline auto splitMediaId(const QString& mediaId)
{
    auto idParts = mediaId.split('/');
    Q_ASSERT_X(idParts.size() == 2, __FUNCTION__,
               ("'" + mediaId +
                "' doesn't look like 'serverName/localMediaId'").toLatin1());
    return idParts;
}

MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId,
    QSize requestedSize, RunningPolicy policy) const
{
    auto idParts = splitMediaId(mediaId);
    return callApi<MediaThumbnailJob>(policy,
        idParts.front(), idParts.back(), requestedSize);
}

MediaThumbnailJob* Connection::getThumbnail(const QUrl& url,
    QSize requestedSize, RunningPolicy policy) const
{
    return getThumbnail(url.authority() + url.path(), requestedSize, policy);
}

MediaThumbnailJob* Connection::getThumbnail(const QUrl& url,
    int requestedWidth, int requestedHeight, RunningPolicy policy) const
{
    return getThumbnail(url, QSize(requestedWidth, requestedHeight), policy);
}

UploadContentJob* Connection::uploadContent(QIODevice* contentSource,
        const QString& filename, const QString& contentType) const
{
    return callApi<UploadContentJob>(contentSource, filename, contentType);
}

UploadContentJob* Connection::uploadFile(const QString& fileName,
                                         const QString& contentType)
{
    auto sourceFile = new QFile(fileName);
    if (sourceFile->open(QIODevice::ReadOnly))
    {
        qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName()
                        << "for reading";
        return nullptr;
    }
    return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(),
                         contentType);
}

GetContentJob* Connection::getContent(const QString& mediaId) const
{
    auto idParts = splitMediaId(mediaId);
    return callApi<GetContentJob>(idParts.front(), idParts.back());
}

GetContentJob* Connection::getContent(const QUrl& url) const
{
    return getContent(url.authority() + url.path());
}

DownloadFileJob* Connection::downloadFile(const QUrl& url,
                                          const QString& localFilename) const
{
    auto mediaId = url.authority() + url.path();
    auto idParts = splitMediaId(mediaId);
    auto* job = callApi<DownloadFileJob>(idParts.front(), idParts.back(),
                                         localFilename);
    return job;
}

CreateRoomJob* Connection::createRoom(RoomVisibility visibility,
    const QString& alias, const QString& name, const QString& topic,
    const QStringList& invites, const QString& presetName,
    bool isDirect, bool guestsCanJoin,
    const QVector<CreateRoomJob::StateEvent>& initialState,
    const QVector<CreateRoomJob::Invite3pid>& invite3pids,
    const QJsonObject& creationContent)
{
    // FIXME: switch to using QStringList instead of const QStringList& in 0.4
    auto filteredInvites = invites;
    filteredInvites.removeOne(d->userId); // The creator is by definition in the room
    auto job = callApi<CreateRoomJob>(
            visibility == PublishRoom ? "public" : "private", alias, name,
            topic, filteredInvites, invite3pids, creationContent, initialState,
            presetName, isDirect, guestsCanJoin);
    connect(job, &BaseJob::success, this, [this,job] {
        emit createdRoom(provideRoom(job->roomId(), JoinState::Join));
    });
    return job;
}

void Connection::requestDirectChat(const QString& userId)
{
    doInDirectChat(userId, [this] (Room* r) { emit directChatAvailable(r); });
}

void Connection::doInDirectChat(const QString& userId,
                                std::function<void (Room*)> operation)
{
    // There can be more than one DC; find the first valid, and delete invalid
    // (left/forgotten) ones along the way.
    const auto* u = user(userId);
    DirectChatsMap removals;
    for (auto it = d->directChats.find(u);
         it != d->directChats.end() && it.key() == u; ++it)
    {
        const auto& roomId = *it;
        if (auto r = room(roomId, JoinState::Join))
        {
            Q_ASSERT(r->id() == roomId);
            // A direct chat with yourself should only involve yourself :)
            if (userId == d->userId && r->memberCount() > 1)
                continue;
            qCDebug(MAIN) << "Requested direct chat with" << userId
                          << "is already available as" << r->id();
            operation(r);
            return;
        }
        if (auto ir = invitation(roomId))
{'fraction_non_alphanumeric': 0.130381270171216, 'fraction_numerical': 0.002270116514413872, 'mean_word_length': 2.4968439173680186, 'pattern_counts': {'":': 6, '<': 249, '<?xml version=': 0, '>': 236, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 54, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2084550', 'n_tokens_mistral': 10339, 'n_tokens_neox': 10158, 'n_words': 2577}