From 8dfc664daca531cef1cb6ce2cf78bd335c0e87be Mon Sep 17 00:00:00 2001 From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> Date: Wed, 6 Mar 2019 13:26:09 -0500 Subject: [PATCH] database: migrate to per account database Gitlab: #407 Change-Id: I834cf0d216dfd9e6badab8d7aab951b8875c1bd6 --- CMakeLists.txt | 2 +- src/api/call.h | 2 +- src/api/contact.h | 7 + src/api/contactmodel.h | 14 +- src/api/interaction.h | 96 +- src/api/lrc.h | 14 +- src/api/newaccountmodel.h | 11 +- src/api/newcallmodel.h | 21 +- src/api/profile.h | 10 +- src/authority/databasehelper.cpp | 574 -------- src/authority/storagehelper.cpp | 1283 +++++++++++++++++ .../{databasehelper.h => storagehelper.h} | 294 ++-- src/avmodel.cpp | 4 +- src/callbackshandler.cpp | 15 +- src/callbackshandler.h | 4 + src/contactmodel.cpp | 124 +- src/conversationmodel.cpp | 436 +++--- src/database.cpp | 446 +++--- src/database.h | 135 +- src/datatransfermodel.cpp | 10 - src/lrc.cpp | 23 +- src/newaccountmodel.cpp | 263 ++-- src/newcallmodel.cpp | 53 +- src/typedefs.h | 4 +- src/uri.cpp | 21 +- src/uri.h | 3 + src/vcard.h | 5 + 27 files changed, 2420 insertions(+), 1454 deletions(-) delete mode 100644 src/authority/databasehelper.cpp create mode 100644 src/authority/storagehelper.cpp rename src/authority/{databasehelper.h => storagehelper.h} (50%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c999a6f..12b2573b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -280,7 +280,7 @@ SET( libringclient_LIB_SRCS src/conversationmodel.cpp src/database.cpp src/authority/daemon.cpp - src/authority/databasehelper.cpp + src/authority/storagehelper.cpp src/lrc.cpp src/newaccountmodel.cpp src/peerdiscoverymodel.cpp diff --git a/src/api/call.h b/src/api/call.h index a91e2734..05714e55 100644 --- a/src/api/call.h +++ b/src/api/call.h @@ -131,7 +131,7 @@ struct Info std::chrono::steady_clock::time_point startTime; Status status = Status::INVALID; Type type = Type::INVALID; - std::string peer; + std::string peerUri; bool isOutgoing; bool audioMuted = false; bool videoMuted = false; diff --git a/src/api/contact.h b/src/api/contact.h index 76608576..748df035 100644 --- a/src/api/contact.h +++ b/src/api/contact.h @@ -33,6 +33,13 @@ namespace api namespace contact { +/** + * @var profileInfo + * @var registeredName + * @var isTrusted + * @var isPresent + * @var isBanned + */ struct Info { profile::Info profileInfo; diff --git a/src/api/contactmodel.h b/src/api/contactmodel.h index 6375dd3f..bffbd752 100644 --- a/src/api/contactmodel.h +++ b/src/api/contactmodel.h @@ -57,7 +57,7 @@ public: const account::Info& owner; ContactModel(const account::Info& owner, - Database& database, + Database& db, const CallbacksHandler& callbacksHandler, const BehaviorController& behaviorController); ~ContactModel(); @@ -85,18 +85,6 @@ public: * @return list of banned contacts uris as string */ const std::list<std::string>& getBannedContacts() const; - /** - * @param uri - * @param isAccount - * @return empty string if no contact, else the uri in db - */ - const std::string getProfileId(const std::string &ur, bool isAccount = false) const; - /** - * @deprecated use getProfileId - * @param contactUri - * @return empty string if no contact, else the uri in db - */ - const std::string getContactProfileId(const std::string& contactUri) const; /** * @return all contacts for this account. */ diff --git a/src/api/interaction.h b/src/api/interaction.h index d2a5e9fb..40818d1b 100644 --- a/src/api/interaction.h +++ b/src/api/interaction.h @@ -36,8 +36,8 @@ enum class Type { TEXT, CALL, CONTACT, - OUTGOING_DATA_TRANSFER, - INCOMING_DATA_TRANSFER + DATA_TRANSFER, + COUNT__ }; static inline const std::string @@ -50,11 +50,10 @@ to_string(const Type& type) return "CALL"; case Type::CONTACT: return "CONTACT"; - case Type::OUTGOING_DATA_TRANSFER: - return "OUTGOING_DATA_TRANSFER"; - case Type::INCOMING_DATA_TRANSFER: - return "INCOMING_DATA_TRANSFER"; + case Type::DATA_TRANSFER: + return "DATA_TRANSFER"; case Type::INVALID: + case Type::COUNT__: default: return "INVALID"; } @@ -69,24 +68,19 @@ to_type(const std::string& type) return interaction::Type::CALL; else if (type == "CONTACT") return interaction::Type::CONTACT; - else if (type == "OUTGOING_DATA_TRANSFER") - return interaction::Type::OUTGOING_DATA_TRANSFER; - else if (type == "INCOMING_DATA_TRANSFER") - return interaction::Type::INCOMING_DATA_TRANSFER; + else if (type == "DATA_TRANSFER") + return interaction::Type::DATA_TRANSFER; else return interaction::Type::INVALID; } - enum class Status { INVALID, UNKNOWN, SENDING, - FAILED, - SUCCEED, - READ, - UNREAD, - TRANSFER_CREATED, /*[jn] mettre à jour les fonctions de conversion */ + FAILURE, + SUCCESS, + TRANSFER_CREATED, TRANSFER_ACCEPTED, TRANSFER_CANCELED, TRANSFER_ERROR, @@ -95,7 +89,8 @@ enum class Status { TRANSFER_AWAITING_PEER, TRANSFER_AWAITING_HOST, TRANSFER_TIMEOUT_EXPIRED, - TRANSFER_FINISHED + TRANSFER_FINISHED, + COUNT__ }; static inline const std::string @@ -106,14 +101,10 @@ to_string(const Status& status) return "UNKNOWN"; case Status::SENDING: return "SENDING"; - case Status::FAILED: - return "FAILED"; - case Status::SUCCEED: - return "SUCCEED"; - case Status::READ: - return "READ"; - case Status::UNREAD: - return "UNREAD"; + case Status::FAILURE: + return "FAILURE"; + case Status::SUCCESS: + return "SUCCESS"; case Status::TRANSFER_CREATED: return "TRANSFER_CREATED"; case Status::TRANSFER_ACCEPTED: @@ -135,6 +126,7 @@ to_string(const Status& status) case Status::TRANSFER_FINISHED: return "TRANSFER_FINISHED"; case Status::INVALID: + case Status::COUNT__: default: return "INVALID"; } @@ -144,56 +136,60 @@ static inline Status to_status(const std::string& status) { if (status == "UNKNOWN") - return interaction::Status::UNKNOWN; + return Status::UNKNOWN; else if (status == "SENDING") - return interaction::Status::SENDING; - else if (status == "FAILED") - return interaction::Status::FAILED; - else if (status == "SUCCEED") - return interaction::Status::SUCCEED; - else if (status == "READ") - return interaction::Status::READ; - else if (status == "UNREAD") - return interaction::Status::UNREAD; + return Status::SENDING; + else if (status == "FAILURE") + return Status::FAILURE; + else if (status == "SUCCESS") + return Status::SUCCESS; else if (status == "TRANSFER_CREATED") - return interaction::Status::TRANSFER_CREATED; + return Status::TRANSFER_CREATED; else if (status == "TRANSFER_ACCEPTED") - return interaction::Status::TRANSFER_ACCEPTED; + return Status::TRANSFER_ACCEPTED; else if (status == "TRANSFER_CANCELED") - return interaction::Status::TRANSFER_CANCELED; + return Status::TRANSFER_CANCELED; else if (status == "TRANSFER_ERROR") - return interaction::Status::TRANSFER_ERROR; + return Status::TRANSFER_ERROR; else if (status == "TRANSFER_UNJOINABLE_PEER") - return interaction::Status::TRANSFER_UNJOINABLE_PEER; + return Status::TRANSFER_UNJOINABLE_PEER; else if (status == "TRANSFER_ONGOING") - return interaction::Status::TRANSFER_ONGOING; + return Status::TRANSFER_ONGOING; else if (status == "TRANSFER_AWAITING_HOST") - return interaction::Status::TRANSFER_AWAITING_HOST; + return Status::TRANSFER_AWAITING_HOST; else if (status == "TRANSFER_AWAITING_PEER") - return interaction::Status::TRANSFER_AWAITING_PEER; + return Status::TRANSFER_AWAITING_PEER; else if (status == "TRANSFER_TIMEOUT_EXPIRED") - return interaction::Status::TRANSFER_TIMEOUT_EXPIRED; + return Status::TRANSFER_TIMEOUT_EXPIRED; else if (status == "TRANSFER_FINISHED") - return interaction::Status::TRANSFER_FINISHED; + return Status::TRANSFER_FINISHED; else - return interaction::Status::INVALID; + return Status::INVALID; } +/** + * @var authorUri + * @var body + * @var timestamp + * @var duration + * @var type + * @var status + * @var isRead + */ struct Info { std::string authorUri; std::string body; std::time_t timestamp = 0; + std::time_t duration = 0; Type type = Type::INVALID; Status status = Status::INVALID; + bool isRead = false; }; static inline bool isOutgoing(const Info& interaction) { - return (interaction.status != lrc::api::interaction::Status::READ - && interaction.status != lrc::api::interaction::Status::UNREAD - && interaction.type != lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) - || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER; + return interaction.authorUri.empty(); } } // namespace interaction diff --git a/src/api/lrc.h b/src/api/lrc.h index 71a8ead8..aec00d77 100644 --- a/src/api/lrc.h +++ b/src/api/lrc.h @@ -41,7 +41,15 @@ class AVModel; class LIB_EXPORT Lrc { public: - Lrc(); + /** + * Construct an Lrc object and optionally invoke callbacks + * to control ui informing the user of a possibly lengthy + * migration process. + * @param willMigrateCb + * @param didMigrateCb + */ + Lrc(MigrationCb willMigrateCb = {}, + MigrationCb didMigrateCb = {}); ~Lrc(); /** * get a reference on account model. @@ -77,6 +85,10 @@ public: * Can communicate with the daemon via dbus */ static bool dbusIsValid(); + /** + * Connect to debugMessageReceived signal + */ + void subscribeToDebugReceived(); /** * Helper: get call list from daemon diff --git a/src/api/newaccountmodel.h b/src/api/newaccountmodel.h index c9c0d47c..d178782e 100644 --- a/src/api/newaccountmodel.h +++ b/src/api/newaccountmodel.h @@ -53,12 +53,11 @@ class BehaviorController; class LIB_EXPORT NewAccountModel : public QObject { Q_OBJECT public: - using AccountInfoMap = std::map<std::string, account::Info>; - NewAccountModel(Lrc& lrc, - Database& database, const CallbacksHandler& callbackHandler, - const api::BehaviorController& behaviorController); + const api::BehaviorController& behaviorController, + MigrationCb& willMigrateCb, + MigrationCb& didMigrateCb); ~NewAccountModel(); /** @@ -173,13 +172,13 @@ public: * Set an account to the first position */ void setTopAccount(const std::string& accountId); + /** - * Build the vCard for an account + * Get the vCard for an account * @param id * @return vcard of the account */ std::string accountVCard(const std::string& accountId, bool compressImage = true) const; - std::string compressedAvatar(const std::string& img) const; Q_SIGNALS: /** diff --git a/src/api/newcallmodel.h b/src/api/newcallmodel.h index b5f0b955..dbf8b549 100644 --- a/src/api/newcallmodel.h +++ b/src/api/newcallmodel.h @@ -72,11 +72,12 @@ public: /** * Create a new call with a contact - * @param url of the contact to call + * @param uri of the contact to call * @param isAudioOnly, set to false by default * @return the call uid created. Empty string is returned if call couldn't be created. */ - std::string createCall(const std::string& url, bool isAudioOnly = false); + std::string createCall(const std::string& uri, bool isAudioOnly = false); + /** * Get the call from its call id * @param uid @@ -84,6 +85,7 @@ public: * @throw out_of_range exception if not found */ const call::Info& getCall(const std::string& uid) const; + /** * Get the call from the peer uri * @param uri @@ -92,6 +94,7 @@ public: * @throw out_of_range exception if not found */ const call::Info& getCallFromURI(const std::string& uri, bool notOver = false) const; + /** * Get conference from a peer uri * @param uri @@ -99,11 +102,13 @@ public: * @throw out_of_range exception if not found */ const call::Info& getConferenceFromURI(const std::string& uri) const; + /** * @param callId to test * @return true if callId is presend else false. */ bool hasCall(const std::string& callId) const; + /** * Send a text message to a SIP call * @param callId @@ -116,64 +121,76 @@ public: * @param callId */ void accept(const std::string& callId) const; + /** * Hang up a call * @param callId */ void hangUp(const std::string& callId) const; + /** * Refuse a call * @param callId */ void refuse(const std::string& callId) const; + /** * Toggle audio record on a call * @param callId */ void toggleAudioRecord(const std::string& callId) const; + /** * Play DTMF in a call * @param callId * @param value to play */ void playDTMF(const std::string& callId, const std::string& value) const; + /** * Toggle pause on a call * @param callId */ void togglePause(const std::string& callId) const; + /** * Toggle a media on a call * @param callId * @param media {AUDIO, VIDEO} */ void toggleMedia(const std::string& callId, const NewCallModel::Media media) const; + /** * Not implemented yet */ void setQuality(const std::string& callId, const double quality) const; + /** * Blind transfer. Directly transfer a call to a sip number * @param callId: the call to transfer * @param to: the sip number (for example: "sip:1412") */ void transfer(const std::string& callId, const std::string& to) const; + /** * Perform an attended. Transfer a call to another call * @param callIdSrc: the call to transfer * @param callIdDest: the destination's call */ void transferToCall(const std::string& callIdSrc, const std::string& callIdDest) const; + /** * Create a conference from 2 calls. * @param callIdA uid of the call A * @param callIdB uid of the call B */ void joinCalls(const std::string& callIdA, const std::string& callIdB) const; + /** * Not implemented yet */ void removeParticipant(const std::string& callId, const std::string& participant) const; + /** * @param callId * @return a human readable call duration (M:ss) diff --git a/src/api/profile.h b/src/api/profile.h index 6a571e52..1a1797e3 100644 --- a/src/api/profile.h +++ b/src/api/profile.h @@ -35,7 +35,8 @@ enum class Type { RING, SIP, PENDING, - TEMPORARY + TEMPORARY, + COUNT__ }; static inline const std::string @@ -51,6 +52,7 @@ to_string(const Type& type) case Type::TEMPORARY: return "TEMPORARY"; case Type::INVALID: + case Type::COUNT__: default: return "INVALID"; } @@ -71,6 +73,12 @@ to_type(const std::string& type) return Type::INVALID; } +/** + * @var uri + * @var avatar + * @var alias + * @var type + */ struct Info { std::string uri = ""; diff --git a/src/authority/databasehelper.cpp b/src/authority/databasehelper.cpp deleted file mode 100644 index 86adaa2d..00000000 --- a/src/authority/databasehelper.cpp +++ /dev/null @@ -1,574 +0,0 @@ -/**************************************************************************** - * 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> * - * * - * 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 "databasehelper.h" -#include "api/profile.h" -#include "api/datatransfer.h" -#include <account_const.h> - -#include <datatransfer_interface.h> - -namespace lrc -{ - -namespace authority -{ - -namespace database -{ - -std::string -getProfileId(Database& db, - const std::string& accountId, - const std::string& isAccount, - const std::string& uri) -{ - auto accountProfiles = db.select("profile_id", "profiles_accounts", - "account_id=:account_id AND is_account=:is_account", - {{":account_id", accountId}, - {":is_account", isAccount}}).payloads; - if (accountProfiles.empty() && (isAccount == "true")) { - return ""; - } - if (isAccount == "true") return accountProfiles[0]; - - // we may have many contacts profiles for one account id, - // and need to check uri in addition to account id - auto profiles = db.select("id", "profiles", "uri=:uri", {{":uri", uri}}).payloads; - - if (profiles.empty()) return ""; - std::sort(accountProfiles.begin(), accountProfiles.end()); - std::sort(profiles.begin(), profiles.end()); - - std::vector<std::string> common; - std::set_intersection(accountProfiles.begin(), accountProfiles.end(), - profiles.begin(), profiles.end(), - std::back_inserter(common)); - //if profile exists but not linked with account id, - // update profiles_accounts. Except empty uri for SIP accounts - if(common.empty()) { - if(!uri.empty()) { - db.insertInto("profiles_accounts", - {{":profile_id", "profile_id"}, {":account_id", "account_id"}, - {":is_account", "is_account"}}, - {{":profile_id", profiles[0]}, {":account_id", accountId}, - {":is_account", isAccount}}); - } - return profiles[0]; - } - return common[0]; -} - -std::string -getOrInsertProfile(Database& db, - const std::string& contactUri, - const std::string& accountId, - bool isAccount, - const std::string& type, - const std::string& alias, - const std::string& avatar) -{ - // Check if profile is already present. - const std::string isAccountStr = isAccount ? "true" : "false"; - auto profileAlreadyExists = getProfileId(db, accountId, isAccountStr, contactUri); - if (profileAlreadyExists.empty()) { - // Doesn't exists, add profile to the database - auto row = db.insertInto("profiles", - {{":uri", "uri"}, {":alias", "alias"}, {":photo", "photo"}, {":type", "type"}, - {":status", "status"}}, - {{":uri", contactUri}, {":alias", alias}, {":photo", avatar}, {":type", type}, - {":status", "TRUSTED"}}); - - if (row == -1) { - qDebug() << "contact not added to the database"; - return ""; - } - // link profile id to account id - auto profiles = db.select("profile_id", "profiles_accounts", - "profile_id=:profile_id AND \ - account_id=:account_id AND \ - is_account=:is_account", - {{":profile_id", std::to_string(row)}, - {":account_id", accountId}, - {":is_account", isAccountStr}}) - .payloads; - - if (profiles.empty()) { - db.insertInto("profiles_accounts", - {{":profile_id", "profile_id"}, - {":account_id", "account_id"}, - {":is_account", "is_account"}}, - {{":profile_id", std::to_string(row)}, - {":account_id", accountId}, - {":is_account", isAccountStr}}); - } - - return std::to_string(row); - } else { - // Exists, update and retrieve it. - if (!avatar.empty() && !alias.empty()) { - db.update("profiles", - "alias=:alias, photo=:photo", - {{":alias", alias}, {":photo", avatar}}, - "id=:id", {{":id", profileAlreadyExists}}); - } else if (!avatar.empty()) { - db.update("profiles", - "photo=:photo", - {{":photo", avatar}}, - "id=:id", {{":id", profileAlreadyExists}}); - } - return profileAlreadyExists; - } -} - -std::vector<std::string> -getConversationsForProfile(Database& db, const std::string& profileId) -{ - return db.select("id", - "conversations", - "participant_id=:participant_id", - {{":participant_id", profileId}}).payloads; -} - -std::vector<std::string> -getPeerParticipantsForConversation(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; -} - -std::string -getAvatarForProfileId(Database& db, const std::string& profileId) -{ - auto returnFromDb = db.select("photo", - "profiles", - "id=:id", - {{":id", profileId}}); - if (returnFromDb.nbrOfCols == 1 && returnFromDb.payloads.size() >= 1) { - auto payloads = returnFromDb.payloads; - return payloads[0]; - } - return ""; -} - -std::string -getAliasForProfileId(Database& db, const std::string& profileId) -{ - auto returnFromDb = db.select("alias", - "profiles", - "id=:id", - {{":id", profileId}}); - if (returnFromDb.nbrOfCols == 1 && returnFromDb.payloads.size() >= 1) { - auto payloads = returnFromDb.payloads; - return payloads[0]; - } - return ""; -} - -bool -profileCouldBeRemoved(Database& db, const std::string& profileId) -{ - auto returnFromDb = db.select("account_id", - "profiles_accounts", - "profile_id=:profile_id", - {{":profile_id", profileId}}); - if (returnFromDb.nbrOfCols == 1 && returnFromDb.payloads.size() >= 1) { - return false; - } - return true; -} - -void -setAliasForProfileId(Database& db, const std::string& profileId, const std::string& alias) -{ - db.update("profiles", - "alias=:alias", - {{":alias", alias}}, - "id=:id", - {{":id", profileId}}); -} - -void -setAvatarForProfileId(Database& db, const std::string& profileId, const std::string& avatar) -{ - db.update("profiles", - "photo=:photo", - {{":photo", avatar}}, - "id=:id", - {{":id", profileId}}); -} - -api::contact::Info -buildContactFromProfileId(Database& db, const std::string& profileId) -{ - auto returnFromDb = db.select("uri, photo, alias, type", - "profiles", - "id=:id", - {{":id", profileId}}); - if (returnFromDb.nbrOfCols == 4 && returnFromDb.payloads.size() >= 4) { - auto payloads = returnFromDb.payloads; - - api::profile::Info profileInfo = {payloads[0], payloads[1], payloads[2], api::profile::to_type(payloads[3])}; - - return {profileInfo, "", true, false}; - } - return api::contact::Info(); -} - -std::vector<std::string> -getConversationsBetween(Database& db, const std::string& accountProfile, const std::string& contactProfile) -{ - auto conversationsForAccount = getConversationsForProfile(db, accountProfile); - std::sort(conversationsForAccount.begin(), conversationsForAccount.end()); - auto conversationsForContact = getConversationsForProfile(db, contactProfile); - std::sort(conversationsForContact.begin(), conversationsForContact.end()); - std::vector<std::string> common; - - std::set_intersection(conversationsForAccount.begin(), conversationsForAccount.end(), - conversationsForContact.begin(), conversationsForContact.end(), - std::back_inserter(common)); - return common; -} - -std::string -beginConversationsBetween(Database& db, const std::string& accountProfile, const std::string& contactProfile, const std::string& firstMessage) -{ - // 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_id", "participant_id"}}, - {{":id", newConversationsId}, {":participant_id", accountProfile}}); - db.insertInto("conversations", - {{":id", "id"}, {":participant_id", "participant_id"}}, - {{":id", newConversationsId}, {":participant_id", contactProfile}}); - // Add first interaction - if (!firstMessage.empty()) - db.insertInto("interactions", - {{":account_id", "account_id"}, {":author_id", "author_id"}, - {":conversation_id", "conversation_id"}, {":timestamp", "timestamp"}, - {":body", "body"}, {":type", "type"}, - {":status", "status"}}, - {{":account_id", accountProfile}, {":author_id", accountProfile}, - {":conversation_id", newConversationsId}, - {":timestamp", std::to_string(std::time(nullptr))}, - {":body", firstMessage}, {":type", "CONTACT"}, - {":status", "SUCCEED"}}); - return newConversationsId; -} - -void -getHistory(Database& db, api::conversation::Info& conversation) -{ - auto accountProfile = getProfileId(db, conversation.accountId, "true"); - auto interactionsResult = db.select("id, author_id, body, timestamp, type, status", - "interactions", - "conversation_id=:conversation_id AND account_id=:account_id", - {{":conversation_id", conversation.uid}, {":account_id", accountProfile}}); - if (interactionsResult.nbrOfCols == 6) { - auto payloads = interactionsResult.payloads; - for (decltype(payloads.size()) i = 0; i < payloads.size(); i += 6) { - auto msg = api::interaction::Info({payloads[i + 1], payloads[i + 2], - std::stoi(payloads[i + 3]), - api::interaction::to_type(payloads[i + 4]), - api::interaction::to_status(payloads[i + 5])}); - conversation.interactions.emplace(std::stoull(payloads[i]), std::move(msg)); - conversation.lastMessageUid = std::stoull(payloads[i]); - } - } -} - -int -addMessageToConversation(Database& db, - const std::string& accountProfile, - const std::string& conversationId, - const api::interaction::Info& msg) -{ - return db.insertInto("interactions", - {{":account_id", "account_id"}, {":author_id", "author_id"}, - {":conversation_id", "conversation_id"}, {":timestamp", "timestamp"}, - {":body", "body"}, {":type", "type"}, - {":status", "status"}}, - {{":account_id", accountProfile}, {":author_id", msg.authorUri}, - {":conversation_id", conversationId}, - {":timestamp", std::to_string(msg.timestamp)}, - {":body", msg.body}, {":type", to_string(msg.type)}, - {":status", to_string(msg.status)}}); -} - -int -addDataTransferToConversation(Database& db, - const std::string& accountProfileId, - const std::string& conversationId, - const api::datatransfer::Info& infoFromDaemon) -{ - auto peerProfileId = getProfileId(db, infoFromDaemon.accountId, "false", - infoFromDaemon.peerUri); - - return db.insertInto("interactions", { - {":account_id", "account_id"}, - {":author_id", "author_id"}, - {":conversation_id", "conversation_id"}, - {":timestamp", "timestamp"}, - {":body", "body"}, - {":type", "type"}, - {":status", "status"} - }, { - {":account_id", accountProfileId}, - {":author_id", infoFromDaemon.isOutgoing? accountProfileId : peerProfileId}, - {":conversation_id", conversationId}, - {":timestamp", std::to_string(std::time(nullptr))}, - {":body", infoFromDaemon.path}, - {":type", infoFromDaemon.isOutgoing ? - "OUTGOING_DATA_TRANSFER" : - "INCOMING_DATA_TRANSFER"}, - {":status", "TRANSFER_CREATED"} - }); -} - -int -addOrUpdateMessage(Database& db, - const std::string& accountProfile, - 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", - "daemon_id=:daemon_id", - {{":daemon_id", daemonId}}); - if (msgAlreadyExists.payloads.empty()) { - return db.insertInto("interactions", - {{":account_id", "account_id"}, {":author_id", "author_id"}, - {":conversation_id", "conversation_id"}, {":timestamp", "timestamp"}, - {":body", "body"}, {":type", "type"}, {":daemon_id", "daemon_id"}, - {":status", "status"}}, - {{":account_id", accountProfile}, {":author_id", msg.authorUri}, - {":conversation_id", conversationId}, - {":timestamp", std::to_string(msg.timestamp)}, - {":body", msg.body}, {":type", to_string(msg.type)}, {":daemon_id", daemonId}, - {":status", to_string(msg.status)}}); - } else { - // already exists - db.update("interactions", - "body=:body", - {{":body", msg.body}}, - "daemon_id=:daemon_id", - {{":daemon_id", daemonId}}); - return std::stoi(msgAlreadyExists.payloads[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& id) -{ - auto ids = db.select("id", "interactions", "daemon_id=:daemon_id", {{":daemon_id", id}}).payloads; - return ids.empty() ? "" : ids[0]; -} - -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)}}); -} - -std::string -conversationIdFromInteractionId(Database& db, unsigned int interactionId) -{ - auto result = db.select("conversation_id", - "interactions", - "id=:interaction_id", - {{":interaction_id", std::to_string(interactionId)}}); - if (result.nbrOfCols == 1) { - auto payloads = result.payloads; - return payloads[0]; - } - - return {}; -} - -void clearHistory(Database& db, - const std::string& conversationId) -{ - db.deleteFrom("interactions", "conversation_id=:id", {{":id", conversationId}}); -} - -void clearInteractionFromConversation(Database& db, - const std::string& conversationId, - const uint64_t& interactionId) -{ - db.deleteFrom("interactions", "conversation_id=:conv_id AND id=:int_id", - {{":conv_id", conversationId}, {":int_id", std::to_string(interactionId)}}); -} - -void clearAllHistoryFor(Database& db, const std::string& accountId) -{ - auto profileId = getProfileId(db, accountId, "true"); - - if (profileId.empty()) - return; - - db.deleteFrom("interactions", "account_id=:account_id", {{":account_id", profileId}}); -} - -void -removeContact(Database& db, const std::string& contactUri, const std::string& accountId) -{ - // Get profile for contact - auto contactId = getProfileId(db, accountId, "false", contactUri); - if (contactId.empty()) return; // No profile - auto accountProfileId = getProfileId(db, accountId, "true"); - // Get common conversations - auto conversations = getConversationsBetween(db, accountProfileId, contactId); - // 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", {{":id", conversationId}}); - } - // Get conversations for this contact. - conversations = getConversationsForProfile(db, contactId); - if (conversations.empty()) { - // Delete profile - db.deleteFrom("profiles_accounts", - "profile_id=:profile_id AND account_id=:account_id AND is_account=:is_account", - {{":profile_id", contactId}, - {":account_id", accountId}, - {":is_account", "false"}}); - if (profileCouldBeRemoved(db, contactId)) - db.deleteFrom("profiles", "id=:id", {{":id", contactId}}); - } -} - -void -removeAccount(Database& db, const std::string& accountId) -{ - auto accountProfileId = database::getProfileId(db, accountId, "true"); - auto conversationsForAccount = getConversationsForProfile(db, accountProfileId); - for (const auto& convId: conversationsForAccount) { - auto peers = getPeerParticipantsForConversation(db, accountProfileId, convId); - db.deleteFrom("conversations", "id=:id", {{":id", convId}}); - db.deleteFrom("interactions", "conversation_id=:id", {{":id", convId}}); - for (const auto& peerId: peers) { - auto otherConversationsForProfile = getConversationsForProfile(db, peerId); - if (otherConversationsForProfile.empty()) { - db.deleteFrom("profiles_accounts", - "profile_id=:profile_id AND account_id=:account_id AND is_account=:is_account", - {{":profile_id", peerId}, - {":account_id", accountId}, - {":is_account", "false"}}); - if (profileCouldBeRemoved(db, peerId)) { - db.deleteFrom("profiles", "id=:id", {{":id", peerId}}); - } - } - } - } - db.deleteFrom("profiles_accounts", - "profile_id=:profile_id AND account_id=:account_id AND is_account=:is_account", - {{":profile_id", accountProfileId}, - {":account_id", accountId}, - {":is_account", "true"}}); - db.deleteFrom("profiles", "id=:id", {{":id", accountProfileId}}); -} - -void -addContact(Database& db, const std::string& contactUri, const std::string& accountId) -{ - // Get profile for contact - auto row = getOrInsertProfile(db, contactUri, accountId, false, "", ""); - if (row.empty()) { - qDebug() << "database::addContact, no profile for contact. abort"; - return; - } - // Get profile of the account linked - auto accountProfileId = getProfileId(db, accountId, "true"); - // Get if conversation exists - auto common = getConversationsBetween(db, accountProfileId, row); - if (common.empty()) { - // conversations doesn't exists, start it. - beginConversationsBetween(db, accountProfileId, row); - } -} - -int -countUnreadFromInteractions(Database& db, const std::string& conversationId) -{ - return db.count("status", "interactions", "status=:status AND conversation_id=:id", - {{":status", "UNREAD"}, {":id", conversationId}}); -} - -void -deleteObsoleteHistory(Database& db, long int date) -{ - db.deleteFrom("interactions", "timestamp<=:date", {{":date", std::to_string(date)}}); -} - -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() << "database::getLastTimestamp, stoull throws an out_of_range exception: " << e.what(); - } catch (const std::invalid_argument& e) { - qDebug() << "database::getLastTimestamp, stoull throws an invalid_argument exception: " << e.what(); - } - return result; -} - -} // namespace database - -} // namespace authority - -} // namespace lrc diff --git a/src/authority/storagehelper.cpp b/src/authority/storagehelper.cpp new file mode 100644 index 00000000..2cd21563 --- /dev/null +++ b/src/authority/storagehelper.cpp @@ -0,0 +1,1283 @@ +/**************************************************************************** + * 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 <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; + if (isPeer) { + filePath = accountLocalPath + "profiles/" + QString::fromStdString(profileInfo.uri) + ".vcf"; + } else { + filePath = accountLocalPath + "profile" + ".vcf"; + } + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "Can't open file: " << filePath; + return; + } + QTextStream(&file) << 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); + 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 filePath; + filePath = accountLocalPath + "profiles/" + QString::fromStdString(peer_uri) + ".vcf"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Can't open file: " << filePath; + return { profileInfo, "", true, false }; + } + QTextStream in(&file); + 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}}); + } +} + +void +removeAccount(const std::string& accountId) +{ + auto accountLocalPath = getPath() + QString::fromStdString(accountId) + "/"; + QDir dir(accountLocalPath); + dir.removeRecursively(); +} + +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(); + } + + // first check if there is any legacy data storage to migrate + { + if (not QFile(dataDir.absoluteFilePath("ring.db")).exists() && + not QDir(dataDir.absoluteFilePath("text/")).exists() && + not QDir(dataDir.absoluteFilePath("profiles/")).exists() && + not QDir(dataDir.absoluteFilePath("peer_profiles/")).exists()) { + qDebug() << "No migration required"; + return dbs; + } + } + + // A fairly long migration may now occur + std::thread migrateThread( + [&appPath, &accountIds, &dbs, &didMigrateCb, &dataDir] { + // 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"); + } + } + + // clean out any potentially failed previous migration attempts thwarted + // by client termination before starting the migration process + for (auto accountId : accountIds) { + QFile(appPath + accountId + "/history.db").remove(); + QFile(appPath + accountId + "/history.db-journal").remove(); + QFile(appPath + accountId + "/profile.vcf").remove(); + QDir(appPath + accountId + "/profiles/").removeRecursively(); + } + + // 3. migrate db version 1.1 -> per account dbs version 1 + int index = 0; + for (auto accountId : accountIds) { + qDebug() << "Migrating account: " << accountId << "..."; + try { + 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); + } catch (const std::runtime_error& e) { + qWarning().noquote() + << "Could not migrate database for account: " + << accountId << "\n " << e.what(); + } + } + + // 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 diff --git a/src/authority/databasehelper.h b/src/authority/storagehelper.h similarity index 50% rename from src/authority/databasehelper.h rename to src/authority/storagehelper.h index fec5f20f..90444a1e 100644 --- a/src/authority/databasehelper.h +++ b/src/authority/storagehelper.h @@ -1,8 +1,9 @@ /**************************************************************************** - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * + * 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 * @@ -37,96 +38,114 @@ struct Info; namespace authority { -namespace database +namespace storage { /** - * Get id from database for a given uri - * @param db - * @param accountId - * @param isAccount - * @param uri - * @return the id - * @note "" if no id - */ -std::string getProfileId(Database& db, - const std::string& accountId, - const std::string& isAccount, - const std::string& uri=""); - - /** - * Get id for a profile. If the profile doesn't exist, create it. - * @param db - * @param contactUri - * @param accountId - * @param isAccount - * @param alias - * @param avatar - * @return the id + * Get the base path for the application's local storage + * @return local storage path + */ +QString getPath(); + +/** + * Get a formatted for local storage + * @param uri that may have a scheme prefixed + * @param type of account for which to transform the uri + * @return formatted uri + */ +std::string prepareUri(const std::string& uri, api::profile::Type type); + +/** + * Get a formatted string for a call interaction's body + * @param author_uri + * @param duration of the call + * @return the formatted and translated call message string + */ +std::string +getCallInteractionString(const std::string& authorUri, + const std::time_t& duration); + +/** + * Get a formatted string for a contact interaction's body + * @param author_uri + * @param status + * @return the formatted and translated call message string + */ +std::string +getContactInteractionString(const std::string& authorUri, + const api::interaction::Status& status); + +namespace vcard +{ + +/** + * Build the vCard for a profile + * @param profileInfo + * @param compressImage + * @return vcard string of the profile + */ +std::string profileToVcard(const api::profile::Info& profileInfo, + bool compressImage = false); + +} // namespace vcard + +/** + * @param duration + * @return a human readable call duration (M:ss) */ - std::string getOrInsertProfile(Database& db, - const std::string& contactUri, - const std::string& accountId, - bool isAccount, - const std::string& type, - const std::string& alias = "", - const std::string& avatar = ""); +std::string getFormattedCallDuration(const std::time_t duration); /** - * Get conversations for a given profile. + * Get all conversations with a given participant's URI * @param db - * @param profileId + * @param participant_uri */ -std::vector<std::string> getConversationsForProfile(Database& db, - const std::string& profileId); +std::vector<std::string> getConversationsWithPeer(Database& db, + const std::string& participant_uri); /** - * Get peer participant for a conversation linked to a profile. + * Get all peer participant(s) URIs for a given conversation id * @param db - * @param profileId * @param conversationId - * @note we don't verify if profileId is in the conversation */ std::vector<std::string> getPeerParticipantsForConversation(Database& db, - const std::string& profileId, const std::string& conversationId); /** - * @param db - * @param profileId - * @return the avatar in the database for a profile - */ -std::string getAvatarForProfileId(Database& db, const std::string& profileId); - -/** - * Check if the profile could be removed - * @param db - * @param profileId + * Creates or updates a contact or account vCard file with profile data. + * @param accountId + * @param profileInfo the contact info containing peer profile information + * @param isPeer indicates that a the profileInfo is that of a peer */ -bool profileCouldBeRemoved(Database& db, const std::string& profileId); +void createOrUpdateProfile(const std::string& accountId, + const api::profile::Info& profileInfo, + const bool isPeer = false); /** - * @param db - * @param profileId - * @param avatar + * Gets the account's avatar from the profile.vcf file + * @param accountId + * @return the account's base64 avatar */ -void setAvatarForProfileId(Database& db, const std::string& profileId, const std::string& avatar); +std::string +getAccountAvatar(const std::string& accountId); /** - * @param db - * @param profileId - * @return the alias in the database for a profile + * Build a contact info struct from a vCard + * @param accountId + * @param peer_uri + * @param type of contact to build + * @return the contact info containing peer profile information */ -std::string getAliasForProfileId(Database& db, const std::string& profileId); +api::contact::Info buildContactFromProfile(const std::string & accountId, + const std::string& peer_uri, + const api::profile::Type& type); /** - * @param db - * @param profileId - * @param alias + * Get all conversations for an account in the database. + * @param db + * @return conversations id for all conversations */ -void setAliasForProfileId(Database& db, const std::string& profileId, const std::string& alias); - -api::contact::Info buildContactFromProfileId(Database& db, const std::string& profileId); +std::vector<std::string> getAllConversations(Database& db); /** * Get conversations shared between an account and a contact. @@ -143,15 +162,13 @@ std::vector<std::string> getConversationsBetween(Database& db, * Start a conversation between account and contact. Creates an entry in the conversations table * and an entry in the interactions table. * @param db - * @param accountProfile the id of the account in the database - * @param contactProfile the id of the contact in the database - * @param firstMessage the body of the first message + * @param peer_uri the URI of the peer + * @param isOutgoing * @return conversation_id of the new conversation. */ -std::string beginConversationsBetween(Database& db, - const std::string& accountProfile, - const std::string& contactProfile, - const std::string& firstMessage = ""); +std::string beginConversationWithPeer(Database& db, + const std::string& peer_uri, + const bool isOutgoing = true); /** * Return interactions from a conversation @@ -163,31 +180,38 @@ void getHistory(Database& db, api::conversation::Info& conversation); /** * Add an entry into interactions linked to a conversation. * @param db - * @param accountProfile * @param conversationId * @param msg * @return the id of the inserted interaction */ int addMessageToConversation(Database& db, - const std::string& accountProfile, - const std::string& conversationId, - const api::interaction::Info& msg); + const std::string& conversationId, + const api::interaction::Info& msg); /** * Add or update an entry into interactions linked to a conversation. * @param db -* @param accountProfile * @param conversationId * @param msg * @param daemonId * @return the id of the inserted interaction */ int addOrUpdateMessage(Database& db, - const std::string& accountProfile, const std::string& conversationId, const api::interaction::Info& msg, const std::string& daemonId); +/** +* Add a data transfer entry into interactions linked to a conversation. +* @param db +* @param conversationId +* @param daemonId +* @return the id of the inserted interaction +*/ +int addDataTransferToConversation(Database& db, + const std::string& conversationId, + const api::datatransfer::Info& infoFromDaemon); + /** * Change the daemon_id column for an interaction * @param db @@ -206,11 +230,22 @@ void addDaemonMsgId(Database& db, std::string getDaemonIdByInteractionId(Database& db, const std::string& id); /** + * Obtain the id of an interaction of a given daemon_id * @param db * @param id * @return the interaction id for a daemon id else an empty string */ -std::string getInteractionIdByDaemonId(Database& db, const std::string& id); +std::string getInteractionIdByDaemonId(Database& db, const std::string& daemon_id); + +/** + * Obtain the extra_data column of an interaction of a given id + * @note if a key is provided and exists, the value will be returned + * @param db + * @param id + * @param key + */ +std::string getInteractionExtraDataById(Database& db, const std::string& id, + const std::string& key = {}); /** * Change the body of an interaction @@ -226,10 +261,18 @@ void updateInteractionBody(Database& db, unsigned int id, * @param db * @param id * @param newStatus + * @param isRead */ void updateInteractionStatus(Database& db, unsigned int id, api::interaction::Status newStatus); +/** + * Set interaction to the read state + * @param db + * @param id + */ +void setInteractionRead(Database& db, unsigned int id); + /** * Clear history but not the conversation started interaction * @param db @@ -249,53 +292,45 @@ void clearInteractionFromConversation(Database& db, const uint64_t& interactionId); /** - * Clear all history stored in the database for the account uri + * Clear all history stored in the interactions table of the database * @param db - * @param accountId */ -void clearAllHistoryFor(Database& db, const std::string& accountId); +void clearAllHistory(Database& db); /** - * delete obsolete histori from the database + * delete obsolete history from the database * @param db * @param date in second since epoch. Below this date, interactions will be deleted */ void deleteObsoleteHistory(Database& db, long int date); /** - * Remove a conversation between an account and a contact. Remove corresponding entries in - * the conversations table and profiles if the profile is not present in conversations. + * Remove all conversation with a contact. Remove corresponding entries in + * the conversations table. * @param db * @param contactUri - * @param accountId */ -void removeContact(Database& db, const std::string& contactUri, const std::string& accountId); +void removeContact(Database& db, const std::string& contactUri); /** - * Remove from conversations and profiles linked to an account. - * @param db + * Ensure that all files located in + * {local_storage}/jami/{accountId} are removed * @param accountId */ -void removeAccount(Database& db, const std::string& accountId); +void removeAccount(const std::string& accountId); /** - * insert into profiles and conversations. + * count number of 'UNREAD' from 'interactions' table. * @param db - * @param contactUri - * @param accountId + * @param conversationId */ -void addContact(Database& db, const std::string& contactUri, const std::string& accountId); +int countUnreadFromInteractions(Database& db, const std::string& conversationId); /** - * count number of 'UNREAD' from 'interactions' table. + * Retrieve an interaction's conversation id + * @param db + * @param conversationId */ -int countUnreadFromInteractions(Database& db, const std::string& conversationId); - -int addDataTransferToConversation(Database& db, - const std::string& accountProfileId, - const std::string& conversationId, - const api::datatransfer::Info& infoFromDaemon); - std::string conversationIdFromInteractionId(Database& db, unsigned int interactionId); /** @@ -305,7 +340,62 @@ std::string conversationIdFromInteractionId(Database& db, unsigned int interacti */ uint64_t getLastTimestamp(Database& db); -} // namespace database +/** + * JSON parsing functions intended for use with the + * extra_data columns(conversations and interactions) + */ +namespace { +/** + * Build a string from a QJsonObject + * @param json + * @return a JSON as a QString + */ +QString stringFromJSON(const QJsonObject& json); + +/** + * Build a QJsonObject from a QString + * @param str + * @return a JSON object + */ +QJsonObject JSONFromString(const QString& str); + +/** + * Build a string from an initializer list of key/value pairs + * @param args + * @return a JSON as a QString + */ +QString JSONStringFromInitList(const std::initializer_list<QPair<QString, QJsonValue> > args); + +/** + * Get the value at a key from a JSON object + * @param json + * @param key + * @return the value as a QString + */ +QString readJSONValue(const QJsonObject& json, const QString& key); + +/** + * Store a value at a key in a JSON object + * @param json + * @param key + * @param value + */ +void writeJSONValue(QJsonObject& json, const QString& key, const QString& value); +} + +/** + * Retrieve a list of account database via a migration + * procedure from the legacy "ring.db", if it exists + * @param accountIds of the accounts to attempt migration upon + * @param willMigrateCb to invoke when migration will occur + * @param didMigrateCb to invoke when migration has completed + */ +std::vector<std::shared_ptr<Database>> +migrateIfNeeded(const QStringList& accountIds, + MigrationCb& willMigrateCb, + MigrationCb& didMigrateCb); + +} // namespace storage } // namespace authority diff --git a/src/avmodel.cpp b/src/avmodel.cpp index cb42fab8..f30a727c 100644 --- a/src/avmodel.cpp +++ b/src/avmodel.cpp @@ -42,7 +42,7 @@ #include "dbus/callmanager.h" #include "dbus/configurationmanager.h" #include "dbus/videomanager.h" -#include "database.h" +#include "authority/storagehelper.h" namespace lrc { @@ -663,7 +663,7 @@ AVModelPimpl::getRecordingPath() const #if defined(_WIN32) || defined(__APPLE__) const QDir dir = QString::fromStdString(linked_.getRecordPath()) + "/" + recorderSavesSubdir.c_str(); #else - const QDir dir = lrc::Database::getPath() + "/" + recorderSavesSubdir.c_str(); + const QDir dir = authority::storage::getPath() + "/" + recorderSavesSubdir.c_str(); #endif dir.mkpath("."); diff --git a/src/callbackshandler.cpp b/src/callbackshandler.cpp index 60c169d0..fbda0d06 100644 --- a/src/callbackshandler.cpp +++ b/src/callbackshandler.cpp @@ -187,12 +187,6 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent) &CallbacksHandler::slotMigrationEnded, Qt::QueuedConnection); - connect(&ConfigurationManager::instance(), - &ConfigurationManagerInterface::debugMessageReceived, - this, - &CallbacksHandler::slotDebugMessageReceived, - Qt::QueuedConnection); - connect(&VideoManager::instance(), &VideoManagerInterface::startedDecoding, this, @@ -220,7 +214,16 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent) CallbacksHandler::~CallbacksHandler() { +} +void +CallbacksHandler::subscribeToDebugReceived() +{ + connect(&ConfigurationManager::instance(), + &ConfigurationManagerInterface::debugMessageReceived, + this, + &CallbacksHandler::slotDebugMessageReceived, + Qt::QueuedConnection); } void diff --git a/src/callbackshandler.h b/src/callbackshandler.h index bf096fc1..4f38a306 100644 --- a/src/callbackshandler.h +++ b/src/callbackshandler.h @@ -51,6 +51,10 @@ public: CallbacksHandler(const api::Lrc& parent); ~CallbacksHandler(); + // This connection relies on the behavior controller + // and needs to be made after the lrc object is constructed + void subscribeToDebugReceived(); + Q_SIGNALS: /** * Connect this signal to get incoming text interaction from the DHT. diff --git a/src/contactmodel.cpp b/src/contactmodel.cpp index 71578a43..3116b525 100644 --- a/src/contactmodel.cpp +++ b/src/contactmodel.cpp @@ -1,10 +1,11 @@ /**************************************************************************** - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * + * 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: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> * * Author: Hugo Lefeuvre <hugo.lefeuvre@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 * @@ -20,6 +21,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * ***************************************************************************/ +#include "api/contactmodel.h" + // Std #include <algorithm> #include <mutex> @@ -28,7 +31,6 @@ #include <account_const.h> // LRC -#include "api/contactmodel.h" #include "api/account.h" #include "api/contact.h" #include "api/interaction.h" @@ -41,7 +43,7 @@ #include "vcard.h" #include "authority/daemon.h" -#include "authority/databasehelper.h" +#include "authority/storagehelper.h" // Dbus #include "dbus/configurationmanager.h" @@ -64,16 +66,16 @@ public: ~ContactModelPimpl(); /** - * Fills with contacts based on database's requests + * Fills the contacts based on database's conversations * @return if the method succeeds */ - bool fillsWithSIPContacts(); + bool fillWithSIPContacts(); /** - * Fills with contacts based on daemon's requests + * Fills the contacts based on database's conversations * @return if the method succeeds */ - bool fillsWithRINGContacts(); + bool fillWithJamiContacts(); /** * Add a contact::Info to contacts. @@ -174,10 +176,12 @@ public Q_SLOTS: using namespace authority; -ContactModel::ContactModel(const account::Info& owner, Database& database, const CallbacksHandler& callbacksHandler, const BehaviorController& behaviorController) -: QObject() -, owner(owner) -, pimpl_(std::make_unique<ContactModelPimpl>(*this, database, callbacksHandler, behaviorController)) +ContactModel::ContactModel(const account::Info& owner, + Database& db, + const CallbacksHandler& callbacksHandler, + const BehaviorController& behaviorController) +: owner(owner) +, pimpl_(std::make_unique<ContactModelPimpl>(*this, db, callbacksHandler, behaviorController)) { } @@ -226,9 +230,9 @@ ContactModel::addContact(contact::Info contactInfo) } if ((owner.profileInfo.type != profile.type) and - (profile.type == profile::Type::RING or profile.type == profile::Type::SIP)) { - qDebug() << "ContactModel::addContact, types invalids."; - return; + (profile.type == profile::Type::RING or profile.type == profile::Type::SIP)) { + qDebug() << "ContactModel::addContact, types invalids."; + return; } MapStringString details = ConfigurationManager::instance().getContactDetails( @@ -258,13 +262,13 @@ ContactModel::addContact(contact::Info contactInfo) case profile::Type::SIP: break; case profile::Type::INVALID: + case profile::Type::COUNT__: default: qDebug() << "ContactModel::addContact, cannot add contact with invalid type."; return; } - database::getOrInsertProfile(pimpl_->db, profile.uri, owner.id, false, - to_string(owner.profileInfo.type),profile.alias, profile.avatar); + storage::createOrUpdateProfile(owner.id, profile, true); { std::lock_guard<std::mutex> lk(pimpl_->contactsMtx_); @@ -299,13 +303,13 @@ ContactModel::removeContact(const std::string& contactUri, bool banned) return; } pimpl_->contacts.erase(contactUri); - database::removeContact(pimpl_->db, contactUri, owner.id); + storage::removeContact(pimpl_->db, contactUri); emitContactRemoved = true; } else if (owner.profileInfo.type == profile::Type::SIP) { // Remove contact from db pimpl_->contacts.erase(contactUri); - database::removeContact(pimpl_->db, contactUri, owner.id); + storage::removeContact(pimpl_->db, contactUri); emitContactRemoved = true; } } @@ -336,18 +340,6 @@ ContactModel::getBannedContacts() const return pimpl_->bannedContacts; } -const std::string -ContactModel::getProfileId(const std::string& uri, bool isAccount) const -{ - return database::getProfileId(pimpl_->db, pimpl_->linked.owner.id, isAccount ? "true" : "false", uri); -} - -const std::string -ContactModel::getContactProfileId(const std::string& contactUri) const -{ - return getProfileId(contactUri, false); -} - void ContactModel::searchContact(const std::string& query) { @@ -442,7 +434,6 @@ ContactModel::sendDhtMessage(const std::string& contactUri, const std::string& b return msgId; } - ContactModelPimpl::ContactModelPimpl(const ContactModel& linked, Database& db, const CallbacksHandler& callbacksHandler, @@ -454,9 +445,9 @@ ContactModelPimpl::ContactModelPimpl(const ContactModel& linked, { // Init contacts map if (linked.owner.profileInfo.type == profile::Type::SIP) - fillsWithSIPContacts(); + fillWithSIPContacts(); else - fillsWithRINGContacts(); + fillWithJamiContacts(); // connect the signals connect(&callbacksHandler, &CallbacksHandler::newBuddySubscription, @@ -498,15 +489,16 @@ ContactModelPimpl::~ContactModelPimpl() } bool -ContactModelPimpl::fillsWithSIPContacts() +ContactModelPimpl::fillWithSIPContacts() { - auto accountProfileId = database::getProfileId(db, linked.owner.id, "true", linked.owner.profileInfo.uri); - auto conversationsForAccount = database::getConversationsForProfile(db, accountProfileId); - for (const auto& c : conversationsForAccount) { - auto otherParticipants = database::getPeerParticipantsForConversation(db, accountProfileId, c); + auto conversationsForAccount = storage::getAllConversations(db); + for (const auto& convId : conversationsForAccount) { + auto otherParticipants = storage::getPeerParticipantsForConversation(db, convId); for (const auto& participant: otherParticipants) { // for each conversations get the other profile id - auto contactInfo = database::buildContactFromProfileId(db, participant); + auto contactInfo = storage::buildContactFromProfile(linked.owner.id, + participant, + profile::Type::SIP); { std::lock_guard<std::mutex> lk(contactsMtx_); contacts.emplace(contactInfo.profileInfo.uri, contactInfo); @@ -518,7 +510,7 @@ ContactModelPimpl::fillsWithSIPContacts() } bool -ContactModelPimpl::fillsWithRINGContacts() { +ContactModelPimpl::fillWithJamiContacts() { // Add contacts from daemon const VectorMapStringString& contacts_vector = ConfigurationManager::instance().getContacts(linked.owner.id.c_str()); @@ -536,18 +528,19 @@ ContactModelPimpl::fillsWithRINGContacts() { auto contactUri = tr_info[DRing::Account::TrustRequest::FROM]; + auto contactInfo = storage::buildContactFromProfile(linked.owner.id, + contactUri.toStdString(), + profile::Type::PENDING); + const auto vCard = lrc::vCard::utils::toHashMap(payload); const auto alias = vCard["FN"]; - const auto photo = (vCard.find("PHOTO;ENCODING=BASE64;TYPE=PNG") == vCard.end()) ? - vCard["PHOTO;ENCODING=BASE64;TYPE=JPEG"] : vCard["PHOTO;ENCODING=BASE64;TYPE=PNG"]; - - lrc::api::profile::Info profileInfo; - profileInfo.uri = contactUri.toStdString(); - profileInfo.avatar = photo.toStdString(); - profileInfo.alias = alias.toStdString(); - profileInfo.type = profile::Type::PENDING; - contact::Info contactInfo; - contactInfo.profileInfo = profileInfo; + const auto photo = (vCard.find("PHOTO;ENCODING=BASE64;TYPE=PNG") != vCard.end()) ? + vCard["PHOTO;ENCODING=BASE64;TYPE=PNG"] : + vCard["PHOTO;ENCODING=BASE64;TYPE=JPEG"]; + + contactInfo.profileInfo.type = profile::Type::PENDING; + if (!alias.isEmpty()) contactInfo.profileInfo.alias = alias.constData(); + if (!photo.isEmpty()) contactInfo.profileInfo.avatar = photo.constData(); contactInfo.registeredName = ""; contactInfo.isBanned = false; @@ -556,13 +549,15 @@ ContactModelPimpl::fillsWithRINGContacts() { contacts.emplace(contactUri.toStdString(), contactInfo); } - database::getOrInsertProfile(db, contactUri.toStdString(), linked.owner.id, false, - profile::to_string(profile::Type::RING), alias.toStdString(), photo.toStdString()); + // create profile vcard for contact + storage::createOrUpdateProfile(linked.owner.id, contactInfo.profileInfo, true); } // Update presence // TODO fix this map. This is dumb for now. The map contains values as keys, and empty values. - const VectorMapStringString& subscriptions {PresenceManager::instance().getSubscriptions(linked.owner.id.c_str())}; + const VectorMapStringString& subscriptions { + PresenceManager::instance().getSubscriptions(linked.owner.id.c_str()) + }; for (const auto& subscription : subscriptions) { auto first = true; std::string uri = ""; @@ -678,7 +673,7 @@ ContactModelPimpl::slotContactRemoved(const std::string& accountId, const std::s bannedContacts.erase(it); } } - database::removeContact(db, contactUri, accountId); + storage::removeContact(db, contactUri); contacts.erase(contactUri); } } @@ -693,20 +688,20 @@ ContactModelPimpl::slotContactRemoved(const std::string& accountId, const std::s } void -ContactModelPimpl::addToContacts(const std::string& contactId, const profile::Type& type, bool banned) +ContactModelPimpl::addToContacts(const std::string& contactUri, const profile::Type& type, bool banned) { - auto profileId = database::getOrInsertProfile(db, contactId, linked.owner.id, - false, to_string(linked.owner.profileInfo.type),"", ""); + // create a vcard if necessary + profile::Info profileInfo{ contactUri, {}, {}, linked.owner.profileInfo.type }; + storage::createOrUpdateProfile(linked.owner.id, profileInfo, true); - auto contactInfo = database::buildContactFromProfileId(db, profileId); + auto contactInfo = storage::buildContactFromProfile(linked.owner.id, contactUri, type); contactInfo.isBanned = banned; - contactInfo.profileInfo.type = type; // PENDING should not be stored in the database // lookup address in case of RING contact if (type == profile::Type::RING) { ConfigurationManager::instance().lookupAddress(QString::fromStdString(linked.owner.id), - "", QString::fromStdString(contactId)); - PresenceManager::instance().subscribeBuddy(linked.owner.id.c_str(), contactId.c_str(), !banned); + "", QString::fromStdString(contactUri)); + PresenceManager::instance().subscribeBuddy(linked.owner.id.c_str(), contactUri.c_str(), !banned); } contactInfo.profileInfo.type = type; // Because PENDING should not be stored in the database @@ -719,7 +714,7 @@ ContactModelPimpl::addToContacts(const std::string& contactId, const profile::Ty contacts.emplace_hint(iter, contactInfo.profileInfo.uri, contactInfo); if (banned) { - bannedContacts.emplace_back(contactId); + bannedContacts.emplace_back(contactUri); } } @@ -760,7 +755,7 @@ ContactModelPimpl::slotRegisteredNameFound(const std::string& accountId, updateTemporaryMessage(tr("Invalid ID").toStdString(), registeredName); break; case 2 /* NOT FOUND */: - updateTemporaryMessage(tr("Not found").toStdString(), registeredName); + updateTemporaryMessage(tr("Registered name not found").toStdString(), registeredName); break; case 3 /* ERROR */: updateTemporaryMessage(tr("Couldn't lookup…").toStdString(), registeredName); @@ -792,8 +787,7 @@ ContactModelPimpl::slotIncomingContactRequest(const std::string& accountId, auto contactInfo = contact::Info {profileInfo, "", false, false, false}; contacts.emplace(contactUri, contactInfo); emitTrust = true; - database::getOrInsertProfile(db, contactUri, accountId, false, - profile::to_string(profile::Type::RING), alias.toStdString(), photo.toStdString()); + storage::createOrUpdateProfile(accountId, profileInfo, true); } } diff --git a/src/conversationmodel.cpp b/src/conversationmodel.cpp index 1f2de475..918e1b71 100644 --- a/src/conversationmodel.cpp +++ b/src/conversationmodel.cpp @@ -45,7 +45,7 @@ #include "api/datatransfer.h" #include "api/datatransfermodel.h" #include "callbackshandler.h" -#include "authority/databasehelper.h" +#include "authority/storagehelper.h" #include "uri.h" @@ -105,19 +105,19 @@ public: /** * Add call interaction for conversation with callId * @param callId - * @param body + * @param duration */ - void addOrUpdateCallMessage(const std::string& callId, const std::string& body); + void addOrUpdateCallMessage(const std::string& callId, + const std::string& from = {}, + const std::time_t& duration = -1); /** * Add a new message from a peer in the database - * @param from the peer uri + * @param from the author uri * @param body the content of the message - * @param authorProfileId override the author of the message (if empty it's from) * @param timestamp the timestamp of the message */ void addIncomingMessage(const std::string& from, const std::string& body, - const std::string& authorProfileId="", const uint64_t& timestamp = 0); /** * Change the status of an interaction. Listen from callbacksHandler @@ -163,7 +163,6 @@ public: Lrc& lrc; Database& db; const CallbacksHandler& callbacksHandler; - const std::string accountProfileId; const BehaviorController& behaviorController; ConversationModel::ConversationQueue conversations; ///< non-filtered conversations @@ -331,6 +330,7 @@ ConversationModel::allFilteredConversations() const if (pimpl_->typeFilter != profile::Type::PENDING) { // Remove pending contacts and get the temporary item if filter is not empty switch (contactInfo.profileInfo.type) { + case profile::Type::COUNT__: case profile::Type::INVALID: case profile::Type::PENDING: return false; @@ -391,7 +391,6 @@ ConversationModel::filteredConversation(const unsigned int row) const return conversation::Info(); auto conversationInfo = conversations.at(row); - conversationInfo.unreadMessages = pimpl_->getNumberOfUnreadMessagesFor(conversationInfo.uid); return conversationInfo; } @@ -502,7 +501,7 @@ ConversationModel::deleteObsoleteHistory(int days) auto currentTime = static_cast<long int>(std::time(nullptr)); // since epoch, in seconds... auto date = currentTime - (days * 86400); - database::deleteObsoleteHistory(pimpl_->db, date); + storage::deleteObsoleteHistory(pimpl_->db, date); } void @@ -547,14 +546,13 @@ ConversationModelPimpl::placeCall(const std::string& uid, bool isAudioOnly) } auto convId = uid; - auto accountId = accountProfileId; auto participant = conversation.participants.front(); bool isTemporary = participant.empty(); auto contactInfo = linked.owner.contactModel->getContact(participant); - auto url = contactInfo.profileInfo.uri; + auto uri = contactInfo.profileInfo.uri; - if (url.empty()) + if (uri.empty()) return; // Incorrect item // Don't call banned contact @@ -564,11 +562,11 @@ ConversationModelPimpl::placeCall(const std::string& uid, bool isAudioOnly) } if (linked.owner.profileInfo.type != profile::Type::SIP) { - url = "ring:" + url; // Add the ring: before or it will fail. + uri = "ring:" + uri; // Add the ring: before or it will fail. } auto cb = std::function<void(std::string)>( - [this, isTemporary, url, isAudioOnly, &conversation](std::string convId) { + [this, isTemporary, uri, isAudioOnly, &conversation](std::string convId) { int contactIndex; if (isTemporary && (contactIndex = indexOfContact(convId)) < 0) { qDebug() << "Can't place call: Other participant is not a contact (removed while placing call ?)"; @@ -578,7 +576,7 @@ ConversationModelPimpl::placeCall(const std::string& uid, bool isAudioOnly) auto& newConv = isTemporary ? conversations.at(contactIndex) : conversation; convId = newConv.uid; - newConv.callId = linked.owner.callModel->createCall(url, isAudioOnly); + newConv.callId = linked.owner.callModel->createCall(uri, isAudioOnly); if (newConv.callId.empty()) { qDebug() << "Can't place call (daemon side failure ?)"; return; @@ -636,7 +634,6 @@ ConversationModel::sendMessage(const std::string& uid, const std::string& body) } auto convId = uid; - auto accountId = pimpl_->accountProfileId; bool isTemporary = conversation.participants.front() == ""; /* Make a copy of participants list: if current conversation is temporary, @@ -644,7 +641,7 @@ ConversationModel::sendMessage(const std::string& uid, const std::string& body) const auto participants = conversation.participants; auto cb = std::function<void(std::string)>( - [this, accountId, isTemporary, body, &conversation](std::string convId) { + [this, isTemporary, body, &conversation](std::string convId) { /* Now we should be able to retrieve the final conversation, in case the previous one was temporary */ // FIXME potential race condition between index check and at() call @@ -682,14 +679,20 @@ ConversationModel::sendMessage(const std::string& uid, const std::string& body) } // Add interaction to database - auto msg = interaction::Info{ accountId, body, std::time(nullptr), - interaction::Type::TEXT, status }; - int msgId = database::addMessageToConversation(pimpl_->db, accountId, convId, msg); + interaction::Info msg { + {}, + body, std::time(nullptr), + 0, + interaction::Type::TEXT, + status, + true + }; + int msgId = storage::addMessageToConversation(pimpl_->db, convId, msg); // Update conversation if (status == interaction::Status::SENDING) { // Because the daemon already give an id for the message, we need to store it. - database::addDaemonMsgId(pimpl_->db, std::to_string(msgId), std::to_string(daemonMsgId)); + storage::addDaemonMsgId(pimpl_->db, std::to_string(msgId), std::to_string(daemonMsgId)); } bool ret = false; @@ -810,13 +813,13 @@ ConversationModel::clearHistory(const std::string& uid) auto& conversation = pimpl_->conversations.at(conversationIdx); // Remove all TEXT interactions from database - database::clearHistory(pimpl_->db, uid); + storage::clearHistory(pimpl_->db, uid); // Update conversation { std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[uid]); conversation.interactions.clear(); } - database::getHistory(pimpl_->db, conversation); // will contains "Conversation started" + storage::getHistory(pimpl_->db, conversation); // will contains "Conversation started" pimpl_->sortConversations(); emit modelSorted(); emit conversationCleared(uid); @@ -836,7 +839,7 @@ ConversationModel::clearInteractionFromConversation(const std::string& convId, c try { auto& conversation = pimpl_->conversations.at(conversationIdx); - database::clearInteractionFromConversation(pimpl_->db, convId, interactionId); + storage::clearInteractionFromConversation(pimpl_->db, convId, interactionId); erased_keys = conversation.interactions.erase(interactionId); if (conversation.lastMessageUid == interactionId) { @@ -886,13 +889,14 @@ ConversationModel::retryInteraction(const std::string& convId, const uint64_t& i return; // Do not retry non outgoing info if (it->second.type == interaction::Type::TEXT - || it->second.type == interaction::Type::OUTGOING_DATA_TRANSFER) { + || (it->second.type == interaction::Type::DATA_TRANSFER + && interaction::isOutgoing(it->second))) { body = it->second.body; interactionType = it->second.type; } else return; - database::clearInteractionFromConversation(pimpl_->db, convId, interactionId); + storage::clearInteractionFromConversation(pimpl_->db, convId, interactionId); conversation.interactions.erase(interactionId); } catch (const std::out_of_range& e) { qDebug() << "can't find interaction from conversation: " << e.what(); @@ -914,14 +918,14 @@ ConversationModel::retryInteraction(const std::string& convId, const uint64_t& i void ConversationModel::clearAllHistory() { - database::clearAllHistoryFor(pimpl_->db, owner.id); + storage::clearAllHistory(pimpl_->db); for (auto& conversation : pimpl_->conversations) { { std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[conversation.uid]); conversation.interactions.clear(); } - database::getHistory(pimpl_->db, conversation); + storage::getHistory(pimpl_->db, conversation); } pimpl_->sortConversations(); emit modelSorted(); @@ -937,21 +941,24 @@ ConversationModel::setInteractionRead(const std::string& convId, } bool emitUpdated = false; interaction::Info itCopy; - auto newStatus = interaction::Status::READ; { std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]); auto& interactions = pimpl_->conversations[conversationIdx].interactions; auto it = interactions.find(interactionId); if (it != interactions.end()) { emitUpdated = true; - if (it->second.status != interaction::Status::UNREAD) return; - it->second.status = newStatus; + if (it->second.isRead) { + return; + } + it->second.isRead = true; + if (pimpl_->conversations[conversationIdx].unreadMessages != 0) + pimpl_->conversations[conversationIdx].unreadMessages -= 1; itCopy = it->second; } } if (emitUpdated) { pimpl_->dirtyConversations = {true, true}; - database::updateInteractionStatus(pimpl_->db, interactionId, newStatus); + storage::setInteractionRead(pimpl_->db, interactionId); emit interactionStatusUpdated(convId, interactionId, itCopy); emit pimpl_->behaviorController.newReadInteraction(owner.id, convId, interactionId); } @@ -969,15 +976,15 @@ ConversationModel::clearUnreadInteractions(const std::string& convId) { auto& interactions = pimpl_->conversations[conversationIdx].interactions; std::for_each(interactions.begin(), interactions.end(), [&] (decltype(*interactions.begin())& it) { - if (it.second.type == lrc::api::interaction::Type::TEXT && - it.second.status == lrc::api::interaction::Status::UNREAD) { + if (!it.second.isRead) { emitUpdated = true; - it.second.status = interaction::Status::READ; - database::updateInteractionStatus(pimpl_->db, it.first, interaction::Status::READ); + it.second.isRead = true; + storage::setInteractionRead(pimpl_->db, it.first); } }); } if (emitUpdated) { + pimpl_->conversations[conversationIdx].unreadMessages = 0; pimpl_->dirtyConversations = {true, true}; emit conversationUpdated(convId); } @@ -994,7 +1001,6 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked, , callbacksHandler(callbacksHandler) , typeFilter(profile::Type::INVALID) , customTypeFilter(profile::Type::INVALID) -, accountProfileId(database::getProfileId(db, linked.owner.id, "true", linked.owner.profileInfo.uri)) , behaviorController(behaviorController) { initConversations(); @@ -1017,7 +1023,6 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked, connect(&callbacksHandler, &CallbacksHandler::accountMessageStatusChanged, this, &ConversationModelPimpl::slotUpdateInteractionStatus); - // Call related connect(&*linked.owner.callModel, &NewCallModel::newIncomingCall, this, &ConversationModelPimpl::slotIncomingCall); @@ -1102,7 +1107,6 @@ ConversationModelPimpl::~ConversationModelPimpl() disconnect(&callbacksHandler, &CallbacksHandler::accountMessageStatusChanged, this, &ConversationModelPimpl::slotUpdateInteractionStatus); - // Call related disconnect(&*linked.owner.callModel, &NewCallModel::newIncomingCall, this, &ConversationModelPimpl::slotIncomingCall); @@ -1148,30 +1152,18 @@ ConversationModelPimpl::initConversations() return; // Fill conversations - if (accountProfileId.empty()) { - // Should not, NewAccountModel must create this profile before. - qDebug() << "ConversationModelPimpl::initConversations(), account not in db"; - return; - } for (auto const& c : linked.owner.contactModel->getAllContacts()) { - auto contactProfileId = database::getProfileId(db, linked.owner.id, "false", - c.second.profileInfo.uri); - if (contactProfileId.empty()) { - // Should not, ContactModel must create profiles before. - qDebug() << "ConversationModelPimpl::initConversations(), contact not in db"; - continue; - } - auto common = database::getConversationsBetween(db, accountProfileId, contactProfileId); - if (common.empty()) { + auto conv = storage::getConversationsWithPeer(db, c.second.profileInfo.uri); + if (conv.empty()) { // Can't find a conversation with this contact. Start it. - auto newConversationsId = database::beginConversationsBetween(db, accountProfileId, contactProfileId); - common.emplace_back(std::move(newConversationsId)); + auto newConversationsId = storage::beginConversationWithPeer(db, c.second.profileInfo.uri, c.second.isTrusted); + conv.emplace_back(std::move(newConversationsId)); } - addConversationWith(common[0], c.first); + addConversationWith(conv[0], c.first); - auto convIdx = indexOf(common[0]); + auto convIdx = indexOf(conv[0]); // Check if file transfer interactions were left in an incorrect state std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]); @@ -1183,7 +1175,7 @@ ConversationModelPimpl::initConversations() || interaction.second.status == interaction::Status::TRANSFER_ACCEPTED) { // If a datatransfer was left in a non-terminal status in DB, we switch this status to ERROR // TODO : Improve for DBus clients as daemon and transfer may still be ongoing - database::updateInteractionStatus(db, interaction.first, interaction::Status::TRANSFER_ERROR); + storage::updateInteractionStatus(db, interaction.first, interaction::Status::TRANSFER_ERROR); interaction.second.status = interaction::Status::TRANSFER_ERROR; } } @@ -1196,13 +1188,16 @@ ConversationModelPimpl::initConversations() // Load all non treated messages for this account QVector<Message> messages = ConfigurationManager::instance().getLastMessages( linked.owner.id.c_str(), - database::getLastTimestamp(db)); + storage::getLastTimestamp(db) + ); for (const auto& message : messages) { uint64_t timestamp = 0; try { timestamp = static_cast<uint64_t>(message.received); } catch (...) {} - addIncomingMessage(message.from.toStdString(), message.payloads["text/plain"].toStdString(), "", timestamp); + addIncomingMessage(message.from.toStdString(), + message.payloads["text/plain"].toStdString(), + timestamp); } } @@ -1264,24 +1259,16 @@ void ConversationModelPimpl::slotContactAdded(const std::string& uri) { auto type = linked.owner.profileInfo.type; - std::string interaction = ""; try { auto contact = linked.owner.contactModel->getContact(uri); type = contact.profileInfo.type; - interaction = type == profile::Type::PENDING ? - QObject::tr("Invitation received").toStdString() : - QObject::tr("Contact added").toStdString(); } catch (...) {} - auto contactProfileId = database::getOrInsertProfile(db, uri, - linked.owner.id, false, to_string(type)); - auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId); + profile::Info profileInfo{ uri, {}, {}, type }; + storage::createOrUpdateProfile(linked.owner.id, profileInfo, true); + auto conv = storage::getConversationsWithPeer(db, uri); if (conv.empty()) { // pass conversation UID through only element - conv.emplace_back( - database::beginConversationsBetween(db, accountProfileId, - contactProfileId, interaction - ) - ); + conv.emplace_back(storage::beginConversationWithPeer(db, uri)); } // Add the conversation if not already here if (indexOf(conv[0]) == -1) { @@ -1306,37 +1293,36 @@ ConversationModelPimpl::slotPendingContactAccepted(const std::string& uri) try { type = linked.owner.contactModel->getContact(uri).profileInfo.type; } catch (std::out_of_range& e) {} - auto contactProfileId = database::getOrInsertProfile(db, uri, linked.owner.id, - false, to_string(type)); - auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId); - if (conv.empty()) { - conv.emplace_back( - database::beginConversationsBetween(db, accountProfileId, - contactProfileId, QObject::tr("Invitation accepted").toStdString() - ) - ); + profile::Info profileInfo{ uri, {}, {}, type }; + storage::createOrUpdateProfile(linked.owner.id, profileInfo, true); + auto convs = storage::getConversationsWithPeer(db, uri); + if (convs.empty()) { + convs.emplace_back(storage::beginConversationWithPeer(db, uri)); } else { try { auto contact = linked.owner.contactModel->getContact(uri); - auto msg = interaction::Info {accountProfileId, - QObject::tr("Invitation accepted").toStdString(), - std::time(nullptr), interaction::Type::CONTACT, - interaction::Status::SUCCEED}; - auto msgId = database::addMessageToConversation(db, accountProfileId, conv[0], msg); - auto convIdx = indexOf(conv[0]); + auto interaction = interaction::Info { uri, + {}, + std::time(nullptr), + 0, + interaction::Type::CONTACT, + interaction::Status::SUCCESS, + true }; + auto msgId = storage::addMessageToConversation(db, convs[0], interaction); + interaction.body = storage::getContactInteractionString(uri, interaction::Status::SUCCESS); + auto convIdx = indexOf(convs[0]); { std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]); - conversations[convIdx].interactions.emplace(msgId, msg); + conversations[convIdx].interactions.emplace(msgId, interaction); } dirtyConversations = {true, true}; - emit linked.newInteraction(conv[0], msgId, msg); + emit linked.newInteraction(convs[0], msgId, interaction); } catch (std::out_of_range& e) { qDebug() << "ConversationModelPimpl::slotContactAdded can't find contact"; } } } - void ConversationModelPimpl::slotContactRemoved(const std::string& uri) { @@ -1443,26 +1429,34 @@ ConversationModelPimpl::addConversationWith(const std::string& convId, } catch (...) { conversation.callId = ""; } - database::getHistory(db, conversation); - std::vector<std::function<void(void)>> slotLambdas; + storage::getHistory(db, conversation); + std::vector<std::function<void(void)>> updateSlots; { std::lock_guard<std::mutex> lk(interactionsLocks[convId]); for (auto& interaction: conversation.interactions) { - if (interaction.second.status == interaction::Status::SENDING) { - // Get the message status from daemon, else unknown - auto id = database::getDaemonIdByInteractionId(db, std::to_string(interaction.first)); - int status = 0; - if (!id.empty()) { - auto msgId = std::stoull(id); - status = ConfigurationManager::instance().getMessageStatus(msgId); - } - slotLambdas.emplace_back([=]() -> void { - slotUpdateInteractionStatus(linked.owner.id, std::stoull(id), contactUri, status); - }); + if (interaction.second.status != interaction::Status::SENDING) { + continue; + } + // Get the message status from daemon, else unknown + auto id = storage::getDaemonIdByInteractionId(db, std::to_string(interaction.first)); + int status = 0; + if (id.empty()) { + continue; + } + try { + auto msgId = std::stoull(id); + status = ConfigurationManager::instance().getMessageStatus(msgId); + updateSlots.emplace_back( + [this, msgId, contactUri, status]() -> void { + auto accId = linked.owner.id; + slotUpdateInteractionStatus(accId, msgId, contactUri, status); + }); + } catch (const std::exception& e) { + qDebug() << "message id was invalid"; } } } - for (const auto& l: slotLambdas) { l(); } + for (const auto& s: updateSlots) { s(); } conversation.unreadMessages = getNumberOfUnreadMessagesFor(convId); conversations.emplace_back(conversation); @@ -1526,12 +1520,10 @@ ConversationModelPimpl::slotCallStatusChanged(const std::string& callId, int cod void ConversationModelPimpl::slotCallStarted(const std::string& callId) { + try { auto call = linked.owner.callModel->getCall(callId); - if (call.isOutgoing) - addOrUpdateCallMessage(callId, QObject::tr("📞 Outgoing call").toStdString()); - else - addOrUpdateCallMessage(callId, QObject::tr("📞 Incoming call").toStdString()); + addOrUpdateCallMessage(callId, (!call.isOutgoing ? call.peerUri : "")); } catch (std::out_of_range& e) { qDebug() << "ConversationModelPimpl::slotCallStarted can't start inexistant call"; } @@ -1542,20 +1534,14 @@ ConversationModelPimpl::slotCallEnded(const std::string& callId) { try { auto call = linked.owner.callModel->getCall(callId); + // get duration + std::time_t duration = 0; if (call.startTime.time_since_epoch().count() != 0) { - if (call.isOutgoing) - addOrUpdateCallMessage(callId, QObject::tr("📞 Outgoing call - ").toStdString() - + linked.owner.callModel->getFormattedCallDuration(callId)); - else - addOrUpdateCallMessage(callId, QObject::tr("📞 Incoming call - ").toStdString() - + linked.owner.callModel->getFormattedCallDuration(callId)); - } else { - if (call.isOutgoing) - addOrUpdateCallMessage(callId, QObject::tr("🕽 Missed outgoing call").toStdString()); - else - addOrUpdateCallMessage(callId, QObject::tr("🕽 Missed incoming call").toStdString()); + auto duration_ns = std::chrono::steady_clock::now() - call.startTime; + duration = std::chrono::duration_cast<std::chrono::seconds>(duration_ns).count(); } - + // add or update call interaction with duration + addOrUpdateCallMessage(callId, (!call.isOutgoing ? call.peerUri : ""), duration); /* Reset the callId stored in the conversation. Do not call selectConversation() since it is already done in slotCallStatusChanged. */ for (auto& conversation: conversations) @@ -1569,33 +1555,42 @@ ConversationModelPimpl::slotCallEnded(const std::string& callId) } void -ConversationModelPimpl::addOrUpdateCallMessage(const std::string& callId, const std::string& body) +ConversationModelPimpl::addOrUpdateCallMessage(const std::string& callId, + const std::string& from, + const std::time_t& duration) { // Get conversation - for (auto& conversation: conversations) { - if (conversation.callId == callId) { - auto uid = conversation.uid; - auto msg = interaction::Info {accountProfileId, body, std::time(nullptr), - interaction::Type::CALL, interaction::Status::SUCCEED}; - int msgId = database::addOrUpdateMessage(db, accountProfileId, conversation.uid, msg, callId); - auto newInteraction = conversation.interactions.find(msgId) == conversation.interactions.end(); - if (newInteraction) { - conversation.lastMessageUid = msgId; - std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]); - conversation.interactions.emplace(msgId, msg); - } else { - std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]); - conversation.interactions[msgId] = msg; - } - dirtyConversations = {true, true}; - if (newInteraction) - emit linked.newInteraction(conversation.uid, msgId, msg); - else - emit linked.interactionStatusUpdated(conversation.uid, msgId, msg); - sortConversations(); - emit linked.modelSorted(); - } + auto conv_it = std::find_if(conversations.begin(), conversations.end(), + [&callId](const conversation::Info& conversation) { + return conversation.callId == callId; + }); + if (conv_it == conversations.end()) { + return; } + auto uid = conv_it->uid; + std::string uriString = storage::prepareUri(from, linked.owner.profileInfo.type); + auto msg = interaction::Info { uriString, {}, std::time(nullptr), duration, + interaction::Type::CALL, interaction::Status::SUCCESS, true }; + // update the db + int msgId = storage::addOrUpdateMessage(db, conv_it->uid, msg, callId); + // now set the formatted call message string in memory only + msg.body = storage::getCallInteractionString(uriString, duration); + auto newInteraction = conv_it->interactions.find(msgId) == conv_it->interactions.end(); + if (newInteraction) { + conv_it->lastMessageUid = msgId; + std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]); + conv_it->interactions.emplace(msgId, msg); + } else { + std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]); + conv_it->interactions[msgId] = msg; + } + dirtyConversations = { true, true }; + if (newInteraction) + emit linked.newInteraction(conv_it->uid, msgId, msg); + else + emit linked.interactionStatusUpdated(conv_it->uid, msgId, msg); + sortConversations(); + emit linked.modelSorted(); } void @@ -1624,14 +1619,10 @@ ConversationModelPimpl::slotIncomingCallMessage(const std::string& callId, const // Show messages in all conversations for conferences. for (const auto& conversation: conversations) { if (conversation.confId == callId) { - if (conversation.participants.empty()) continue; - auto type = linked.owner.profileInfo.type; - try { - type = linked.owner.contactModel->getContact(from).profileInfo.type; - } catch (std::out_of_range& e) {} - auto authorProfileId = database::getOrInsertProfile(db, from, linked.owner.id, - false, to_string(type)); - addIncomingMessage(conversation.participants.front(), body, authorProfileId); + if (conversation.participants.empty()) { + continue; + } + addIncomingMessage(from, body); } } } else { @@ -1643,44 +1634,32 @@ ConversationModelPimpl::slotIncomingCallMessage(const std::string& callId, const void ConversationModelPimpl::addIncomingMessage(const std::string& from, const std::string& body, - const std::string& authorProfileId, const uint64_t& timestamp) { - auto type = linked.owner.profileInfo.type; - try { - type = linked.owner.contactModel->getContact(from).profileInfo.type; - } catch (std::out_of_range& e) {} - auto contactProfileId = database::getOrInsertProfile(db, from, linked.owner.id, - false, to_string(type)); - auto accountProfileId = database::getProfileId(db, linked.owner.id, "true", - linked.owner.profileInfo.uri); - auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId); - if (conv.empty()) { - conv.emplace_back(database::beginConversationsBetween( - db, accountProfileId, contactProfileId, - QObject::tr("Invitation received").toStdString() - )); - } - auto authorId = authorProfileId.empty()? contactProfileId: authorProfileId; - auto msg = interaction::Info {authorId, body, - timestamp == 0 ? std::time(nullptr) : static_cast<time_t>(timestamp), - interaction::Type::TEXT, interaction::Status::UNREAD}; - auto msgId = database::addMessageToConversation(db, accountProfileId, conv[0], msg); - auto conversationIdx = indexOf(conv[0]); + auto convIds = storage::getConversationsWithPeer(db, from); + if (convIds.empty()) { + convIds.emplace_back(storage::beginConversationWithPeer(db, from, false)); + } + auto msg = interaction::Info { from, body, + timestamp == 0 ? std::time(nullptr) : static_cast<time_t>(timestamp), 0, + interaction::Type::TEXT, interaction::Status::SUCCESS, false}; + auto msgId = storage::addMessageToConversation(db, convIds[0], msg); + auto conversationIdx = indexOf(convIds[0]); // Add the conversation if not already here if (conversationIdx == -1) { - addConversationWith(conv[0], from); - emit linked.newConversation(conv[0]); + addConversationWith(convIds[0], from); + emit linked.newConversation(convIds[0]); } else { { std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]); conversations[conversationIdx].interactions.emplace(msgId, msg); } conversations[conversationIdx].lastMessageUid = msgId; + conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convIds[0]); } dirtyConversations = {true, true}; - emit behaviorController.newUnreadInteraction(linked.owner.id, conv[0], msgId, msg); - emit linked.newInteraction(conv[0], msgId, msg); + emit behaviorController.newUnreadInteraction(linked.owner.id, convIds[0], msgId, msg); + emit linked.newInteraction(convIds[0], msgId, msg); sortConversations(); emit linked.modelSorted(); } @@ -1699,11 +1678,13 @@ ConversationModelPimpl::slotCallAddedToConference(const std::string& callId, con void ConversationModelPimpl::slotUpdateInteractionStatus(const std::string& accountId, - const uint64_t id, - const std::string& to, + const uint64_t daemon_id, + const std::string& peer_uri, int status) { - if (accountId != linked.owner.id) return; + if (accountId != linked.owner.id) { + return; + } auto newStatus = interaction::Status::INVALID; switch (static_cast<DRing::Account::MessageStates>(status)) { @@ -1714,13 +1695,11 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const std::string& accountId newStatus = interaction::Status::TRANSFER_CANCELED; break; case DRing::Account::MessageStates::SENT: - newStatus = interaction::Status::SUCCEED; + case DRing::Account::MessageStates::READ: + newStatus = interaction::Status::SUCCESS; break; case DRing::Account::MessageStates::FAILURE: - newStatus = interaction::Status::FAILED; - break; - case DRing::Account::MessageStates::READ: - newStatus = interaction::Status::READ; + newStatus = interaction::Status::FAILURE; break; case DRing::Account::MessageStates::UNKNOWN: default: @@ -1728,17 +1707,16 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const std::string& accountId break; } // Update database - auto interactionId = database::getInteractionIdByDaemonId(db, std::to_string(id)); - if (interactionId.empty()) return; + auto interactionId = storage::getInteractionIdByDaemonId(db, std::to_string(daemon_id)); + if (interactionId.empty()) { + return; + } auto msgId = std::stoull(interactionId); - database::updateInteractionStatus(db, msgId, newStatus); + storage::updateInteractionStatus(db, msgId, newStatus); // Update conversations - auto contactProfileId = database::getProfileId(db, linked.owner.id, "false", to); - auto accountProfileId = database::getProfileId(db, linked.owner.id, "true", - linked.owner.profileInfo.uri); - auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId); - if (!conv.empty()) { - auto conversationIdx = indexOf(conv[0]); + auto convIds = storage::getConversationsWithPeer(db, peer_uri); + if (!convIds.empty()) { + auto conversationIdx = indexOf(convIds[0]); interaction::Info itCopy; bool emitUpdated = false; if (conversationIdx != -1) { @@ -1751,8 +1729,10 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const std::string& accountId itCopy = it->second; } } - if (emitUpdated) - emit linked.interactionStatusUpdated(conv[0], msgId, itCopy); + if (emitUpdated) { + dirtyConversations = { true, true }; + emit linked.interactionStatusUpdated(convIds[0], msgId, itCopy); + } } } @@ -1760,19 +1740,19 @@ void ConversationModelPimpl::slotConferenceRemoved(const std::string& confId) { // Get conversation - for(auto& i : conversations){ - if (i.confId == confId) + for(auto& i : conversations) { + if (i.confId == confId) { i.confId = ""; + } } } int ConversationModelPimpl::getNumberOfUnreadMessagesFor(const std::string& uid) { - return database::countUnreadFromInteractions(db, uid); + return storage::countUnreadFromInteractions(db, uid); } - void ConversationModel::sendFile(const std::string& convUid, const std::string& path, @@ -1853,7 +1833,7 @@ ConversationModel::cancelTransfer(const std::string& convUid, uint64_t interacti it->second.status = interaction::Status::TRANSFER_CANCELED; // update information in the db - database::updateInteractionStatus(pimpl_->db, interactionId, interaction::Status::TRANSFER_CANCELED); + storage::updateInteractionStatus(pimpl_->db, interactionId, interaction::Status::TRANSFER_CANCELED); emitUpdated = true; itCopy = it->second; } @@ -1890,11 +1870,12 @@ ConversationModelPimpl::usefulDataFromDataTransfer(long long dringId, const data { try { interactionId = lrc.getDataTransferModel().getInteractionIdFromDringId(dringId); - } catch (...) { + } catch (const std::out_of_range& e) { + qWarning() << "Couldn't get interaction from daemon Id: " << dringId; return false; } - convId = database::conversationIdFromInteractionId(db, interactionId); + convId = storage::conversationIdFromInteractionId(db, interactionId); return true; } @@ -1907,38 +1888,24 @@ ConversationModelPimpl::slotTransferStatusCreated(long long dringId, datatransfe const MapStringString accountDetails = ConfigurationManager::instance().getAccountDetails(linked.owner.id.c_str()); if (accountDetails.empty()) return; - auto type = linked.owner.profileInfo.type; - try { - type = linked.owner.contactModel->getContact(info.peerUri).profileInfo.type; - } catch (std::out_of_range& e) {} - auto contactProfileId = database::getOrInsertProfile(db, info.peerUri, info.accountId, - false, to_string(type)); - auto accountProfileId = database::getProfileId(db, info.accountId, "true", - linked.owner.profileInfo.uri); - // create a new conversation if needed - auto conversation_list = database::getConversationsBetween(db, accountProfileId, contactProfileId); - if (conversation_list.empty()) { - conversation_list.emplace_back(database::beginConversationsBetween( - db, accountProfileId, contactProfileId, - QObject::tr("Invitation received").toStdString())); + auto convIds = storage::getConversationsWithPeer(db, info.peerUri); + if (convIds.empty()) { + convIds.emplace_back(storage::beginConversationWithPeer(db, info.peerUri, false)); } // add interaction to the db - const auto& convId = conversation_list[0]; - auto interactionId = database::addDataTransferToConversation(db, accountProfileId, convId, info); + const auto& convId = convIds[0]; + auto interactionId = storage::addDataTransferToConversation(db, convId, info); // map dringId and interactionId for latter retrivial from client (that only known the interactionId) lrc.getDataTransferModel().registerTransferId(dringId, interactionId); - auto interactioType = info.isOutgoing ? - interaction::Type::OUTGOING_DATA_TRANSFER : - interaction::Type::INCOMING_DATA_TRANSFER; - auto interaction = interaction::Info {info.isOutgoing? accountProfileId : contactProfileId, - info.isOutgoing? info.path : info.displayName, - std::time(nullptr), - interactioType, - interaction::Status::TRANSFER_CREATED}; + auto interaction = interaction::Info{ info.isOutgoing ? "" : info.peerUri, + info.isOutgoing ? info.path : info.displayName, + std::time(nullptr), 0, + interaction::Type::DATA_TRANSFER, + interaction::Status::TRANSFER_CREATED, false}; // prepare interaction Info and emit signal for the client auto conversationIdx = indexOf(convId); @@ -1951,6 +1918,7 @@ ConversationModelPimpl::slotTransferStatusCreated(long long dringId, datatransfe conversations[conversationIdx].interactions.emplace(interactionId, interaction); } conversations[conversationIdx].lastMessageUid = interactionId; + conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convId); } dirtyConversations = {true, true}; emit behaviorController.newUnreadInteraction(linked.owner.id, convId, interactionId, interaction); @@ -1974,7 +1942,7 @@ ConversationModelPimpl::slotTransferStatusAwaitingHost(long long dringId, datatr return; auto newStatus = interaction::Status::TRANSFER_AWAITING_HOST; - database::updateInteractionStatus(db, interactionId, newStatus); + storage::updateInteractionStatus(db, interactionId, newStatus); auto conversationIdx = indexOf(convId); if (conversationIdx != -1) { @@ -2018,8 +1986,8 @@ void ConversationModelPimpl::acceptTransfer(const std::string& convUid, uint64_t interactionId, const std::string& path) { lrc.getDataTransferModel().accept(interactionId, path, 0); - database::updateInteractionBody(db, interactionId, path); - database::updateInteractionStatus(db, interactionId, interaction::Status::TRANSFER_ACCEPTED); + storage::updateInteractionBody(db, interactionId, path); + storage::updateInteractionStatus(db, interactionId, interaction::Status::TRANSFER_ACCEPTED); // prepare interaction Info and emit signal for the client auto conversationIdx = indexOf(convUid); @@ -2053,7 +2021,7 @@ ConversationModelPimpl::slotTransferStatusOngoing(long long dringId, datatransfe return; auto newStatus = interaction::Status::TRANSFER_ONGOING; - database::updateInteractionStatus(db, interactionId, newStatus); + storage::updateInteractionStatus(db, interactionId, newStatus); auto conversationIdx = indexOf(convId); if (conversationIdx != -1) { @@ -2109,7 +2077,7 @@ ConversationModelPimpl::slotTransferStatusFinished(long long dringId, datatransf } if (emitUpdated) { dirtyConversations = {true, true}; - database::updateInteractionStatus(db, interactionId, newStatus); + storage::updateInteractionStatus(db, interactionId, newStatus); emit linked.interactionStatusUpdated(convId, interactionId, itCopy); } } @@ -2148,7 +2116,7 @@ ConversationModelPimpl::updateTransferStatus(long long dringId, datatransfer::In return; // update information in the db - database::updateInteractionStatus(db, interactionId, newStatus); + storage::updateInteractionStatus(db, interactionId, newStatus); // prepare interaction Info and emit signal for the client auto conversationIdx = indexOf(convId); diff --git a/src/database.cpp b/src/database.cpp index 62088cc5..13a5026f 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -1,9 +1,10 @@ /**************************************************************************** - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * + * 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: Guillaume Roguez <guillaume.roguez@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 * @@ -53,160 +54,131 @@ namespace lrc using namespace api; -Database::Database() -: QObject() +Database::Database(const QString& name, const QString& basePath) + : QObject() + , connectionName_(name) + , basePath_(basePath) + , version_(DB_VERSION) { if (not QSqlDatabase::drivers().contains("QSQLITE")) { throw std::runtime_error("QSQLITE not supported"); } - { - QDir dataDir(getPath()); - // create data directory if not created yet - dataDir.mkpath(getPath()); - QDir oldDataDir(getPath()); - oldDataDir.cdUp(); - oldDataDir = oldDataDir - .absolutePath() -#if defined(_WIN32) || 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. - oldDataDir.removeRecursively(); - } - } + // initalize the database. + db_ = QSqlDatabase::addDatabase("QSQLITE", connectionName_); + + auto databaseFile = QFileInfo(basePath_ + connectionName_ + ".db"); + QString databaseFileName = databaseFile.fileName(); + auto absoluteDir = databaseFile.absoluteDir(); - // initialize the database. - db_ = QSqlDatabase::addDatabase("QSQLITE"); #ifdef ENABLE_TEST - db_.setDatabaseName(QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).filePath(NAME)); + databaseFullPath_ = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).filePath(databaseFileName); #else - db_.setDatabaseName(QDir(getPath()).filePath(NAME)); + // make sure the directory exists + if (!absoluteDir.exists()) + absoluteDir.mkpath("."); + databaseFullPath_ = absoluteDir.filePath(databaseFileName); #endif + db_.setDatabaseName(databaseFullPath_); +} + +Database::~Database() +{ +} + +void +Database::remove() +{ + // close db and remove file + if (db_.isOpen()) { + db_.close(); + } + QFile(databaseFullPath_).remove(); +} +void +Database::load() +{ // open the database. if (not db_.open()) { - throw std::runtime_error("cannot open database"); + std::stringstream ss; + ss << "cannot open database: " << connectionName_.toStdString(); + throw std::runtime_error(ss.str()); } // if db is empty we create them. if (db_.tables().empty()) { try { - QSqlDatabase::database().transaction(); + QSqlDatabase::database(connectionName_).transaction(); createTables(); - QSqlDatabase::database().commit(); + QSqlDatabase::database(connectionName_).commit(); } catch (QueryError& e) { - QSqlDatabase::database().rollback(); + QSqlDatabase::database(connectionName_).rollback(); throw std::runtime_error("Could not correctly create the database"); } - // NOTE: the migration can take some time. - migrateOldFiles(); } else { migrateIfNeeded(); } -} - -QString -lrc::Database::getPath() -{ - QDir dataDir(QStandardPaths::writableLocation( - QStandardPaths::AppLocalDataLocation)); - // Avoid to depends on the client name. - dataDir.cdUp(); - return dataDir.absolutePath() + "/jami"; -} - -Database::~Database() -{ } void Database::createTables() { - QSqlQuery query; - - auto tableProfiles = "CREATE TABLE profiles (id INTEGER PRIMARY KEY, \ - uri TEXT NOT NULL, \ - alias TEXT, \ - photo TEXT, \ - type TEXT, \ - status TEXT)"; - - auto tableConversations = "CREATE TABLE conversations (id INTEGER,\ - participant_id INTEGER, \ - FOREIGN KEY(participant_id) REFERENCES profiles(id))"; - - auto tableInteractions = "CREATE TABLE interactions (id INTEGER PRIMARY KEY,\ - account_id INTEGER, \ - author_id INTEGER, \ - conversation_id INTEGER, \ - timestamp INTEGER, \ - body TEXT, \ - type TEXT, \ - status TEXT, \ - daemon_id TEXT, \ - FOREIGN KEY(account_id) REFERENCES profiles(id), \ - FOREIGN KEY(author_id) REFERENCES profiles(id), \ - FOREIGN KEY(conversation_id) REFERENCES conversations(id))"; - - auto tableProfileAccounts = "CREATE TABLE profiles_accounts (profile_id INTEGER NOT NULL, \ - account_id TEXT NOT NULL, \ - is_account TEXT, \ - FOREIGN KEY(profile_id) REFERENCES profiles(id))"; - // add profiles table - if (not db_.tables().contains("profiles", Qt::CaseInsensitive) - and not query.exec(tableProfiles)) { - throw QueryError(query); - } + QSqlQuery query(db_); + + auto tableConversations = "CREATE TABLE conversations ( \ + id INTEGER, \ + participant TEXT, \ + extra_data TEXT \ + )"; + + auto indexConversations = "CREATE INDEX `idx_conversations_uri` ON `conversations` (`participant`)"; + + auto tableInteractions = "CREATE TABLE interactions ( \ + id INTEGER PRIMARY KEY, \ + author TEXT, \ + conversation INTEGER, \ + timestamp INTEGER, \ + body TEXT, \ + type TEXT, \ + status TEXT, \ + is_read INTEGER, \ + daemon_id BIGINT, \ + extra_data TEXT, \ + FOREIGN KEY(conversation) REFERENCES conversations(id) \ + )"; + + auto indexInteractions = "CREATE INDEX `idx_interactions_uri` ON `interactions` (`author`)"; // add conversations table - if (not db_.tables().contains("conversations", Qt::CaseInsensitive) - and not query.exec(tableConversations)) { + if (!db_.tables().contains("conversations", Qt::CaseInsensitive)) { + if (!query.exec(tableConversations) || ! query.exec(indexConversations)) { throw QueryError(query); + } } // add interactions table - if (not db_.tables().contains("interactions", Qt::CaseInsensitive) - and not query.exec(tableInteractions)) { - throw QueryError(query); - } - - // add profiles accounts table - if (not db_.tables().contains("profiles_accounts", Qt::CaseInsensitive) - and not query.exec(tableProfileAccounts)) { + if (!db_.tables().contains("interactions", Qt::CaseInsensitive)) { + if (!query.exec(tableInteractions) || !query.exec(indexInteractions)) { throw QueryError(query); + } } - storeVersion(VERSION); + storeVersion(version_); } void Database::migrateIfNeeded() { try { - std::string currentVersion = getVersion(); - if (currentVersion == VERSION) { + auto currentVersion = getVersion(); + if (currentVersion == version_) { return; } QSqlDatabase::database().transaction(); migrateFromVersion(currentVersion); - storeVersion(VERSION); + storeVersion(version_); QSqlDatabase::database().commit(); } catch (QueryError& e) { QSqlDatabase::database().rollback(); @@ -215,49 +187,33 @@ Database::migrateIfNeeded() } void -Database::migrateFromVersion(const std::string& currentVersion) -{ - if (currentVersion == "1") { - migrateSchemaFromVersion1(); - } -} - -void -Database::migrateSchemaFromVersion1() +Database::migrateFromVersion(const QString& currentVersion) { - QSqlQuery query; - auto tableProfileAccounts = "CREATE TABLE profiles_accounts (profile_id INTEGER NOT NULL, \ - account_id TEXT NOT NULL, \ - is_account TEXT, \ - FOREIGN KEY(profile_id) REFERENCES profiles(id))"; - // add profiles accounts table - if (not db_.tables().contains("profiles_accounts", Qt::CaseInsensitive) - and not query.exec(tableProfileAccounts)) { - throw QueryError(query); - } - linkRingProfilesWithAccounts(false); + (void)currentVersion; } void -Database::storeVersion(const std::string& version) +Database::storeVersion(const QString& version) { - QSqlQuery query; + QSqlQuery query(db_); - auto storeVersionQuery = std::string("PRAGMA user_version = ") + version; + auto storeVersionQuery = "PRAGMA user_version = " + version; - if (not query.exec(storeVersionQuery.c_str())) + if (not query.exec(storeVersionQuery)) throw QueryError(query); + + qDebug() << "database " << databaseFullPath_ << " version set to:" << version; } -std::string +QString Database::getVersion() { - QSqlQuery query; - auto getVersionQuery = std::string("pragma user_version"); - if (not query.exec(getVersionQuery.c_str())) + QSqlQuery query(db_); + auto getVersionQuery = "pragma user_version"; + if (not query.exec(getVersionQuery)) throw QueryError(query); query.first(); - return query.value(0).toString().toStdString(); + return query.value(0).toString(); } int @@ -265,7 +221,7 @@ Database::insertInto(const std::string& table, // "t const std::map<std::string, std::string>& bindCol, // {{":id", "id"}, {":forename", "colforname"}, {":name", "colname"}} const std::map<std::string, std::string>& bindsSet) // {{":id", "7"}, {":forename", "alice"}, {":name", "cooper"}} { - QSqlQuery query; + QSqlQuery query(db_); std::string columns; std::string binds; @@ -303,7 +259,7 @@ Database::update(const std::string& table, // "test const std::string& where, // "contact=:name AND id=:id const std::map<std::string, std::string>& bindsWhere) // {{":name", "toto"}, {":id", "65"}} { - QSqlQuery query; + QSqlQuery query(db_); auto prepareStr = std::string("UPDATE " + table + " SET " + set + " WHERE " + where); query.prepare(prepareStr.c_str()); @@ -324,10 +280,11 @@ Database::select(const std::string& select, // "id", const std::string& where, // "contact=:name AND id=:id const std::map<std::string, std::string>& bindsWhere) // {{":name", "toto"}, {":id", "65"}} { - QSqlQuery query; + QSqlQuery query(db_); std::string columnsSelect; - auto prepareStr = std::string("SELECT " + select + " FROM " + table + " WHERE " + where); + auto prepareStr = std::string("SELECT " + select + " FROM " + table + + (where.empty() ? "" : (" WHERE " + where))); query.prepare(prepareStr.c_str()); for (const auto& entry : bindsWhere) @@ -355,7 +312,7 @@ Database::count(const std::string& count, // "id", "body", ... const std::string& where, // "contact=:name AND id=:id" const std::map<std::string, std::string>& bindsWhere) // {{":name", "toto"}, {":id", "65"}} { - QSqlQuery query; + QSqlQuery query(db_); std::string columnsSelect; auto prepareStr = std::string("SELECT count(" + count + ") FROM " + table + " WHERE " + where); query.prepare(prepareStr.c_str()); @@ -375,7 +332,7 @@ Database::deleteFrom(const std::string& table, // "t const std::string& where, // "contact=:name AND id=:id const std::map<std::string, std::string>& bindsWhere) // {{":name", "toto"}, {":id", "65"}} { - QSqlQuery query; + QSqlQuery query(db_); auto prepareStr = std::string("DELETE FROM " + table + " WHERE " + where); query.prepare(prepareStr.c_str()); @@ -387,6 +344,18 @@ Database::deleteFrom(const std::string& table, // "t throw QueryDeleteError(query, table, where, bindsWhere); } +void +Database::truncateTable(const std::string& table) +{ + QSqlQuery query(db_); + + auto prepareStr = std::string("TRUNCATE TABLE " + table); + query.prepare(prepareStr.c_str()); + + if (not query.exec()) + throw QueryTruncateError(query, table); +} + Database::QueryError::QueryError(const QSqlQuery& query) : std::runtime_error(query.lastError().text().toStdString()) , query(query) @@ -484,20 +453,140 @@ Database::QueryDeleteError::details() return oss.str(); } +Database::QueryTruncateError::QueryTruncateError(const QSqlQuery& query, + const std::string& table) + : QueryError(query) + , table(table) +{} + +std::string +Database::QueryTruncateError::details() +{ + std::ostringstream oss; + oss << "paramaters sent :"; + oss << "table = " << table.c_str(); + return oss.str(); +} + +/***************************************************************************** + * * + * LegacyDatabase * + * * + ****************************************************************************/ +LegacyDatabase::LegacyDatabase(const QString& basePath) + : Database("ring", basePath) +{ + version_ = LEGACY_DB_VERSION; +} + +LegacyDatabase::~LegacyDatabase() +{ + remove(); + // remove old LRC files + QDir(basePath_ + "text/").removeRecursively(); + QDir(basePath_ + "profiles/").removeRecursively(); + QDir(basePath_ + "peer_profiles/").removeRecursively(); +} + +void +LegacyDatabase::load() +{ + // open the database. + if (not db_.open()) { + std::stringstream ss; + ss << "cannot open database: " << connectionName_.toStdString(); + throw std::runtime_error(ss.str()); + } + + // if db is empty we create them. + if (db_.tables().empty()) { + try { + QSqlDatabase::database(connectionName_).transaction(); + createTables(); + QSqlDatabase::database(connectionName_).commit(); + } catch (QueryError& e) { + QSqlDatabase::database(connectionName_).rollback(); + throw std::runtime_error("Could not correctly create the database"); + } + migrateOldFiles(); + } else { + migrateIfNeeded(); + } +} + +void +LegacyDatabase::createTables() +{ + QSqlQuery query(db_); + + auto tableProfiles = "CREATE TABLE profiles (id INTEGER PRIMARY KEY, \ + uri TEXT NOT NULL, \ + alias TEXT, \ + photo TEXT, \ + type TEXT, \ + status TEXT)"; + + auto tableConversations = "CREATE TABLE conversations (id INTEGER,\ + participant_id INTEGER, \ + FOREIGN KEY(participant_id) REFERENCES profiles(id))"; + + auto tableInteractions = "CREATE TABLE interactions (id INTEGER PRIMARY KEY,\ + account_id INTEGER, \ + author_id INTEGER, \ + conversation_id INTEGER, \ + timestamp INTEGER, \ + body TEXT, \ + type TEXT, \ + status TEXT, \ + daemon_id TEXT, \ + FOREIGN KEY(account_id) REFERENCES profiles(id), \ + FOREIGN KEY(author_id) REFERENCES profiles(id), \ + FOREIGN KEY(conversation_id) REFERENCES conversations(id))"; + + auto tableProfileAccounts = "CREATE TABLE profiles_accounts (profile_id INTEGER NOT NULL, \ + account_id TEXT NOT NULL, \ + is_account TEXT, \ + FOREIGN KEY(profile_id) REFERENCES profiles(id))"; + // add profiles table + if (not db_.tables().contains("profiles", Qt::CaseInsensitive) + and not query.exec(tableProfiles)) { + throw QueryError(query); + } + + // add conversations table + if (not db_.tables().contains("conversations", Qt::CaseInsensitive) + and not query.exec(tableConversations)) { + throw QueryError(query); + } + + // add interactions table + if (not db_.tables().contains("interactions", Qt::CaseInsensitive) + and not query.exec(tableInteractions)) { + throw QueryError(query); + } + + // add profiles accounts table + if (not db_.tables().contains("profiles_accounts", Qt::CaseInsensitive) + and not query.exec(tableProfileAccounts)) { + throw QueryError(query); + } + + storeVersion(version_); +} + void -Database::migrateOldFiles() +LegacyDatabase::migrateOldFiles() { migrateLocalProfiles(); migratePeerProfiles(); migrateTextHistory(); linkRingProfilesWithAccounts(true); - // NOTE we don't remove old files for now. } void -Database::migrateLocalProfiles() +LegacyDatabase::migrateLocalProfiles() { - const QDir profilesDir = getPath() + "/profiles/"; + const QDir profilesDir = basePath_ + "profiles/"; const QStringList entries = profilesDir.entryList({QStringLiteral("*.vcf")}, QDir::Files); foreach (const QString& item , entries) { auto filePath = profilesDir.path() + '/' + item; @@ -514,7 +603,6 @@ Database::migrateLocalProfiles() const auto alias = vCard[lrc::vCard::Property::FORMATTED_NAME]; const auto avatar = vCard["PHOTO;ENCODING=BASE64;TYPE=PNG"]; - const QStringList accountIds = ConfigurationManager::instance().getAccountList(); for (auto accountId : accountIds) { MapStringString account = ConfigurationManager::instance(). @@ -562,9 +650,9 @@ Database::migrateLocalProfiles() } void -Database::migratePeerProfiles() +LegacyDatabase::migratePeerProfiles() { - const QDir profilesDir = getPath() + "/peer_profiles/"; + const QDir profilesDir = basePath_ + "peer_profiles/"; const QStringList entries = profilesDir.entryList({QStringLiteral("*.vcf")}, QDir::Files); @@ -601,10 +689,10 @@ Database::migratePeerProfiles() } void -Database::migrateTextHistory() +LegacyDatabase::migrateTextHistory() { // load all text recordings so we can recover CMs that are not in the call history - QDir dir(getPath() + "/text/"); + QDir dir(basePath_ + "text/"); if (dir.exists()) { // get .json files, sorted by time, latest first QStringList filters; @@ -733,29 +821,31 @@ Database::migrateTextHistory() } void -Database::updateProfileAccountForContact(const std::string& contactURI, - const std::string& accountId) +LegacyDatabase::migrateFromVersion(const QString& currentVersion) { - auto profileIds = select("id", "profiles","uri=:uri", - {{":uri", contactURI}}) - .payloads; - if (profileIds.empty()) { - return; + if (currentVersion == "1") { + migrateSchemaFromVersion1(); } - auto rows = select("profile_id", "profiles_accounts", - "account_id=:account_id AND is_account=:is_account", {{":account_id", accountId}, - {":is_account", "false"}}).payloads; - if (std::find(rows.begin(), rows.end(), profileIds[0]) == rows.end()) { - insertInto("profiles_accounts", - {{":profile_id", "profile_id"}, {":account_id", "account_id"}, - {":is_account", "is_account"}}, - {{":profile_id", profileIds[0]}, {":account_id", accountId}, - {":is_account", "false"}}); +} + +void +LegacyDatabase::migrateSchemaFromVersion1() +{ + QSqlQuery query(db_); + auto tableProfileAccounts = "CREATE TABLE profiles_accounts (profile_id INTEGER NOT NULL, \ + account_id TEXT NOT NULL, \ + is_account TEXT, \ + FOREIGN KEY(profile_id) REFERENCES profiles(id))"; + // add profiles accounts table + if (not db_.tables().contains("profiles_accounts", Qt::CaseInsensitive) + and not query.exec(tableProfileAccounts)) { + throw QueryError(query); } + linkRingProfilesWithAccounts(false); } void -Database::linkRingProfilesWithAccounts(bool contactsOnly) +LegacyDatabase::linkRingProfilesWithAccounts(bool contactsOnly) { const QStringList accountIds = ConfigurationManager::instance().getAccountList(); @@ -832,4 +922,26 @@ Database::linkRingProfilesWithAccounts(bool contactsOnly) } } +void +LegacyDatabase::updateProfileAccountForContact(const std::string& contactURI, + const std::string& accountId) +{ + auto profileIds = select("id", "profiles", "uri=:uri", + { {":uri", contactURI} }) + .payloads; + if (profileIds.empty()) { + return; + } + auto rows = select("profile_id", "profiles_accounts", + "account_id=:account_id AND is_account=:is_account", { {":account_id", accountId}, + {":is_account", "false"} }).payloads; + if (std::find(rows.begin(), rows.end(), profileIds[0]) == rows.end()) { + insertInto("profiles_accounts", + { {":profile_id", "profile_id"}, {":account_id", "account_id"}, + {":is_account", "is_account"} }, + { {":profile_id", profileIds[0]}, {":account_id", accountId}, + {":is_account", "false"} }); + } +} + } // namespace lrc diff --git a/src/database.h b/src/database.h index 03e551ce..deddf189 100644 --- a/src/database.h +++ b/src/database.h @@ -1,9 +1,10 @@ /**************************************************************************** - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * + * 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: Guillaume Roguez <guillaume.roguez@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 * @@ -20,25 +21,27 @@ ***************************************************************************/ #pragma once -// Std -#include <memory> -#include <string> -#include <stdexcept> - // Qt -#include <qobject.h> +#include <QObject> #include <QtCore/QDir> #include <QtSql/QSqlQuery> #include <QtCore/QStandardPaths> +#include <QDebug> + +// Std +#include <memory> +#include <string> +#include <stdexcept> +#include <type_traits> namespace lrc { -static constexpr auto VERSION = "1.1"; -static constexpr auto NAME = "ring.db"; +static constexpr auto LEGACY_DB_VERSION = "1.1"; +static constexpr auto DB_VERSION = "1"; /** - * @brief Class that communicates with the database. + * @brief Base class that communicates with a database. * @note not thread safe. */ class Database : public QObject { @@ -47,12 +50,16 @@ class Database : public QObject { public: /** * Create a database on the user system. + * @param the name for which to construct the db. * @exception QueryError database query error. */ - Database(); - + Database(const QString& name, const QString& basePath); ~Database(); + void remove(); + + virtual void load(); + /** * A structure which contains result(s) returned by a database query. */ @@ -151,6 +158,19 @@ public: const std::map<std::string, std::string> bindsWhere; }; + /** + * Exception on database truncate operation. + * details() returns more information. + */ + class QueryTruncateError final : public QueryError { + public: + explicit QueryTruncateError(const QSqlQuery& query, + const std::string& table); + std::string details() override; + + const std::string table; + }; + /** * Insert value(s) inside a table. * @param table where to perfom the action on. @@ -197,6 +217,14 @@ public: void deleteFrom(const std::string& table, const std::string& where, const std::map<std::string, std::string>& bindsWhere); + /** + * Delete all rows from a table(truncate). + * @param table where to perfom the action on. + * @exception QueryDeleteError delete query failed. + * + * @note usually, identifiers between where and bindsWhere, are equals. + */ + void truncateTable(const std::string& table); /** * Select data from table. * @param select column(s) to select.e @@ -225,27 +253,86 @@ public: int count(const std::string& count, const std::string& table, const std::string& where, const std::map<std::string, std::string>& bindsWhere); - static QString getPath(); -private: - void createTables(); - void storeVersion(const std::string& version); + QString basePath_; + +protected: + virtual void createTables(); + + /** + * Migration helpers. + */ + void migrateIfNeeded(); + void storeVersion(const QString& version); + QString getVersion(); + + virtual void migrateFromVersion(const QString& version); + + QString version_; + QString connectionName_; + QString databaseFullPath_; + QSqlDatabase db_; +}; + +/** + * @brief A legacy database to help migrate from the single db epoch. + * @note not thread safe. + */ +class LegacyDatabase final : public Database { + Q_OBJECT +public: /** - * Migration helpers. Parse JSON for history and VCards and add it into the database. + * Create a migratory legacy database. + * @exception QueryError database query error. + */ + LegacyDatabase(const QString& basePath); + ~LegacyDatabase(); + + void load() override; + +protected: + void createTables() override; + +private: + /** + * Migration helpers from old LRC. Parse JSON for history and VCards and add it into the database. */ void migrateOldFiles(); void migrateLocalProfiles(); void migratePeerProfiles(); void migrateTextHistory(); - void linkRingProfilesWithAccounts(bool contactsOnly); - void migrateIfNeeded(); - std::string getVersion(); - void migrateFromVersion(const std::string& version); + + void migrateFromVersion(const QString& version) override; + + /** + * Migration helpers from version 1 + */ void migrateSchemaFromVersion1(); + void linkRingProfilesWithAccounts(bool contactsOnly); void updateProfileAccountForContact(const std::string& contactURI, - const std::string& accountID); - - QSqlDatabase db_; + const std::string& accountID); }; +namespace DatabaseFactory +{ +template<typename T, class... Args> +std::enable_if_t< + std::is_constructible<T, Args...>::value, + std::shared_ptr<Database> +> +create(Args&&... args) { + auto pdb = std::static_pointer_cast<Database>( + std::make_shared<T>(std::forward<Args>(args)...) + ); + // To allow override of the db load method we don't + // call it from the constructor. + try { + pdb->load(); + } catch (const std::runtime_error& e) { + throw std::runtime_error(e); + } + return pdb; +} +} // DatabaseFactory + } // namespace lrc diff --git a/src/datatransfermodel.cpp b/src/datatransfermodel.cpp index 41a428d2..0db15909 100644 --- a/src/datatransfermodel.cpp +++ b/src/datatransfermodel.cpp @@ -76,7 +76,6 @@ DataTransferModel::Impl::Impl(DataTransferModel& up_link) , upLink {up_link} {} - void DataTransferModel::registerTransferId(long long dringId, int interactionId) { @@ -85,7 +84,6 @@ DataTransferModel::registerTransferId(long long dringId, int interactionId) pimpl_->lrc2dringIdMap.emplace(interactionId, dringId); } - DataTransferModel::DataTransferModel() : QObject() , pimpl_ { std::make_unique<Impl>(*this) } @@ -113,14 +111,6 @@ DataTransferModel::transferInfo(long long ringId, datatransfer::Info& lrc_info) { DataTransferInfo infoFromDaemon; if (ConfigurationManager::instance().dataTransferInfo(ringId, infoFromDaemon) == 0) { -#if 0 - int interactionId; - try { - interactionId = pimpl_->dring2lrcIdMap.at(ringId); - } catch (...) { - interactionId = -1; - } -#endif //lrc_info.uid = ? lrc_info.status = convertDataTransferEvent(DRing::DataTransferEventCode(infoFromDaemon.lastEvent)); lrc_info.isOutgoing = !(infoFromDaemon.flags & (1 << uint32_t(DRing::DataTransferFlags::direction))); diff --git a/src/lrc.cpp b/src/lrc.cpp index f6e44c45..f612b7f8 100644 --- a/src/lrc.cpp +++ b/src/lrc.cpp @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * + * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * * Author : Nicolas Jäger <nicolas.jager@savoirfairelinux.com> * * Author : Sébastien Blin <sebastien.blin@savoirfairelinux.com> * * * @@ -30,11 +30,11 @@ #include "api/datatransfermodel.h" #include "api/newaccountmodel.h" #include "callbackshandler.h" -#include "database.h" #include "dbus/callmanager.h" #include "dbus/configurationmanager.h" #include "dbus/instancemanager.h" #include "dbus/configurationmanager.h" +#include "authority/storagehelper.h" namespace lrc { @@ -45,23 +45,23 @@ class LrcPimpl { public: - LrcPimpl(Lrc& linked); + LrcPimpl(Lrc& linked, MigrationCb& willMigrateCb, MigrationCb& didMigrateCb); const Lrc& linked; std::unique_ptr<BehaviorController> behaviorController; std::unique_ptr<CallbacksHandler> callbackHandler; - std::unique_ptr<Database> database; std::unique_ptr<NewAccountModel> accountModel; std::unique_ptr<DataTransferModel> dataTransferModel; std::unique_ptr<AVModel> AVModel_; + }; -Lrc::Lrc() +Lrc::Lrc(MigrationCb willDoMigrationCb, MigrationCb didDoMigrationCb) { // Ensure Daemon is running/loaded (especially on non-DBus platforms) // before instantiating LRC and its members InstanceManager::instance(); - lrcPimpl_ = std::make_unique<LrcPimpl>(*this); + lrcPimpl_ = std::make_unique<LrcPimpl>(*this, willDoMigrationCb, didDoMigrationCb); } Lrc::~Lrc() @@ -124,6 +124,12 @@ Lrc::dbusIsValid() #endif } +void +Lrc::subscribeToDebugReceived() +{ + lrcPimpl_->callbackHandler->subscribeToDebugReceived(); +} + std::vector<std::string> Lrc::activeCalls() { @@ -136,12 +142,11 @@ Lrc::activeCalls() return result; } -LrcPimpl::LrcPimpl(Lrc& linked) +LrcPimpl::LrcPimpl(Lrc& linked, MigrationCb& willMigrateCb, MigrationCb& didMigrateCb) : linked(linked) , behaviorController(std::make_unique<BehaviorController>()) , callbackHandler(std::make_unique<CallbacksHandler>(linked)) -, database(std::make_unique<Database>()) -, accountModel(std::make_unique<NewAccountModel>(linked, *database, *callbackHandler, *behaviorController)) +, accountModel(std::make_unique<NewAccountModel>(linked, *callbackHandler, *behaviorController, willMigrateCb, didMigrateCb)) , dataTransferModel {std::make_unique<DataTransferModel>()} , AVModel_ {std::make_unique<AVModel>(*callbackHandler)} { diff --git a/src/newaccountmodel.cpp b/src/newaccountmodel.cpp index 14151bbf..04208dbc 100644 --- a/src/newaccountmodel.cpp +++ b/src/newaccountmodel.cpp @@ -1,8 +1,9 @@ /**************************************************************************** - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * + * 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 * @@ -36,7 +37,7 @@ #include "api/newcodecmodel.h" #include "api/newdevicemodel.h" #include "api/behaviorcontroller.h" -#include "authority/databasehelper.h" +#include "authority/storagehelper.h" #include "callbackshandler.h" #include "database.h" #include "vcard.h" @@ -61,17 +62,20 @@ class NewAccountModelPimpl: public QObject public: NewAccountModelPimpl(NewAccountModel& linked, Lrc& lrc, - Database& database, const CallbacksHandler& callbackHandler, - const BehaviorController& behaviorController); + const BehaviorController& behaviorController, + MigrationCb& willMigrateCb, + MigrationCb& didMigrateCb); ~NewAccountModelPimpl(); + using AccountInfoDbMap = std::map<std::string, + std::pair<account::Info, std::shared_ptr<Database>>>; + NewAccountModel& linked; Lrc& lrc; const CallbacksHandler& callbacksHandler; - Database& database; - NewAccountModel::AccountInfoMap accounts; const BehaviorController& behaviorController; + AccountInfoDbMap accounts; // Synchronization tools std::mutex m_mutex_account; @@ -83,9 +87,10 @@ public: /** * Add the profile information from an account to the db then add it to accounts. * @param accountId + * @param db an optional migrated database object * @note this method get details for an account from the daemon. */ - void addToAccounts(const std::string& accountId); + void addToAccounts(const std::string& accountId, std::shared_ptr<Database> db = nullptr); /** * Remove account from accounts list. Emit accountRemoved. @@ -147,11 +152,13 @@ public Q_SLOTS: }; NewAccountModel::NewAccountModel(Lrc& lrc, - Database& database, const CallbacksHandler& callbacksHandler, - const BehaviorController& behaviorController) + const BehaviorController& behaviorController, + MigrationCb& willMigrateCb, + MigrationCb& didMigrateCb) : QObject() -, pimpl_(std::make_unique<NewAccountModelPimpl>(*this, lrc, database, callbacksHandler, behaviorController)) +, pimpl_(std::make_unique<NewAccountModelPimpl>(*this, lrc, callbacksHandler, behaviorController, + willMigrateCb, didMigrateCb)) { } @@ -166,9 +173,9 @@ NewAccountModel::getAccountList() const const QStringList accountIds = ConfigurationManager::instance().getAccountList(); for (auto const& id : accountIds) { - auto accountInfo = pimpl_->accounts.find(id.toStdString()); + auto account = pimpl_->accounts.find(id.toStdString()); // Do not include accounts flagged for removal - if (accountInfo != pimpl_->accounts.end() && accountInfo->second.valid) + if (account != pimpl_->accounts.end() && account->second.first.valid) accountsId.emplace_back(id.toStdString()); } @@ -178,11 +185,12 @@ NewAccountModel::getAccountList() const void NewAccountModel::setAccountEnabled(const std::string& accountId, bool enabled) const { - auto accountInfo = pimpl_->accounts.find(accountId); - if (accountInfo == pimpl_->accounts.end()) { + auto account = pimpl_->accounts.find(accountId); + if (account == pimpl_->accounts.end()) { throw std::out_of_range("NewAccountModel::getAccountConfig, can't find " + accountId); } - accountInfo->second.enabled = enabled; + auto& accountInfo = account->second.first; + accountInfo.enabled = enabled; ConfigurationManager::instance().sendRegister(QString::fromStdString(accountId), enabled); } @@ -190,11 +198,11 @@ void NewAccountModel::setAccountConfig(const std::string& accountId, const account::ConfProperties_t& confProperties) const { - auto accountInfoEntry = pimpl_->accounts.find(accountId); - if (accountInfoEntry == pimpl_->accounts.end()) { + auto account = pimpl_->accounts.find(accountId); + if (account == pimpl_->accounts.end()) { throw std::out_of_range("NewAccountModel::save, can't find " + accountId); } - auto& accountInfo = accountInfoEntry->second; + auto& accountInfo = account->second.first; auto& configurationManager = ConfigurationManager::instance(); MapStringString details = confProperties.toDetails(); // Set values from Info. No need to include ID and TYPE. SIP accounts may modify the USERNAME @@ -223,45 +231,41 @@ NewAccountModel::setAccountConfig(const std::string& accountId, account::ConfProperties_t NewAccountModel::getAccountConfig(const std::string& accountId) const { - auto accountInfo = pimpl_->accounts.find(accountId); - if (accountInfo == pimpl_->accounts.end()) { + auto account = pimpl_->accounts.find(accountId); + if (account == pimpl_->accounts.end()) { throw std::out_of_range("NewAccountModel::getAccountConfig, can't find " + accountId); } - - return accountInfo->second.confProperties; + auto& accountInfo = account->second.first; + return accountInfo.confProperties; } void NewAccountModel::setAlias(const std::string& accountId, const std::string& alias) { - auto accountInfo = pimpl_->accounts.find(accountId); - if (accountInfo == pimpl_->accounts.end()) { + auto account = pimpl_->accounts.find(accountId); + if (account == pimpl_->accounts.end()) { throw std::out_of_range("NewAccountModel::setAlias, can't find " + accountId); } - accountInfo->second.profileInfo.alias = alias; - auto accountProfileId = authority::database::getOrInsertProfile(pimpl_->database, - accountInfo->second.profileInfo.uri, accountId, true, - to_string(accountInfo->second.profileInfo.type)); - if (!accountProfileId.empty()) { - authority::database::setAliasForProfileId(pimpl_->database, accountProfileId, alias); - } + auto& accountInfo = account->second.first; + accountInfo.profileInfo.alias = alias; + + authority::storage::createOrUpdateProfile(accountInfo.id, accountInfo.profileInfo); + emit profileUpdated(accountId); } void NewAccountModel::setAvatar(const std::string& accountId, const std::string& avatar) { - auto accountInfo = pimpl_->accounts.find(accountId); - if (accountInfo == pimpl_->accounts.end()) { + auto account = pimpl_->accounts.find(accountId); + if (account == pimpl_->accounts.end()) { throw std::out_of_range("NewAccountModel::setAvatar, can't find " + accountId); } - accountInfo->second.profileInfo.avatar = avatar; - auto accountProfileId = authority::database::getOrInsertProfile(pimpl_->database, - accountInfo->second.profileInfo.uri, accountId, true, - to_string(accountInfo->second.profileInfo.type)); - if (!accountProfileId.empty()) { - authority::database::setAvatarForProfileId(pimpl_->database, accountProfileId, avatar); - } + auto& accountInfo = account->second.first; + accountInfo.profileInfo.avatar = avatar; + + authority::storage::createOrUpdateProfile(accountInfo.id, accountInfo.profileInfo); + emit profileUpdated(accountId); } @@ -301,13 +305,13 @@ NewAccountModel::changeAccountPassword(const std::string& accountId, void NewAccountModel::flagFreeable(const std::string& accountId) const { - auto accountInfo = pimpl_->accounts.find(accountId); - if (accountInfo == pimpl_->accounts.end()) + auto account = pimpl_->accounts.find(accountId); + if (account == pimpl_->accounts.end()) throw std::out_of_range("NewAccountModel::flagFreeable, can't find " + accountId); { std::lock_guard<std::mutex> lock(pimpl_->m_mutex_account_removal); - accountInfo->second.freeable = true; + account->second.first.freeable = true; } pimpl_->m_condVar_account_removal.notify_all(); } @@ -319,25 +323,26 @@ NewAccountModel::getAccountInfo(const std::string& accountId) const if (accountInfo == pimpl_->accounts.end()) throw std::out_of_range("NewAccountModel::getAccountInfo, can't find " + accountId); - return accountInfo->second; + return accountInfo->second.first; } NewAccountModelPimpl::NewAccountModelPimpl(NewAccountModel& linked, Lrc& lrc, - Database& database, const CallbacksHandler& callbacksHandler, - const BehaviorController& behaviorController) + const BehaviorController& behaviorController, + MigrationCb& willMigrateCb, + MigrationCb& didMigrateCb) : linked(linked) , lrc {lrc} , behaviorController(behaviorController) , callbacksHandler(callbacksHandler) -, database(database) , username_changed(false) { const QStringList accountIds = ConfigurationManager::instance().getAccountList(); - - for (auto& id : accountIds) - addToAccounts(id.toStdString()); + auto accountDbs = authority::storage::migrateIfNeeded(accountIds, willMigrateCb, didMigrateCb); + for (const auto& id : accountIds) { + addToAccounts(id.toStdString(), accountDbs.at(accountIds.indexOf(id))); + } connect(&callbacksHandler, &CallbacksHandler::accountsChanged, this, &NewAccountModelPimpl::updateAccounts); connect(&callbacksHandler, &CallbacksHandler::accountStatusChanged, this, &NewAccountModelPimpl::slotAccountStatusChanged); @@ -362,7 +367,7 @@ NewAccountModelPimpl::updateAccounts() // Detect removed accounts std::list<std::string> toBeRemoved; for (auto& it : accounts) { - auto& accountInfo = it.second; + auto& accountInfo = it.second.first; if (!accountIds.contains(QString::fromStdString(accountInfo.id))) { qDebug("detected account removal %s", accountInfo.id.c_str()); toBeRemoved.push_back(accountInfo.id); @@ -375,15 +380,15 @@ NewAccountModelPimpl::updateAccounts() // Detect new accounts for (auto& id : accountIds) { - auto accountInfo = accounts.find(id.toStdString()); - if (accountInfo == accounts.end()) { + auto account = accounts.find(id.toStdString()); + if (account == accounts.end()) { qDebug("detected new account %s", id.toStdString().c_str()); addToAccounts(id.toStdString()); - auto updatedAccountInfo = accounts.find(id.toStdString()); - if (updatedAccountInfo == accounts.end()) { + auto updatedAccount = accounts.find(id.toStdString()); + if (updatedAccount == accounts.end()) { return; } - if (updatedAccountInfo->second.profileInfo.type == profile::Type::SIP) { + if (updatedAccount->second.first.profileInfo.type == profile::Type::SIP) { // NOTE: At this point, a SIP account is ready, but not a Ring // account. Indeed, the keys are not generated at this point. // See slotAccountStatusChanged for more details. @@ -407,7 +412,7 @@ NewAccountModelPimpl::slotAccountStatusChanged(const std::string& accountID, con return; } - auto& accountInfo = it->second; + auto& accountInfo = it->second.first; if (accountInfo.profileInfo.type != profile::Type::SIP) { if (status != api::account::Status::INITIALIZING @@ -431,14 +436,15 @@ NewAccountModelPimpl::slotAccountStatusChanged(const std::string& accountID, con void NewAccountModelPimpl::slotAccountDetailsChanged(const std::string& accountId, const std::map<std::string, std::string>& details) { - auto accountInfo = accounts.find(accountId); - if (accountInfo == accounts.end()) { + auto account = accounts.find(accountId); + if (account == accounts.end()) { throw std::out_of_range("NewAccountModelPimpl::slotAccountDetailsChanged, can't find " + accountId); } - accountInfo->second.fromDetails(convertMap(details)); + auto& accountInfo = account->second.first; + accountInfo.fromDetails(convertMap(details)); if (username_changed) { username_changed = false; - accountInfo->second.registeredName = new_username; + accountInfo.registeredName = new_username; emit linked.profileUpdated(accountId); } emit linked.accountStatusChanged(accountId); @@ -472,8 +478,8 @@ NewAccountModelPimpl::slotNameRegistrationEnded(const std::string& accountId, in { case 0: { convertedStatus = account::RegisterNameStatus::SUCCESS; - auto accountInfo = accounts.find(accountId); - if (accountInfo != accounts.end() && accountInfo->second.registeredName.empty()) { + auto account = accounts.find(accountId); + if (account != accounts.end() && account->second.first.registeredName.empty()) { auto conf = linked.getAccountConfig(accountId); username_changed = true; new_username = name; @@ -533,9 +539,26 @@ NewAccountModelPimpl::slotMigrationEnded(const std::string& accountId, bool ok) } void -NewAccountModelPimpl::addToAccounts(const std::string& accountId) -{ - auto it = accounts.emplace(accountId, account::Info()); +NewAccountModelPimpl::addToAccounts(const std::string& accountId, + std::shared_ptr<Database> db) +{ + if (db == nullptr) { + try { + auto appPath = authority::storage::getPath(); + auto dbName = QString::fromStdString(accountId + "/history"); + db = DatabaseFactory::create<Database>(dbName, appPath); + // create the profiles path if necessary + QDir profilesDir(appPath + QString::fromStdString(accountId) + "/profiles"); + if (!profilesDir.exists()) { + profilesDir.mkpath("."); + } + } catch (const std::runtime_error& e) { + qWarning() << e.what(); + return; + } + } + + auto it = accounts.emplace(accountId, std::make_pair(account::Info(), db)); if (!it.second) { qDebug("failed to add new account: id already present in map"); @@ -543,45 +566,26 @@ NewAccountModelPimpl::addToAccounts(const std::string& accountId) } // Init profile - account::Info& newAcc = (it.first)->second; - newAcc.id = accountId; + account::Info& newAccInfo = (it.first)->second.first; + newAccInfo.id = accountId; + newAccInfo.profileInfo.avatar = authority::storage::getAccountAvatar(accountId); // Fill account::Info struct with details from daemon MapStringString details = ConfigurationManager::instance().getAccountDetails(accountId.c_str()); - newAcc.fromDetails(details); - - // Add profile to database - std::string accountType = newAcc.profileInfo.type == profile::Type::RING ? - DRing::Account::ProtocolNames::RING : - DRing::Account::ProtocolNames::SIP; - if (accountType == DRing::Account::ProtocolNames::SIP || !newAcc.profileInfo.uri.empty()) { - auto accountProfileId = authority::database::getOrInsertProfile(database, - newAcc.profileInfo.uri, - accountId, - true, - accountType, - newAcc.profileInfo.alias, - ""); - - // Retrieve avatar from database - newAcc.profileInfo.avatar = authority::database::getAvatarForProfileId(database, accountProfileId); - - // Retrieve alias from database - newAcc.profileInfo.alias = authority::database::getAliasForProfileId(database, accountProfileId); - } + newAccInfo.fromDetails(details); // Init models for this account - newAcc.callModel = std::make_unique<NewCallModel>(newAcc, callbacksHandler); - newAcc.contactModel = std::make_unique<ContactModel>(newAcc, database, callbacksHandler, behaviorController); - newAcc.conversationModel = std::make_unique<ConversationModel>(newAcc, lrc, database, callbacksHandler, behaviorController); - newAcc.peerDiscoveryModel = std::make_unique<PeerDiscoveryModel>(callbacksHandler, accountId); - newAcc.deviceModel = std::make_unique<NewDeviceModel>(newAcc, callbacksHandler); - newAcc.codecModel = std::make_unique<NewCodecModel>(newAcc, callbacksHandler); - newAcc.accountModel = &linked; + newAccInfo.accountModel = &linked; + newAccInfo.callModel = std::make_unique<NewCallModel>(newAccInfo, callbacksHandler); + newAccInfo.contactModel = std::make_unique<ContactModel>(newAccInfo, *db, callbacksHandler, behaviorController); + newAccInfo.conversationModel = std::make_unique<ConversationModel>(newAccInfo, lrc, *db, callbacksHandler, behaviorController); + newAccInfo.peerDiscoveryModel = std::make_unique<PeerDiscoveryModel>(callbacksHandler, accountId); + newAccInfo.deviceModel = std::make_unique<NewDeviceModel>(newAccInfo, callbacksHandler); + newAccInfo.codecModel = std::make_unique<NewCodecModel>(newAccInfo, callbacksHandler); MapStringString volatileDetails = ConfigurationManager::instance().getVolatileAccountDetails(accountId.c_str()); std::string daemonStatus = volatileDetails[DRing::Account::ConfProperties::Registration::STATUS].toStdString(); - newAcc.status = lrc::api::account::to_status(daemonStatus); + newAccInfo.status = lrc::api::account::to_status(daemonStatus); } void @@ -589,16 +593,19 @@ NewAccountModelPimpl::removeFromAccounts(const std::string& accountId) { /* Update db before waiting for the client to stop using the structs is fine as long as we don't free anything */ - auto accountInfo = accounts.find(accountId); - if (accountInfo == accounts.end()) { + auto account = accounts.find(accountId); + if (account == accounts.end()) { return; } - authority::database::removeAccount(database, accountId); + auto& accountInfo = account->second.first; + auto& accountDb = *(account->second.second); + accountDb.remove(); + authority::storage::removeAccount(accountId); /* Inform client about account removal. Do *not* free account structures before we are sure that the client stopped using it, otherwise we might get into use-after-free troubles. */ - accounts[accountId].valid = false; + accountInfo.valid = false; emit linked.accountRemoved(accountId); #ifdef CHK_FREEABLE_BEFORE_ERASE_ACCOUNT @@ -879,58 +886,12 @@ NewAccountModel::setTopAccount(const std::string& accountId) std::string NewAccountModel::accountVCard(const std::string& accountId, bool compressImage) const { - auto accountInfo = pimpl_->accounts.find(accountId); - if (accountInfo == pimpl_->accounts.end()) { + auto account = pimpl_->accounts.find(accountId); + if (account == pimpl_->accounts.end()) { return {}; } - 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::UID; - vCardStr += ":"; - vCardStr += accountInfo->second.id; - vCardStr += vCard::Delimiter::END_LINE_TOKEN; - vCardStr += vCard::Property::FORMATTED_NAME; - vCardStr += ":"; - vCardStr += accountInfo->second.profileInfo.alias; - vCardStr += vCard::Delimiter::END_LINE_TOKEN; - if (accountInfo->second.profileInfo.type == profile::Type::RING) { - vCardStr += vCard::Property::TELEPHONE; - vCardStr += vCard::Delimiter::SEPARATOR_TOKEN; - vCardStr += "other:ring:"; - vCardStr += accountInfo->second.profileInfo.uri; - vCardStr += vCard::Delimiter::END_LINE_TOKEN; - } else { - vCardStr += vCard::Property::TELEPHONE; - vCardStr += accountInfo->second.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 += compressImage ? "TYPE=JPEG:" : "TYPE=PNG:"; - vCardStr += compressImage ? compressedAvatar(accountInfo->second.profileInfo.avatar) : accountInfo->second.profileInfo.avatar; - vCardStr += vCard::Delimiter::END_LINE_TOKEN; - vCardStr += vCard::Delimiter::END_TOKEN; - return vCardStr; -} -std::string NewAccountModel::compressedAvatar(const std::string& img) const -{ - QImage image; - const bool ret = image.loadFromData(QByteArray::fromBase64(img.c_str()), 0); - if (!ret) { - qDebug() << "vCard image loading failed"; - return img; - } - QByteArray bArray; - QBuffer buffer(&bArray); - buffer.open(QIODevice::WriteOnly); - image.scaled({128,128}).save(&buffer, "JPEG", 90); - auto b64Img = bArray.toBase64().trimmed(); - return std::string(b64Img.constData(), b64Img.length()); + auto& accountInfo = account->second.first; + return authority::storage::vcard::profileToVcard(accountInfo.profileInfo, compressImage); } } // namespace lrc diff --git a/src/newcallmodel.cpp b/src/newcallmodel.cpp index dc94bf54..a1fbc963 100644 --- a/src/newcallmodel.cpp +++ b/src/newcallmodel.cpp @@ -29,6 +29,7 @@ #include "api/contact.h" #include "api/contactmodel.h" #include "api/newaccountmodel.h" +#include "authority/storagehelper.h" #include "dbus/callmanager.h" #include "vcard.h" #include "video/renderer.h" @@ -183,7 +184,7 @@ NewCallModel::getCallFromURI(const std::string& uri, bool notOver) const // peer url = ring:uri or sip number auto url = (owner.profileInfo.type != profile::Type::SIP && uri.find("ring:") == std::string::npos) ? "ring:" + uri : uri; for (const auto& call: pimpl_->calls) { - if (call.second->peer == url) { + if (call.second->peerUri == url) { if (!notOver || call.second->status != call::Status::ENDED) return *call.second; } @@ -198,7 +199,7 @@ NewCallModel::getConferenceFromURI(const std::string& uri) const if (call.second->type == call::Type::CONFERENCE) { QStringList callList = CallManager::instance().getParticipantList(call.first.c_str()); foreach(const auto& callId, callList) { - if (pimpl_->calls[callId.toStdString()]->peer == uri) { + if (pimpl_->calls[callId.toStdString()]->peerUri == uri) { return *call.second; } } @@ -214,29 +215,29 @@ NewCallModel::getCall(const std::string& uid) const } std::string -NewCallModel::createCall(const std::string& url, bool isAudioOnly) +NewCallModel::createCall(const std::string& uri, bool isAudioOnly) { #ifdef ENABLE_LIBWRAP auto callId = isAudioOnly ? CallManager::instance().placeCall(owner.id.c_str(), - url.c_str(), + uri.c_str(), {{"AUDIO_ONLY", "true"}}) - : CallManager::instance().placeCall(owner.id.c_str(), url.c_str()); + : CallManager::instance().placeCall(owner.id.c_str(), uri.c_str()); #else // dbus // do not use auto here (QDBusPendingReply<QString>) QString callId = isAudioOnly ? CallManager::instance().placeCallWithDetails(owner.id.c_str(), - url.c_str(), + uri.c_str(), {{"AUDIO_ONLY", "true"}}) - : CallManager::instance().placeCall(owner.id.c_str(), url.c_str()); + : CallManager::instance().placeCall(owner.id.c_str(), uri.c_str()); #endif // ENABLE_LIBWRAP if (callId.isEmpty()) { - qDebug() << "no call placed between (account :" << owner.id.c_str() << ", contact :" << url.c_str() << ")"; + qDebug() << "no call placed between (account: " << owner.id.c_str() << ", contact: " << uri.c_str() << ")"; return ""; } auto callInfo = std::make_shared<call::Info>(); callInfo->id = callId.toStdString(); - callInfo->peer = url; + callInfo->peerUri = uri; callInfo->isOutgoing = true; callInfo->status = call::Status::SEARCHING; callInfo->type = call::Type::DIALOG; @@ -393,22 +394,8 @@ NewCallModel::getFormattedCallDuration(const std::string& callId) const if (startTime.time_since_epoch().count() == 0) return "00:00"; auto now = std::chrono::steady_clock::now(); auto d = std::chrono::duration_cast<std::chrono::seconds>( - now.time_since_epoch() - startTime.time_since_epoch()).count(); - - std::string formattedString; - auto minutes = d / 60; - auto seconds = d % 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; + now.time_since_epoch() - startTime.time_since_epoch()).count(); + return authority::storage::getFormattedCallDuration(d); } bool @@ -428,7 +415,6 @@ NewCallModel::getSIPCallStatusString(const short& statusCode) return ""; } - NewCallModelPimpl::NewCallModelPimpl(const NewCallModel& linked, const CallbacksHandler& callbacksHandler) : linked(linked) , callbacksHandler(callbacksHandler) @@ -467,9 +453,9 @@ NewCallModelPimpl::initCallFromDaemon() callInfo->startTime = now - std::chrono::seconds(diff); callInfo->status = call::to_status(details["CALL_STATE"].toStdString()); auto endId = details["PEER_NUMBER"].indexOf("@"); - callInfo->peer = details["PEER_NUMBER"].left(endId).toStdString(); + callInfo->peerUri = details["PEER_NUMBER"].left(endId).toStdString(); if (linked.owner.profileInfo.type == lrc::api::profile::Type::RING) { - callInfo->peer = "ring:" + callInfo->peer; + callInfo->peerUri = "ring:" + callInfo->peerUri; } callInfo->videoMuted = details["VIDEO_MUTED"] == "true"; callInfo->audioMuted = details["AUDIO_MUTED"] == "true"; @@ -510,7 +496,6 @@ NewCallModelPimpl::initConferencesFromDaemon() } } - void NewCallModel::sendSipMessage(const std::string& callId, const std::string& body) const { @@ -536,16 +521,18 @@ NewCallModel::hangupCallsAndConferences() void NewCallModelPimpl::slotIncomingCall(const std::string& accountId, const std::string& callId, const std::string& fromId) { - if (linked.owner.id != accountId) return; + if (linked.owner.id != accountId) { + return; + } // do not use auto here (QDBusPendingReply<MapStringString>) MapStringString callDetails = CallManager::instance().getCallDetails(callId.c_str()); auto callInfo = std::make_shared<call::Info>(); callInfo->id = callId; - // peer url = ring:uri or sip number - auto url = (linked.owner.profileInfo.type != profile::Type::SIP && fromId.find("ring:") == std::string::npos) ? "ring:" + fromId : fromId; - callInfo->peer = url; + // peer uri = ring:<jami_id> or sip number + auto uri = (linked.owner.profileInfo.type != profile::Type::SIP && fromId.find("ring:") == std::string::npos) ? "ring:" + fromId : fromId; + callInfo->peerUri = uri; callInfo->isOutgoing = false; callInfo->status = call::Status::INCOMING_RINGING; callInfo->type = call::Type::DIALOG; diff --git a/src/typedefs.h b/src/typedefs.h index 7d8656be..ab429508 100644 --- a/src/typedefs.h +++ b/src/typedefs.h @@ -194,7 +194,6 @@ private: #pragma GCC diagnostic ignored "-Wunused-function" #endif - #define DECLARE_ENUM_FLAGS(T)\ DO_PRAGMA(GCC diagnostic push)\ DO_PRAGMA(GCC diagnostic ignored "-Wunused-function")\ @@ -203,3 +202,6 @@ __attribute__ ((unused)) static FlagPack<T> operator|(const T& first, const T& s return p | second; \ } \ DO_PRAGMA(GCC diagnostic pop) + +#include <functional> +typedef std::function<void()> MigrationCb; \ No newline at end of file diff --git a/src/uri.cpp b/src/uri.cpp index 4ceae91e..e1d97b95 100644 --- a/src/uri.cpp +++ b/src/uri.cpp @@ -200,7 +200,7 @@ QString URI::hostname() const */ bool URI::hasHostname() const { - return hostname().isEmpty(); + return !hostname().isEmpty(); } /** @@ -443,6 +443,25 @@ QString URI::userinfo() const return pimpl_->m_Userinfo; } +void URI::setUserinfo(const QString & userinfo) +{ + pimpl_->m_Userinfo = userinfo; +} + +void URI::setHostname(const QString& hostname) +{ + pimpl_->m_ExtHostname = hostname; +} + +void URI::setPort(const QString& port) +{ + try { + pimpl_->m_Port = port.toInt(); + } catch (...) { + qWarning() << "Can't convert port to integer"; + } +} + /** * Sometime, some metadata can be used to deduce the scheme even if it wasn't * originally known. diff --git a/src/uri.h b/src/uri.h index 46d5e076..2c146608 100644 --- a/src/uri.h +++ b/src/uri.h @@ -174,6 +174,9 @@ public: ProtocolHint protocolHint() const; // Setter + void setUserinfo(const QString& userinfo); + void setHostname(const QString& hostname); + void setPort(const QString& port); void setSchemeType(SchemeType t); // Converter diff --git a/src/vcard.h b/src/vcard.h index a5b7c111..84fc64a6 100644 --- a/src/vcard.h +++ b/src/vcard.h @@ -64,6 +64,11 @@ struct Property { constexpr static const char* TIME_ZONE = "TZ"; constexpr static const char* TITLE = "TITLE"; constexpr static const char* URL = "URL"; + constexpr static const char* BASE64 = "ENCODING=BASE64"; + constexpr static const char* TYPE_PNG = "TYPE=PNG"; + constexpr static const char* TYPE_JPEG = "TYPE=JPEG"; + constexpr static const char* PHOTO_PNG = "PHOTO;ENCODING=BASE64;TYPE=PNG"; + constexpr static const char* PHOTO_JPEG = "PHOTO;ENCODING=BASE64;TYPE=JPEG"; constexpr static const char* X_RINGACCOUNT = "X-RINGACCOUNTID"; }; -- GitLab