Skip to content
Snippets Groups Projects
Select Git revision
  • cb0b513cbc1b29abdc99d4fa2255f1c9cf4a7bae
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/201911
  • release/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • release/201811
  • release/201808
  • releases/beta1
  • packaging
  • native
  • release-0.2.x
  • 1.0.0
  • 0.2.0
  • 0.1.1
  • 0.1.0
25 results

chatview.html

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    storagehelper.cpp 47.12 KiB
    /****************************************************************************
     *    Copyright (C) 2017-2019 Savoir-faire Linux Inc.                       *
     *   Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com>             *
     *   Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>           *
     *   Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>       *
     *   Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>         *
     *                                                                          *
     *   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 General Public License      *
     *   along with this program.  If not, see <http://www.gnu.org/licenses/>.  *
     ***************************************************************************/
    #include "storagehelper.h"
    
    #include "api/profile.h"
    #include "api/datatransfer.h"
    #include "uri.h"
    #include "vcard.h"
    
    #include <account_const.h>
    #include <datatransfer_interface.h>
    
    #include <QImage>
    #include <QByteArray>
    #include <QBuffer>
    #include <QJsonObject>
    #include <QJsonDocument>
    
    #include <fstream>
    #include <thread>
    #include <cstring>
    
    namespace lrc
    {
    
    namespace authority
    {
    
    namespace storage
    {
    
    QString getPath()
    {
        QDir dataDir(QStandardPaths::writableLocation(
            QStandardPaths::AppLocalDataLocation));
        // Avoid to depends on the client name.
        dataDir.cdUp();
        return dataDir.absolutePath() + "/jami/";
    }
    
    std::string
    prepareUri(const std::string& uri, api::profile::Type type)
    {
        URI uriObject(QString::fromStdString(uri));
        switch (type) {
        case api::profile::Type::SIP:
            return uriObject.format(URI::Section::USER_INFO | URI::Section::HOSTNAME)
                .toStdString();
            break;
        case api::profile::Type::RING:
            return uriObject.format(URI::Section::USER_INFO)
                .toStdString();
            break;
        case api::profile::Type::INVALID:
        case api::profile::Type::PENDING:
        case api::profile::Type::TEMPORARY:
        case api::profile::Type::COUNT__:
        default:
            return uri;
        }
    }
    
    std::string
    getFormattedCallDuration(const std::time_t duration)
    {
        if (duration == 0) return {};
        std::string formattedString;
        auto minutes = duration / 60;
        auto seconds = duration % 60;
        if (minutes > 0) {
            formattedString += std::to_string(minutes) + ":";
            if (formattedString.length() == 2) {
                formattedString = "0" + formattedString;
            }
        } else {
            formattedString += "00:";
        }
        if (seconds < 10) formattedString += "0";
        formattedString += std::to_string(seconds);
        return formattedString;
    }
    
    std::string
    getCallInteractionString(const std::string& authorUri,
                             const std::time_t& duration)
    {
        if (duration < 0) {
            if (authorUri.empty()) {
                return "📞 " + QObject::tr("Outgoing call").toStdString();
            } else {
                return "📞 " + QObject::tr("Incoming call").toStdString();
            }
        } else if (authorUri.empty()) {
            if (duration) {
                return "📞 " + QObject::tr("Outgoing call").toStdString()
                        + " - " + getFormattedCallDuration(duration);
            } else {
                return "🕽 " + QObject::tr("Missed outgoing call").toStdString();
            }
        } else {
            if (duration) {
                return "📞 " + QObject::tr("Incoming call").toStdString()
                        + " - " + getFormattedCallDuration(duration);
            } else {
                return "🕽 " + QObject::tr("Missed incoming call").toStdString();
            }
        }
    }
    
    std::string
    getContactInteractionString(const std::string& authorUri,
                                const api::interaction::Status& status)
    {
        if (authorUri.empty()) {
            return QObject::tr("Contact added").toStdString();
        } else {
            if (status == api::interaction::Status::UNKNOWN) {
                return QObject::tr("Invitation received").toStdString();
            } else if (status == api::interaction::Status::SUCCESS) {
                return QObject::tr("Invitation accepted").toStdString();
            }
        }
        return {};
    }
    
    namespace vcard
    {
    std::string compressedAvatar(const std::string& image);
    void setProfile(const std::string& accountId,
                    const api::profile::Info& profileInfo,
                    const bool overwrite,
                    const bool isPeer);
    
    std::string
    compressedAvatar(const std::string& image)
    {
        QImage qimage;
        const bool ret = qimage.loadFromData(QByteArray::fromBase64(image.c_str()), 0);
        if (!ret) {
            qDebug() << "vCard image loading failed";
            return image;
        }
        QByteArray bArray;
        QBuffer buffer(&bArray);
        buffer.open(QIODevice::WriteOnly);
        qimage.scaled({ 128,128 }).save(&buffer, "JPEG", 90);
        auto b64Img = bArray.toBase64().trimmed();
        return std::string(b64Img.constData(), b64Img.length());
    }
    
    std::string
    profileToVcard(const api::profile::Info& profileInfo,
                   bool compressImage)
    {
        using namespace api;
        bool compressedImage = std::strncmp(profileInfo.avatar.c_str(), "/9j/", 4) == 0;
        if (compressedImage && !compressImage) {
            compressImage = false;
        }
        std::string vCardStr = vCard::Delimiter::BEGIN_TOKEN;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        vCardStr += vCard::Property::VERSION;
        vCardStr += ":2.1";
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        vCardStr += vCard::Property::FORMATTED_NAME;
        vCardStr += ":";
        vCardStr += profileInfo.alias;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        if (profileInfo.type == profile::Type::RING) {
            vCardStr += vCard::Property::TELEPHONE;
            vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
            vCardStr += "other:ring:";
            vCardStr += profileInfo.uri;
            vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        } else {
            vCardStr += vCard::Property::TELEPHONE;
            vCardStr += ":";
            vCardStr += profileInfo.uri;
            vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        }
        vCardStr += vCard::Property::PHOTO;
        vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
        vCardStr += vCard::Property::BASE64;
        vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
        if (compressImage) {
            vCardStr += vCard::Property::TYPE_JPEG;
            vCardStr += ":";
            vCardStr += compressedImage ? profileInfo.avatar : compressedAvatar(profileInfo.avatar);
        } else {
            vCardStr += compressedImage ? vCard::Property::TYPE_JPEG : vCard::Property::TYPE_PNG;
            vCardStr += ":";
            vCardStr += profileInfo.avatar;
        }
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        vCardStr += vCard::Delimiter::END_TOKEN;
        return vCardStr;
    }
    
    void
    setProfile(const std::string& accountId,
               const api::profile::Info& profileInfo,
               const bool isPeer)
    {
        auto vcard = vcard::profileToVcard(profileInfo);
        auto accountLocalPath = getPath() + QString::fromStdString(accountId) + "/";
        QString filePath;
        QFile file;
        if (isPeer) {
            filePath = accountLocalPath + "profiles/" +
                QString(QByteArray::fromStdString(profileInfo.uri).toBase64()) + ".vcf";
            file.setFileName(filePath);
        } else {
            filePath = accountLocalPath + "profile" + ".vcf";
            file.setFileName(filePath);
        }
        if (!file.open(QIODevice::WriteOnly)) {
            qWarning().noquote() << "Can't open file for writing: " << filePath;
            return;
        }
        QTextStream in(&file);
        in.setCodec("UTF-8");
        in << QString::fromStdString(vcard);
    }
    } // namespace vcard
    
    std::vector<std::string>
    getConversationsWithPeer(Database& db, const std::string& participant_uri)
    {
        return db.select("id",
                         "conversations",
                         "participant=:participant",
                         {{":participant", participant_uri}}).payloads;
    }
    
    std::vector<std::string>
    getPeerParticipantsForConversation(Database& db, const std::string& conversationId)
    {
        return db.select("participant",
                         "conversations",
                         "id=:id",
                         { {":id", conversationId} }).payloads;
    }
    
    void
    createOrUpdateProfile(const std::string & accountId,
                  const api::profile::Info & profileInfo,
                  const bool isPeer)
    {
        if (isPeer) {
            auto contact = storage::buildContactFromProfile(accountId, profileInfo.uri, profileInfo.type);
            if (!profileInfo.alias.empty()) contact.profileInfo.alias = profileInfo.alias;
            if (!profileInfo.avatar.empty()) contact.profileInfo.avatar = profileInfo.avatar;
            vcard::setProfile(accountId, contact.profileInfo, isPeer);
            return;
        }
        vcard::setProfile(accountId, profileInfo, isPeer);
    }
    
    std::string
    getAccountAvatar(const std::string& accountId)
    {
        auto accountLocalPath = getPath() + QString::fromStdString(accountId) + "/";
        QString filePath;
        filePath = accountLocalPath + "profile.vcf";
        QFile file(filePath);
        if (!file.open(QIODevice::ReadOnly)) {
            qWarning() << "Can't open file: " << filePath;
            return {};
        }
        QTextStream in(&file);
        in.setCodec("UTF-8");
        const auto vCard = lrc::vCard::utils::toHashMap(in.readAll().toUtf8());
        const auto photo = (vCard.find(vCard::Property::PHOTO_PNG) == vCard.end()) ?
            vCard[vCard::Property::PHOTO_JPEG] : vCard[vCard::Property::PHOTO_PNG];
        return photo.toStdString();
    }
    
    api::contact::Info
    buildContactFromProfile(const std::string & accountId,
                            const std::string& peer_uri,
                            const api::profile::Type& type)
    {
        lrc::api::profile::Info profileInfo;
        profileInfo.uri = peer_uri;
        profileInfo.type = type;
        auto accountLocalPath = getPath() + QString::fromStdString(accountId) + "/";
        QString b64filePath;
        b64filePath = accountLocalPath + "profiles/" + QString(QByteArray::fromStdString(peer_uri).toBase64()) + ".vcf";
        QFile file(b64filePath);
        if (!file.open(QIODevice::ReadOnly)) {
            // try non-base64 path
            QString filePath = accountLocalPath + "profiles/" + QString::fromStdString(peer_uri) + ".vcf";
            file.setFileName(filePath);
            if (!file.open(QIODevice::ReadOnly)) {
                qWarning().noquote() << "Can't open file: " << filePath;
                return { profileInfo, "", true, false };
            }
            // rename it
            qWarning().noquote() << "Renaming profile: " << filePath;
            file.rename(b64filePath);
            // reopen it
            if (!file.open(QIODevice::ReadOnly)) {
                qWarning().noquote() << "Can't open file: " << b64filePath;
                return { profileInfo, "", true, false };
            }
        }
        QTextStream in(&file);
        in.setCodec("UTF-8");
        QByteArray vcard = in.readAll().toUtf8();
        const auto vCard = lrc::vCard::utils::toHashMap(vcard);
        const auto alias = vCard[vCard::Property::FORMATTED_NAME];
        const auto photo = (vCard.find(vCard::Property::PHOTO_PNG) == vCard.end()) ?
            vCard[vCard::Property::PHOTO_JPEG] : vCard[vCard::Property::PHOTO_PNG];
    
        profileInfo.avatar = photo.toStdString();
        profileInfo.alias = alias.toStdString();
        return { profileInfo, "", true, false };
    }
    
    std::vector<std::string> getAllConversations(Database & db)
    {
        return db.select("id", "conversations", {}, {}).payloads;
    }
    
    std::vector<std::string>
    getConversationsBetween(Database& db, const std::string& peer1_uri, const std::string& peer2_uri)
    {
        auto conversationsForPeer1 = getConversationsWithPeer(db, peer1_uri);
        std::sort(conversationsForPeer1.begin(), conversationsForPeer1.end());
        auto conversationsForPeer2 = getConversationsWithPeer(db, peer2_uri);
        std::sort(conversationsForPeer2.begin(), conversationsForPeer2.end());
        std::vector<std::string> common;
    
        std::set_intersection(conversationsForPeer1.begin(), conversationsForPeer1.end(),
                              conversationsForPeer2.begin(), conversationsForPeer2.end(),
                              std::back_inserter(common));
        return common;
    }
    
    std::string
    beginConversationWithPeer(Database& db, const std::string& peer_uri, const bool isOutgoing)
    {
        // Add conversation between account and profile
        auto newConversationsId = db.select("IFNULL(MAX(id), 0) + 1",
                                            "conversations",
                                            "1=1",
                                            {}).payloads[0];
        db.insertInto("conversations",
                      {{":id", "id"}, {":participant", "participant"}},
                      {{":id", newConversationsId}, {":participant", peer_uri}});
        api::interaction::Info msg{
            isOutgoing ? "" : peer_uri,
            {},
            std::time(nullptr),
            0,
            api::interaction::Type::CONTACT,
            isOutgoing ? api::interaction::Status::SUCCESS : api::interaction::Status::UNKNOWN,
            isOutgoing
        };
        // Add first interaction
        addMessageToConversation(db, newConversationsId, msg);
        return newConversationsId;
    }
    
    void
    getHistory(Database& db, api::conversation::Info& conversation)
    {
        auto interactionsResult = db.select("id, author, body, timestamp, type, status, is_read, extra_data",
                                            "interactions",
                                            "conversation=:conversation",
                                            {{":conversation", conversation.uid}});
        auto nCols = 8;
        if (interactionsResult.nbrOfCols == nCols) {
            auto payloads = interactionsResult.payloads;
            for (decltype(payloads.size()) i = 0; i < payloads.size(); i += nCols) {
                std::string durationString;
                auto extra_data_str = QString::fromStdString(payloads[i + 7]);
                if (!extra_data_str.isEmpty()) {
                    auto jsonData = JSONFromString(extra_data_str);
                    durationString = readJSONValue(jsonData, "duration").toStdString();
                }
                auto body = payloads[i + 2];
                auto type = api::interaction::to_type(payloads[i + 4]);
                std::time_t duration = durationString.empty() ? 0 : std::stoi(durationString);
                auto status = api::interaction::to_status(payloads[i + 5]);
                if (type == api::interaction::Type::CALL) {
                    body = getCallInteractionString(payloads[i + 1], duration);
                } else if(type == api::interaction::Type::CONTACT) {
                    body = getContactInteractionString(payloads[i + 1], status);
                }
                auto msg = api::interaction::Info({
                        payloads[i + 1],
                        body,
                        std::stoi(payloads[i + 3]),
                        duration,
                        type,
                        status,
                        (payloads[i + 6] == "1" ? true : false)
                    });
                conversation.interactions.emplace(std::stoull(payloads[i]), std::move(msg));
                conversation.lastMessageUid = std::stoull(payloads[i]);
            }
        }
    }
    
    int
    addMessageToConversation(Database& db,
                             const std::string& conversationId,
                             const api::interaction::Info& msg)
    {
        return db.insertInto("interactions", {
                { ":author", "author" },
                { ":conversation", "conversation" },
                { ":timestamp", "timestamp" },
                { ":body", "body" },
                { ":type", "type" },
                { ":status", "status" },
                { ":is_read", "is_read" }
            }, {
                { ":author", msg.authorUri},
                { ":conversation", conversationId},
                { ":timestamp", std::to_string(msg.timestamp)},
                { ":body", msg.body},
                { ":type", to_string(msg.type)},
                { ":status", to_string(msg.status)},
                { ":is_read", msg.isRead ? "1" : "0" }
            });
    }
    
    int
    addOrUpdateMessage(Database& db,
                       const std::string& conversationId,
                       const api::interaction::Info& msg,
                       const std::string& daemonId)
    {
        // Check if profile is already present.
        auto msgAlreadyExists = db.select("id",
                                          "interactions",
                                          "author=:author AND daemon_id=:daemon_id",
                                          { {":author", msg.authorUri},
                                          { ":daemon_id", daemonId } }).payloads;
        if (msgAlreadyExists.empty()) {
            return db.insertInto("interactions", {
                    {":author", "author"},
                    {":conversation", "conversation"},
                    {":timestamp", "timestamp"},
                    {":body", "body"},
                    {":type", "type"},
                    {":status", "status"},
                    {":daemon_id", "daemon_id"}
                }, {
                    {":author", msg.authorUri.empty() ? "" : msg.authorUri},
                    {":conversation", conversationId},
                    {":timestamp", std::to_string(msg.timestamp)},
                    {msg.body.empty() ? "" : ":body", msg.body},
                    {":type", to_string(msg.type)},
                    {daemonId.empty() ? "" : ":daemon_id", daemonId},
                    {":status", to_string(msg.status)}
                });
        } else {
            // already exists @ id(msgAlreadyExists[0])
            auto id = msgAlreadyExists[0];
            std::string extra_data;
            if (msg.type == api::interaction::Type::CALL) {
                auto duration = std::max(msg.duration, static_cast<std::time_t>(0));
                auto extra_data_str = getInteractionExtraDataById(db, id);
                auto extra_data_JSON = JSONFromString(QString::fromStdString(extra_data_str));
                writeJSONValue(extra_data_JSON, "duration", QString::number(duration));
                extra_data = stringFromJSON(extra_data_JSON).toStdString();
            }
            db.update("interactions",
                      { "body=:body, extra_data=:extra_data" },
                      { {msg.body.empty() ? "" : ":body", msg.body},
                      { extra_data.empty() ? "" : ":extra_data", extra_data } },
                      "id=:id", { {":id", id} });
            return std::stoi(id);
        }
    
    }
    int
    addDataTransferToConversation(Database& db,
                                  const std::string& conversationId,
                                  const api::datatransfer::Info& infoFromDaemon)
    {
        return db.insertInto("interactions", {
                {":author", "author"},
                {":conversation", "conversation"},
                {":timestamp", "timestamp"},
                {":body", "body"},
                {":type", "type"},
                {":status", "status"},
                {":is_read", "is_read"}
            }, {
                {":author", infoFromDaemon.isOutgoing ? "" : infoFromDaemon.peerUri},
                {":conversation", conversationId},
                {":timestamp", std::to_string(std::time(nullptr))},
                {":body", infoFromDaemon.path},
                {":type", infoFromDaemon.isOutgoing ?
                        "DATA_TRANSFER" :
                        "DATA_TRANSFER"},
                {":status", "TRANSFER_CREATED"},
                {":is_read", "0"}
            });
    }
    
    void addDaemonMsgId(Database& db,
                        const std::string& interactionId,
                        const std::string& daemonId)
    {
        db.update("interactions",
                  "daemon_id=:daemon_id",
                  {{":daemon_id", daemonId}},
                  "id=:id", {{":id", interactionId}});
    }
    
    std::string getDaemonIdByInteractionId(Database& db, const std::string& id)
    {
        auto ids = db.select("daemon_id",
                             "interactions",
                             "id=:id",
                             {{":id", id}}).payloads;
        return ids.empty() ? "" : ids[0];
    }
    
    std::string getInteractionIdByDaemonId(Database& db, const std::string& daemon_id)
    {
        auto ids = db.select("id",
                             "interactions",
                             "daemon_id=:daemon_id",
                             {{":daemon_id", daemon_id}}).payloads;
        return ids.empty() ? "" : ids[0];
    }
    
    std::string getInteractionExtraDataById(Database& db, const std::string& id,
                                            const std::string& key)
    {
        auto extra_datas = db.select("extra_data",
                                     "interactions",
                                     "id=:id",
                                     { {":id", id} }).payloads;
        if (key.empty()) {
            return extra_datas.empty() ? "" : extra_datas[0];
        }
        std::string value;
        auto extra_data_str = QString::fromStdString(extra_datas[0]);
        if (!extra_data_str.isEmpty()) {
            value = readJSONValue(JSONFromString(extra_data_str), QString::fromStdString(key))
                .toStdString();
        }
        return value;
    }
    
    void updateInteractionBody(Database& db, unsigned int id,
                               const std::string& newBody)
    {
        db.update("interactions", "body=:body",
                  {{":body", newBody}},
                  "id=:id", {{":id", std::to_string(id)}});
    }
    
    void updateInteractionStatus(Database& db, unsigned int id,
                                 api::interaction::Status newStatus)
    {
        db.update("interactions",
                  { "status=:status" },
                  {{":status", api::interaction::to_string(newStatus)}},
                  "id=:id", {{":id", std::to_string(id)}});
    }
    
    void setInteractionRead(Database& db, unsigned int id)
    {
        db.update("interactions",
                  { "is_read=:is_read" },
                  { {":is_read", "1"} },
                  "id=:id", { {":id", std::to_string(id)} });
    }
    
    std::string
    conversationIdFromInteractionId(Database& db, unsigned int interactionId)
    {
        auto result = db.select("conversation",
                                "interactions",
                                "id=:id",
                                {{":id", std::to_string(interactionId)}});
        if (result.nbrOfCols == 1 && result.payloads.size()) {
            return result.payloads[0];
        }
        return {};
    }
    
    void clearHistory(Database& db,
                      const std::string& conversationId)
    {
        db.deleteFrom("interactions",
                      "conversation=:conversation",
                      {{":conversation", conversationId}});
    }
    
    void clearInteractionFromConversation(Database& db,
                                          const std::string& conversationId,
                                          const uint64_t& interactionId)
    {
        db.deleteFrom("interactions",
                      "conversation=:conversation AND id=:id",
                      {{":conversation", conversationId},
                      {":id", std::to_string(interactionId)}});
    }
    
    void clearAllHistory(Database& db)
    {
        db.truncateTable("interactions");
    }
    
    void
    deleteObsoleteHistory(Database& db, long int date)
    {
        db.deleteFrom("interactions", "timestamp<=:date", { {":date", std::to_string(date)} });
    }
    
    void
    removeContact(Database& db, const std::string& contactUri)
    {
        // Get common conversations
        auto conversations = getConversationsWithPeer(db, contactUri);
        // Remove conversations + interactions
        for (const auto& conversationId: conversations) {
            // Remove conversation
            db.deleteFrom("conversations", "id=:id", {{":id", conversationId}});
            // clear History
            db.deleteFrom("interactions", "conversation=:id", {{":id", conversationId}});
        }
    }
    
    int
    countUnreadFromInteractions(Database& db, const std::string& conversationId)
    {
        return db.count("is_read",
                        "interactions",
                        "is_read=:is_read AND conversation=:id",
                        {{":is_read", "0"}, {":id", conversationId}});
    }
    
    uint64_t
    getLastTimestamp(Database& db)
    {
        auto timestamps = db.select("MAX(timestamp)", "interactions", "1=1", {}).payloads;
        auto result = std::time(nullptr);
        try {
            if (!timestamps.empty() && !timestamps[0].empty()) {
                result = std::stoull(timestamps[0]);
            }
        } catch (const std::out_of_range& e) {
            qDebug() << "storage::getLastTimestamp, stoull throws an out_of_range exception: " << e.what();
        } catch (const std::invalid_argument& e) {
            qDebug() << "storage::getLastTimestamp, stoull throws an invalid_argument exception: " << e.what();
        }
        return result;
    }
    
    namespace {
    QString stringFromJSON(const QJsonObject& json)
    {
        QJsonDocument doc(json);
        return QString::fromLocal8Bit(doc.toJson(QJsonDocument::Compact));
    }
    
    QJsonObject
    JSONFromString(const QString& str)
    {
        QJsonObject json;
        QJsonDocument doc = QJsonDocument::fromJson(str.toUtf8());
    
        if (!doc.isNull()) {
            if (doc.isObject()) {
                json = doc.object();
            } else {
                qDebug() << "Document is not a JSON object: " << str;
            }
        } else {
            qDebug() << "Invalid JSON: " << str;
        }
        return json;
    }
    
    QString JSONStringFromInitList(const std::initializer_list<QPair<QString, QJsonValue>> args)
    {
        QJsonObject jsonObject(args);
        return stringFromJSON(jsonObject);
    }
    
    QString
    readJSONValue(const QJsonObject& json, const QString& key)
    {
        if (!json.isEmpty() && json.contains(key) && json[key].isString()) {
            if (json[key].isString()) {
                return json[key].toString();
            }
        }
        return {};
    }
    
    void
    writeJSONValue(QJsonObject& json, const QString& key, const QString& value)
    {
        json[key] = value;
    }
    }
    
    //================================================================================
    // This section provides migration helpers from ring.db
    // to per-account databases yielding a file structure like:
    //
    // { local_storage } / jami
    // └──{ account_id }
    // ├── config.yml
    // ├── contacts
    // ├── export.gz
    // ├── incomingTrustRequests
    // ├── knownDevicesNames
    // ├── history.db < --conversations and interactions database
    // ├── profile.vcf < --account vcard
    // ├── profiles < --account contact vcards
    // │   │──{ contact_uri }.vcf
    // │   └── ...
    // ├── ring_device.crt
    // └── ring_device.key
    //================================================================================
    namespace migration {
    
    enum class msgFlag {
        IS_INCOMING,
        IS_OUTGOING,
        IS_CONTACT_ADDED,
        IS_INVITATION_RECEIVED,
        IS_INVITATION_ACCEPTED,
        IS_TEXT
    };
    
    std::string profileToVcard(const lrc::api::profile::Info&, const std::string&);
    uint64_t getTimeFromTimeStr(const std::string&) noexcept;
    std::pair<msgFlag, uint64_t> migrateMessageBody(const std::string&,
                                                    const lrc::api::interaction::Type&);
    std::vector<std::string> getPeerParticipantsForConversationId(lrc::Database&,
                                                                  const std::string&,
                                                                  const std::string&);
    void migrateAccountDb(const QString&,
                          std::shared_ptr<lrc::Database>,
                          std::shared_ptr<lrc::Database>);
    
    namespace interaction {
    
    static inline api::interaction::Type
    to_type(const std::string& type)
    {
        if (type == "TEXT")
            return api::interaction::Type::TEXT;
        else if (type == "CALL")
            return api::interaction::Type::CALL;
        else if (type == "CONTACT")
            return api::interaction::Type::CONTACT;
        else if (type == "OUTGOING_DATA_TRANSFER")
            return api::interaction::Type::DATA_TRANSFER;
        else if (type == "INCOMING_DATA_TRANSFER")
            return api::interaction::Type::DATA_TRANSFER;
        else
            return api::interaction::Type::INVALID;
    }
    
    static inline std::string
    to_migrated_status_string(const std::string& status)
    {
        if (status == "FAILED")
            return "FAILURE";
        else if (status == "SUCCEED")
            return "SUCCESS";
        else if (status == "READ")
            return "SUCCESS";
        else if (status == "UNREAD")
            return "SUCCESS";
        else
            return status;
    
    }
    
    } // namespace interaction
    
    std::string
    profileToVcard(const api::profile::Info& profileInfo,
                   const std::string& accountId = {})
    {
        using namespace api;
        bool compressedImage = std::strncmp(profileInfo.avatar.c_str(), "/9g=", 4) == 0;;
        std::string vCardStr = vCard::Delimiter::BEGIN_TOKEN;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        vCardStr += vCard::Property::VERSION;
        vCardStr += ":2.1";
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        if (!accountId.empty()) {
            vCardStr += vCard::Property::UID;
            vCardStr += ":";
            vCardStr += accountId;
            vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        }
        vCardStr += vCard::Property::FORMATTED_NAME;
        vCardStr += ":";
        vCardStr += profileInfo.alias;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        if (profileInfo.type == profile::Type::RING) {
            vCardStr += vCard::Property::TELEPHONE;
            vCardStr += ":";
            vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
            vCardStr += "other:ring:";
            vCardStr += profileInfo.uri;
            vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        } else {
            vCardStr += vCard::Property::TELEPHONE;
            vCardStr += profileInfo.uri;
            vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        }
        vCardStr += vCard::Property::PHOTO;
        vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
        vCardStr += "ENCODING=BASE64";
        vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
        vCardStr += compressedImage ? "TYPE=JPEG:" : "TYPE=PNG:";
        vCardStr += profileInfo.avatar;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
        vCardStr += vCard::Delimiter::END_TOKEN;
        return vCardStr;
    }
    
    uint64_t
    getTimeFromTimeStr(const std::string& str) noexcept
    {
        uint64_t minutes = 0, seconds = 0;
        std::size_t delimiterPos = str.find(":");
        if (delimiterPos != std::string::npos) {
            try {
                minutes = std::stoull(str.substr(0, delimiterPos));
                seconds = std::stoull(str.substr(delimiterPos + 1));
            } catch (const std::exception&) {
                return 0;
            }
        }
        return minutes * 60 + seconds;
    }
    
    std::pair<msgFlag, uint64_t>
    migrateMessageBody(const std::string& body, const api::interaction::Type& type)
    {
        uint64_t duration{ 0 };
        // check in english and local to determine the direction of the call
        static QString emo = "Missed outgoing call";
        static QString lmo = QObject::tr("Missed outgoing call");
        static QString eo  = "Outgoing call";
        static QString lo  = QObject::tr("Outgoing call");
        static QString eca = "Contact added";
        static QString lca = QObject::tr("Contact added");
        static QString eir = "Invitation received";
        static QString lir = QObject::tr("Invitation received");
        static QString eia = "Invitation accepted";
        static QString lia = QObject::tr("Invitation accepted");
        auto qstrBody = QString::fromStdString(body);
        switch (type) {
        case api::interaction::Type::CALL:
            {
            bool en_missedOut   = qstrBody.contains(emo);
            bool en_out         = qstrBody.contains(eo);
            bool loc_missedOut  = qstrBody.contains(lmo);
            bool loc_out        = qstrBody.contains(lo);
            bool outgoingCall   = en_missedOut || en_out || loc_missedOut || loc_out;
            std::size_t dashPos = body.find("-");
            if (dashPos != std::string::npos) {
                duration = getTimeFromTimeStr(body.substr(dashPos + 2));
            }
            return std::make_pair(msgFlag(outgoingCall),
                                  duration);
            }
            break;
        case api::interaction::Type::CONTACT:
            if (qstrBody.contains(eca) || qstrBody.contains(lca)) {
                return std::make_pair(msgFlag::IS_CONTACT_ADDED, 0);
            } else if (qstrBody.contains(eir) || qstrBody.contains(lir)) {
                return std::make_pair(msgFlag::IS_INVITATION_RECEIVED, 0);
            } else if (qstrBody.contains(eia) || qstrBody.contains(lia)) {
                return std::make_pair(msgFlag::IS_INVITATION_ACCEPTED, 0);
            }
            break;
        case api::interaction::Type::INVALID:
        case api::interaction::Type::TEXT:
        case api::interaction::Type::DATA_TRANSFER:
        case api::interaction::Type::COUNT__:
        default:
            return std::make_pair(msgFlag::IS_TEXT, 0);
        }
        return std::make_pair(msgFlag::IS_OUTGOING, 0);
    }
    
    std::vector<std::string>
    getPeerParticipantsForConversationId(Database& db, const std::string& profileId, const std::string& conversationId)
    {
        return db.select("participant_id",
            "conversations",
            "id=:id AND participant_id!=:participant_id",
            { {":id", conversationId}, {":participant_id", profileId} }).payloads;
    }
    
    void
    migrateAccountDb(const QString& accountId,
                     std::shared_ptr<Database> db,
                     std::shared_ptr<Database> legacyDb)
    {
        using namespace lrc::api;
        using namespace migration;
    
        auto accountLocalPath = getPath() + accountId + "/";
    
        using namespace DRing::Account;
        MapStringString accountDetails = ConfigurationManager::instance().
            getAccountDetails(accountId.toStdString().c_str());
        bool isRingAccount = accountDetails[ConfProperties::TYPE] == "RING";
        std::map<std::string, std::string> profileIdUriMap;
        std::map<std::string, std::string> convIdPeerUriMap;
        std::string accountProfileId;
    
        // 1. profiles_accounts
        // migrate account's avatar/alias from profiles table to {data_dir}/profile.vcf
        std::string accountUri;
        if (isRingAccount) {
            accountUri = accountDetails[DRing::Account::ConfProperties::USERNAME].contains("ring:") ?
                accountDetails[DRing::Account::ConfProperties::USERNAME].toStdString().substr(std::string("ring:").size()) :
                accountDetails[DRing::Account::ConfProperties::USERNAME].toStdString();
        } else {
            accountUri = accountDetails[DRing::Account::ConfProperties::USERNAME].toStdString();
        }
    
        auto accountProfileIds = legacyDb->select(
            "profile_id", "profiles_accounts",
            "account_id=:account_id AND is_account=:is_account",
            { {":account_id", accountId.toStdString()},
            {":is_account", "true"} }).payloads;
        if (accountProfileIds.size() != 1) {
            return;
        }
        accountProfileId = accountProfileIds[0];
        auto accountProfile = legacyDb->select(
            "photo, alias",
            "profiles", "id=:id",
            { {":id", accountProfileId} }).payloads;
        profile::Info accountProfileInfo;
        // if we can not find the uri in the database
        // (in the case of poorly kept SIP account uris),
        // than we cannot migrate the conversations and vcard
        if (!accountProfile.empty()) {
            accountProfileInfo = { accountUri, accountProfile[0], accountProfile[1],
                isRingAccount ? profile::Type::RING : profile::Type::SIP };
        }
        auto accountVcard = profileToVcard(accountProfileInfo, accountId.toStdString());
        auto profileFilePath = accountLocalPath + "profile" + ".vcf";
        QFile file(profileFilePath);
        if (!file.open(QIODevice::WriteOnly)) {
            throw std::runtime_error("Can't open file: " + profileFilePath.toStdString());
        }
        QTextStream(&file) << QString::fromStdString(accountVcard);
    
        // 2. profiles
        // migrate profiles from profiles table to {data_dir}/{uri}.vcf
        // - for JAMI, the scheme and the hostname is omitted
        // - for SIP, the uri is must be stripped of prefix and port
        // e.g. 3d1112ab2bb089370c0744a44bbbb0786418d40b.vcf
        //      username.vcf or username@hostname.vcf
    
        // only select non-account profiles
        auto profileIds = legacyDb->select(
            "profile_id", "profiles_accounts",
            "account_id=:account_id AND is_account=:is_account",
            { {":account_id", accountId.toStdString()},
            {":is_account", "false"} }).payloads;
        for (const auto& profileId : profileIds) {
            auto profile = legacyDb->select(
                "uri, alias, photo, type", "profiles",
                "id=:id",
                { {":id", profileId} }).payloads;
            if (profile.empty()) {
                continue;
            }
            profile::Info profileInfo{ profile[0], profile[2], profile[1] };
            auto uri = URI(QString::fromStdString(profile[0]));
            auto profileUri = uri.userinfo();
            if (!isRingAccount && uri.hasHostname()) {
                profileUri += "@" + uri.hostname();
            }
            // insert into map for use during the conversations table migration
            profileIdUriMap.insert(std::make_pair(profileId, profileUri.toStdString()));
            auto vcard = profileToVcard(profileInfo);
            // make sure the directory exists
            QDir dir(accountLocalPath + "profiles");
            if (!dir.exists())
                dir.mkpath(".");
            profileFilePath = accountLocalPath + "profiles/" + profileUri + ".vcf";
            QFile file(profileFilePath);
            // if we catch duplicates here, skip the profile because
            // the previous db structure does not guarantee unique uris
            if (file.exists()) {
                qWarning() << "Profile file already exits: " << profileFilePath;
                continue;
            }
            if (!file.open(QIODevice::WriteOnly)) {
                qWarning() << "Can't open file: " << profileFilePath;
                continue;
            }
            QTextStream(&file) << QString::fromStdString(vcard);
        }
    
        // 3. conversations
        // migrate old conversations table ==> new conversations table
        // a) participant_id INTEGER becomes participant TEXT (the uri of the participant)
        //    use the selected non-account profiles
        auto conversationIds = legacyDb->select(
            "id", "conversations",
            "participant_id=:participant_id",
            { {":participant_id", accountProfileId} }).payloads;
        if (conversationIds.empty()) {
            return;
        }
        for (auto conversationId : conversationIds) {
            // only one peer pre-groupchat
            auto peerProfileId = getPeerParticipantsForConversationId(*legacyDb, accountProfileId, conversationId);
            if (peerProfileId.empty()) {
                continue;
            }
            auto it = profileIdUriMap.find(peerProfileId.at(0));
            // we cannot insert in the conversations table without a uri
            if (it == profileIdUriMap.end()) {
                continue;
            }
            convIdPeerUriMap.insert(std::make_pair(conversationId, it->second));
            try {
                db->insertInto("conversations",
                    { {":id", "id"} ,
                    {":participant", "participant"} },
                    { { ":id", conversationId } ,
                    { ":participant", it->second } });
            } catch (const std::runtime_error& e) {
                qWarning() << "Couldn't migrate conversation: " << e.what();
                continue;
            }
        }
    
        // 4. interactions
        auto allInteractions = legacyDb->select(
            "account_id, author_id, conversation_id, \
             timestamp, body, type, status, daemon_id",
            "interactions",
            "account_id=:account_id",
            { {":account_id", accountProfileId} });
        auto interactionIt = allInteractions.payloads.begin();
        while (interactionIt != allInteractions.payloads.end()) {
            auto author_id = *(interactionIt + 1);
            auto convId = *(interactionIt + 2);
            auto timestamp = *(interactionIt + 3);
            auto body = *(interactionIt + 4);
            auto type = interaction::to_type(*(interactionIt + 5));
            auto statusStr = *(interactionIt + 6);
            auto daemonId = *(interactionIt + 7);
    
            auto it = profileIdUriMap.find(author_id);
            if (it == profileIdUriMap.end() && author_id != accountProfileId) {
                std::advance(interactionIt, allInteractions.nbrOfCols);
                continue;
            }
            // migrate body+type ==> msgFlag+duration
            auto migratedMsg = migrateMessageBody(body, type);
            auto profileUri = it == profileIdUriMap.end() ? "" : it->second;
            // clear author uri if outgoing
            switch (migratedMsg.first) {
            case msgFlag::IS_OUTGOING:
            case msgFlag::IS_CONTACT_ADDED:
                profileUri.clear();
                break;
            case msgFlag::IS_INCOMING:
            case msgFlag::IS_INVITATION_RECEIVED:
            case msgFlag::IS_INVITATION_ACCEPTED:
            {
                // try to set profile uri using the conversation id
                auto it = convIdPeerUriMap.find(convId);
                if (it == convIdPeerUriMap.end()) {
                    std::advance(interactionIt, allInteractions.nbrOfCols);
                    continue;
                }
                profileUri = it->second;
                break;
            }
            case msgFlag::IS_TEXT:
            default:
                break;
            }
            // Set all read, call and datatransfer, and contact added
            // interactions to a read state
            bool is_read = statusStr != "UNREAD"
                || type == api::interaction::Type::CALL
                || type == api::interaction::Type::CONTACT;
            // migrate status
            if (migratedMsg.first == msgFlag::IS_INVITATION_RECEIVED) {
                statusStr = "UNKNOWN";
            }
            std::string extra_data = migratedMsg.second == 0 ? "" :
                JSONStringFromInitList({
                        qMakePair(QString("duration"),
                        QJsonValue(QString::number(migratedMsg.second)))
                    })
                .toStdString();
            if (accountUri == profileUri)
                profileUri.clear();
            auto typeStr = api::interaction::to_string(type);
            try {
                db->insertInto("interactions", {
                    {":author", "author"},
                    {":conversation", "conversation"},
                    {":timestamp", "timestamp"},
                    {":body", "body"},
                    {":type", "type"},
                    {":status", "status"},
                    {":is_read", "is_read"},
                    {":daemon_id", "daemon_id"},
                    {":extra_data", "extra_data"}
                }, {
                    {":author", profileUri},
                    {":conversation", convId},
                    {":timestamp", timestamp},
                    {migratedMsg.first != msgFlag::IS_TEXT ? "" : ":body", body},
                    {":type", api::interaction::to_string(type)},
                    {":status", interaction::to_migrated_status_string(statusStr)},
                    {":is_read", is_read ? "1" : "0" },
                    {daemonId.empty() ? "" : ":daemon_id", daemonId},
                    {extra_data.empty() ? "" : ":extra_data", extra_data }
                });
            } catch (const std::runtime_error& e) {
                qWarning() << e.what();
            }
            std::advance(interactionIt, allInteractions.nbrOfCols);
        }
        qDebug() << "Done";
    }
    
    } // namespace migration
    
    std::vector<std::shared_ptr<Database>>
    migrateIfNeeded(const QStringList& accountIds,
                    MigrationCb& willMigrateCb,
                    MigrationCb& didMigrateCb)
    {
        using namespace lrc::api;
        using namespace migration;
    
        std::vector<std::shared_ptr<Database>> dbs(accountIds.size());
    
        if (!accountIds.size()) {
            qDebug() << "No accounts to migrate";
            return dbs;
        }
    
        auto appPath = getPath();
    
        // ring -> jami path migration
        QDir dataDir(appPath);
        // create data directory if not created yet
        dataDir.mkpath(appPath);
        QDir oldDataDir(appPath);
        oldDataDir.cdUp();
        oldDataDir = oldDataDir
            .absolutePath()
    #if defined(_WIN32)
            +"/Savoir-faire Linux/Ring";
    #elif defined(__APPLE__)
            +"/ring";
    #else
            + "/gnome-ring";
    #endif
        QStringList filesList = oldDataDir.entryList();
        QString filename;
        QDir dir;
        bool success = true;
        foreach(filename, filesList) {
            qDebug() << "Migrate " << oldDataDir.absolutePath() << "/" << filename
                     << " to " << dataDir.absolutePath() + "/" + filename;
            if (filename != "." && filename != "..") {
                success &= dir.rename(oldDataDir.absolutePath() + "/" + filename,
                    dataDir.absolutePath() + "/" + filename);
            }
        }
        if (success) {
            // Remove old directory if the migration is successful.
    #if defined(_WIN32)
            oldDataDir.cdUp();
    #endif
            oldDataDir.removeRecursively();
        }
    
        bool needsMigration = false;
        std::map<QString, bool> hasMigratedData;
        for (const auto& accountId : accountIds) {
            auto hasMigratedDb = QFile(appPath + accountId + "/history.db").exists() &&
                                !QFile(appPath + accountId + "/history.db-journal").exists();
            hasMigratedData.insert(std::make_pair(accountId, hasMigratedDb));
            needsMigration |= !hasMigratedDb;
        }
        if (!needsMigration) {
            // if there's any lingering pre-migration data, remove it
            QFile(dataDir.absoluteFilePath("ring.db")).remove();
            QDir(dataDir.absoluteFilePath("text/")).removeRecursively();
            QDir(dataDir.absoluteFilePath("profiles/")).removeRecursively();
            QDir(dataDir.absoluteFilePath("peer_profiles/")).removeRecursively();
            qDebug() << "No migration required";
            return dbs;
        }
    
        // A fairly long migration may now occur
        std::thread migrateThread(
            [&appPath, &accountIds, &dbs, &didMigrateCb, &dataDir, &hasMigratedData] {
                // 1. migrate old lrc -> new lrc if needed
                // 2. migrate new lrc db version 1 -> db version 1.1 if needed
                // the destructor of LegacyDatabase will remove 'ring.db' and clean out
                // old lrc files
                std::shared_ptr<Database> legacyDb;
                try {
                    legacyDb = lrc::DatabaseFactory::create<LegacyDatabase>(appPath);
                } catch (const std::runtime_error& e) {
                    qDebug() << "Exception while attempting to load legacy database: " << e.what();
                    if (didMigrateCb)
                        didMigrateCb();
                    return;
                }
    
                // attempt to make a backup of ring.db
                {
                    QFile dbFile(dataDir.absoluteFilePath("ring.db"));
                    if (dbFile.open(QIODevice::ReadOnly)) {
                        dbFile.copy(appPath + "ring.db.bak");
                    }
                }
    
                // 3. migrate db version 1.1 -> per account dbs version 1
                int index = 0;
                for (const auto& accountId : accountIds) {
                    if (hasMigratedData.at(accountId)) {
                        index++;
                        continue;
                    }
                    qDebug() << "Migrating account: " << accountId << "...";
                    // try to remove the transaction journal from a failed migration
                    QFile(appPath + accountId + "/history.db-journal").remove();
                    try {
                        QSqlDatabase::database().transaction();
                        auto dbName = QString::fromStdString(accountId.toStdString() + "/history");
                        dbs.at(index) = lrc::DatabaseFactory::create<Database>(dbName, appPath);
                        auto& db = dbs.at(index++);
                        migration::migrateAccountDb(accountId, db, legacyDb);
                        QSqlDatabase::database().commit();
                    } catch (const std::runtime_error& e) {
                        qWarning().noquote()
                            << "Could not migrate database for account: "
                            << accountId << "\n " << e.what();
                        QSqlDatabase::database().rollback();
                    }
                }
    
                // done
                if (didMigrateCb)
                    didMigrateCb();
            });
    
        // if willMigrateCb blocks, it must be unblocked by didMigrateCb
        if (willMigrateCb)
            willMigrateCb();
    
        migrateThread.join();
    
        return dbs;
    }
    
    } // namespace database
    
    } // namespace authority
    
    } // namespace lrc