diff --git a/src/account.h b/src/account.h index 0b00e46b48f12da5540967e5e2a2e2a760127ebe..7ebd4fb4ec79905ddb69377e7ed181ebf62ad98c 100644 --- a/src/account.h +++ b/src/account.h @@ -302,6 +302,11 @@ public: */ virtual void connectivityChanged() {}; + virtual void onNewGitCommit(const std::string& /*peer*/, + const std::string& /*deviceId*/, + const std::string& /*conversationId*/, + const std::string& /*commitId*/) {}; + /** * Helper function used to load the default codec order from the codec factory */ diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index a366ed602d516961a535d9a31e1789de0eb35c44..5c4479ec6e2fdc14fcb9e1ef44e5f8513f5954f1 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -11,6 +11,7 @@ list (APPEND Source_Files__client "${CMAKE_CURRENT_SOURCE_DIR}/videomanager.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/videomanager.h" "${CMAKE_CURRENT_SOURCE_DIR}/plugin_manager_interface.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/conversation_interface.cpp" ) set (Source_Files__client ${Source_Files__client} PARENT_SCOPE) \ No newline at end of file diff --git a/src/client/Makefile.am b/src/client/Makefile.am index 736a894eebc3fc37166d273048bc38749816546b..fc5ad392f0255b5736160caadce5ec966a560240 100644 --- a/src/client/Makefile.am +++ b/src/client/Makefile.am @@ -21,6 +21,7 @@ libclient_la_SOURCES = \ callmanager.cpp \ configurationmanager.cpp \ datatransfer.cpp \ + conversation_interface.cpp \ $(PLUGIN_SRC) \ $(PRESENCE_SRC) \ $(VIDEO_SRC) diff --git a/src/client/conversation_interface.cpp b/src/client/conversation_interface.cpp new file mode 100644 index 0000000000000000000000000000000000000000..75475ca86889c636662e4add749a84a6a892c2fe --- /dev/null +++ b/src/client/conversation_interface.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2013-2019 Savoir-faire Linux Inc. + * + * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "conversation_interface.h" + +#include <cerrno> +#include <sstream> +#include <cstring> + +#include "logger.h" +#include "manager.h" +#include "jamidht/jamiaccount.h" + +namespace DRing { + +std::string +startConversation(const std::string& accountId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + return acc->startConversation(); + return {}; +} + +void +acceptConversationRequest(const std::string& accountId, const std::string& conversationId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + acc->acceptConversationRequest(conversationId); +} + +void +declineConversationRequest(const std::string& accountId, const std::string& conversationId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + acc->declineConversationRequest(conversationId); +} + +bool +removeConversation(const std::string& accountId, const std::string& conversationId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + return acc->removeConversation(conversationId); + return false; +} + +std::vector<std::string> +getConversations(const std::string& accountId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + return acc->getConversations(); + return {}; +} + +std::vector<std::map<std::string, std::string>> +getConversationRequests(const std::string& accountId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + return acc->getConversationRequests(); + return {}; +} + +// Member management +bool +addConversationMember(const std::string& accountId, + const std::string& conversationId, + const std::string& contactUri) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + return acc->addConversationMember(conversationId, contactUri); + return false; +} + +bool +removeConversationMember(const std::string& accountId, + const std::string& conversationId, + const std::string& contactUri) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + return acc->removeConversationMember(conversationId, contactUri); + return false; +} + +std::vector<std::map<std::string, std::string>> +getConversationMembers(const std::string& accountId, const std::string& conversationId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + return acc->getConversationMembers(conversationId); + return {}; +} + +// Message send/load +void +sendMessage(const std::string& accountId, + const std::string& conversationId, + const std::string& message, + const std::string& parent) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + acc->sendMessage(conversationId, message, parent); +} + +void +loadConversationMessages(const std::string& accountId, + const std::string& conversationId, + const std::string& fromMessage, + size_t n) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + acc->loadConversationMessages(conversationId, fromMessage, n); +} + +} // namespace DRing diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp index d4e05aa3185b6cfae4aecb87b771ab070b0eb6e0..689f7812947d83bdbd0487346ebb69829655cc5a 100644 --- a/src/client/ring_signal.cpp +++ b/src/client/ring_signal.cpp @@ -125,6 +125,12 @@ getSignalHandlers() exported_callback<DRing::VideoSignal::DeviceAdded>(), exported_callback<DRing::VideoSignal::ParametersChanged>(), #endif + + /* Conversation */ + exported_callback<DRing::ConversationSignal::ConversationLoaded>(), + exported_callback<DRing::ConversationSignal::MessageReceived>(), + exported_callback<DRing::ConversationSignal::ConversationRequestReceived>(), + exported_callback<DRing::ConversationSignal::ConversationReady>(), }; return handlers; diff --git a/src/client/ring_signal.h b/src/client/ring_signal.h index 9326d899af8faf36c449b8d96c132fba896f4841..2d75e0bac0226f56f689bba25903a87a0839ea20 100644 --- a/src/client/ring_signal.h +++ b/src/client/ring_signal.h @@ -26,6 +26,7 @@ #include "callmanager_interface.h" #include "configurationmanager_interface.h" +#include "conversation_interface.h" #include "presencemanager_interface.h" #include "datatransfer_interface.h" diff --git a/src/dring/conversation_interface.h b/src/dring/conversation_interface.h new file mode 100644 index 0000000000000000000000000000000000000000..890fdf3ac91b918587fdb689aaac072d018015f0 --- /dev/null +++ b/src/dring/conversation_interface.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2013-2019 Savoir-faire Linux Inc. + * + * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef DRING_CONVERSATIONI_H +#define DRING_CONVERSATIONI_H + +#include "def.h" + +#include <vector> +#include <map> +#include <string> + +#include "dring.h" + +namespace DRing { + +// Conversation management +DRING_PUBLIC std::string startConversation(const std::string& accountId); +DRING_PUBLIC void acceptConversationRequest(const std::string& accountId, + const std::string& conversationId); +DRING_PUBLIC void declineConversationRequest(const std::string& accountId, + const std::string& conversationId); +DRING_PUBLIC bool removeConversation(const std::string& accountId, + const std::string& conversationId); +DRING_PUBLIC std::vector<std::string> getConversations(const std::string& accountId); +DRING_PUBLIC std::vector<std::map<std::string, std::string>> getConversationRequests( + const std::string& accountId); + +// Member management +DRING_PUBLIC bool addConversationMember(const std::string& accountId, + const std::string& conversationId, + const std::string& contactUri); +DRING_PUBLIC bool removeConversationMember(const std::string& accountId, + const std::string& conversationId, + const std::string& contactUri); +DRING_PUBLIC std::vector<std::map<std::string, std::string>> getConversationMembers( + const std::string& accountId, const std::string& conversationId); + +// Message send/load +DRING_PUBLIC void sendMessage(const std::string& accountId, + const std::string& conversationId, + const std::string& message, + const std::string& parent); +DRING_PUBLIC void loadConversationMessages(const std::string& accountId, + const std::string& conversationId, + const std::string& fromMessage, + size_t n); + +struct DRING_PUBLIC ConversationSignal +{ + struct DRING_PUBLIC ConversationLoaded + { + constexpr static const char* name = "ConversationLoaded"; + using cb_type = void(const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::vector<std::map<std::string, std::string>> /*messages*/); + }; + struct DRING_PUBLIC MessageReceived + { + constexpr static const char* name = "MessageReceived"; + using cb_type = void(const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/); + }; + struct DRING_PUBLIC ConversationRequestReceived + { + constexpr static const char* name = "ConversationRequestReceived"; + using cb_type = void(const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/); + }; + struct DRING_PUBLIC ConversationReady + { + constexpr static const char* name = "ConversationReady"; + using cb_type = void(const std::string& /*accountId*/, + const std::string& /* conversationId */); + }; +}; + +} // namespace DRing + +#endif // DRING_CONVERSATIONI_H diff --git a/src/gittransport.cpp b/src/gittransport.cpp index 6caedddf26d5c9ee93cefb5d020f551bb327ec9e..473e6053354cee760a74c5b07dd094d7a2c4f93b 100644 --- a/src/gittransport.cpp +++ b/src/gittransport.cpp @@ -47,7 +47,7 @@ generateRequest(git_buf* request, const std::string& cmd, const std::string_view + cmd.size() /* followed by the command */ + 1 /* space */ + conversationId.size() /* conversation */ - + nullSeparator.size() /* \0 */ + + 1 /* \0 */ + HOST_TAG.size() + deviceId.size() /* device */ + nullSeparator.size() /* \0 */; @@ -74,6 +74,7 @@ sendCmd(P2PStream* s) if ((res = s->socket->write(reinterpret_cast<const unsigned char*>(request.ptr), request.size, ec))) { + s->sent_command = 1; git_buf_free(&request); return res; } diff --git a/src/jamidht/CMakeLists.txt b/src/jamidht/CMakeLists.txt index 6a87b1cd7b1c54687534535f2b0e12a9959ea5fd..143342ccd01b7f58371e1e2c850c45813f11b666 100644 --- a/src/jamidht/CMakeLists.txt +++ b/src/jamidht/CMakeLists.txt @@ -17,6 +17,8 @@ list (APPEND Source_Files__jamidht "${CMAKE_CURRENT_SOURCE_DIR}/connectionmanager.h" "${CMAKE_CURRENT_SOURCE_DIR}/conversationrepository.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/conversationrepository.h" + "${CMAKE_CURRENT_SOURCE_DIR}/conversation.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/conversation.h" "${CMAKE_CURRENT_SOURCE_DIR}/channeled_transport.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/channeled_transport.h" "${CMAKE_CURRENT_SOURCE_DIR}/channeled_transfers.cpp" diff --git a/src/jamidht/Makefile.am b/src/jamidht/Makefile.am index 43eadf64b83733c2e8bd2eaf98aa9a8d6185f0c6..47b477c0101dda90a2338f5670d8c733e0c7ef94 100644 --- a/src/jamidht/Makefile.am +++ b/src/jamidht/Makefile.am @@ -19,6 +19,8 @@ libringacc_la_SOURCES = \ channeled_transport.cpp \ channeled_transfers.h \ channeled_transfers.cpp \ + conversation.h \ + conversation.cpp \ conversationrepository.h \ conversationrepository.cpp \ gitserver.h \ diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c5a88b358260f101cf517d3e1cfe60fa0fd61c44 --- /dev/null +++ b/src/jamidht/conversation.cpp @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2014-2019 Savoir-faire Linux Inc. + * + * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 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 <https://www.gnu.org/licenses/>. + */ +#include "conversation.h" + +#include "fileutils.h" +#include "jamiaccount.h" +#include "conversationrepository.h" + +#include <json/json.h> + +namespace jami { + +class Conversation::Impl +{ +public: + Impl(const std::weak_ptr<JamiAccount>& account, const std::string& conversationId) + : account_(account) + { + if (conversationId.empty()) + repository_ = ConversationRepository::createConversation(account); + else + repository_ = std::make_unique<ConversationRepository>(account, conversationId); + if (!repository_) { + throw std::logic_error("Couldn't create repository"); + } + } + + Impl(const std::weak_ptr<JamiAccount>& account, + const std::string& remoteDevice, + const std::string& conversationId) + : account_(account) + { + repository_ = ConversationRepository::cloneConversation(account, + remoteDevice, + conversationId); + if (!repository_) { + throw std::logic_error("Couldn't clone repository"); + } + } + ~Impl() = default; + + std::unique_ptr<ConversationRepository> repository_; + std::weak_ptr<JamiAccount> account_; + std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "", + const std::string& toMessage = "", + size_t n = 0); +}; + +std::vector<std::map<std::string, std::string>> +Conversation::Impl::loadMessages(const std::string& fromMessage, + const std::string& toMessage, + size_t n) +{ + if (!repository_) + return {}; + std::vector<ConversationCommit> convCommits; + if (toMessage.empty()) + convCommits = repository_->logN(fromMessage, n); + else + convCommits = repository_->log(fromMessage, toMessage); + std::vector<std::map<std::string, std::string>> result = {}; + for (const auto& commit : convCommits) { + auto authorDevice = commit.author.email; + auto cert = tls::CertificateStore::instance().getCertificate(authorDevice); + if (!cert && cert->issuer) { + JAMI_WARN("No author found for commit %s", commit.id.c_str()); + } + auto authorId = cert->issuer->getId().toString(); + std::string parents; + auto parentsSize = commit.parents.size(); + for (auto i = 0; i < parentsSize; ++i) { + parents += commit.parents[i]; + if (i != parentsSize - 1) + parents += ","; + } + std::string type {}; + if (parentsSize > 1) { + type = "merge"; + } + // TODO check diff for member + std::string body {}; + if (type.empty()) { + std::string err; + Json::Value cm; + Json::CharReaderBuilder rbuilder; + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + if (reader->parse(commit.commit_msg.data(), + commit.commit_msg.data() + commit.commit_msg.size(), + &cm, + &err)) { + type = cm["type"].asString(); + body = cm["body"].asString(); + } else { + JAMI_WARN("%s", err.c_str()); + } + } + std::map<std::string, std::string> message {{"id", commit.id}, + {"parents", parents}, + {"author", authorId}, + {"type", type}, + {"body", body}, + {"timestamp", std::to_string(commit.timestamp)}}; + result.emplace_back(message); + } + return result; +} + +Conversation::Conversation(const std::weak_ptr<JamiAccount>& account, + const std::string& conversationId) + : pimpl_ {new Impl {account, conversationId}} +{} + +Conversation::Conversation(const std::weak_ptr<JamiAccount>& account, + const std::string& remoteDevice, + const std::string& conversationId) + : pimpl_ {new Impl {account, remoteDevice, conversationId}} +{} + +Conversation::~Conversation() {} + +std::string +Conversation::id() const +{ + return pimpl_->repository_ ? pimpl_->repository_->id() : ""; +} + +std::string +Conversation::addMember(const std::string& contactUri) +{ + // Add member files and commit + return pimpl_->repository_->addMember(contactUri); +} + +bool +Conversation::removeMember(const std::string& contactUri) +{ + // TODO + return true; +} + +std::vector<std::map<std::string, std::string>> +Conversation::getMembers() +{ + std::vector<std::map<std::string, std::string>> result; + auto shared = pimpl_->account_.lock(); + if (!shared) + return result; + + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + + pimpl_->repository_->id(); + auto adminsPath = repoPath + DIR_SEPARATOR_STR + "admins"; + auto membersPath = repoPath + DIR_SEPARATOR_STR + "members"; + for (const auto& certificate : fileutils::readDirectory(adminsPath)) { + if (certificate.find(".crt") == std::string::npos) { + JAMI_WARN("Incorrect file found: %s/%s", adminsPath.c_str(), certificate.c_str()); + continue; + } + std::map<std::string, std::string> + details {{"uri", certificate.substr(0, certificate.size() - std::string(".crt").size())}, + {"role", "admin"}}; + result.emplace_back(details); + } + for (const auto& certificate : fileutils::readDirectory(membersPath)) { + if (certificate.find(".crt") == std::string::npos) { + JAMI_WARN("Incorrect file found: %s/%s", membersPath.c_str(), certificate.c_str()); + continue; + } + std::map<std::string, std::string> + details {{"uri", certificate.substr(0, certificate.size() - std::string(".crt").size())}, + {"role", "member"}}; + result.emplace_back(details); + } + + return result; +} + +std::string +Conversation::join() +{ + auto shared = pimpl_->account_.lock(); + if (!shared) + return {}; + return pimpl_->repository_->join(); +} + +bool +Conversation::isMember(const std::string& uri, bool includeInvited) +{ + auto shared = pimpl_->account_.lock(); + if (!shared) + return false; + + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + + pimpl_->repository_->id(); + auto invitedPath = repoPath + DIR_SEPARATOR_STR + "invited"; + auto adminsPath = repoPath + DIR_SEPARATOR_STR + "admins"; + auto membersPath = repoPath + DIR_SEPARATOR_STR + "members"; + std::vector<std::string> pathsToCheck = {adminsPath, membersPath}; + if (includeInvited) + pathsToCheck.emplace_back(invitedPath); + for (const auto& path : pathsToCheck) { + for (const auto& certificate : fileutils::readDirectory(path)) { + if (certificate.find(".crt") == std::string::npos) { + JAMI_WARN("Incorrect file found: %s/%s", path.c_str(), certificate.c_str()); + continue; + } + auto crtUri = certificate.substr(0, certificate.size() - std::string(".crt").size()); + if (crtUri == uri) + return true; + } + } + + return false; +} + +std::string +Conversation::sendMessage(const std::string& message, + const std::string& type, + const std::string& parent) +{ + Json::Value json; + json["body"] = message; + json["type"] = type; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + return pimpl_->repository_->commitMessage(Json::writeString(wbuilder, json)); +} + +std::vector<std::map<std::string, std::string>> +Conversation::loadMessages(const std::string& fromMessage, size_t n) +{ + return pimpl_->loadMessages(fromMessage, "", n); +} + +std::vector<std::map<std::string, std::string>> +Conversation::loadMessages(const std::string& fromMessage, const std::string& toMessage) +{ + return pimpl_->loadMessages(fromMessage, toMessage, 0); +} + +bool +Conversation::fetchFrom(const std::string& uri) +{ + // TODO check if device id or account id + return pimpl_->repository_->fetch(uri); +} + +bool +Conversation::mergeHistory(const std::string& uri) +{ + auto remoteHead = pimpl_->repository_->remoteHead(uri); + if (remoteHead.empty()) { + JAMI_WARN("Could not get HEAD of %s", uri.c_str()); + return false; + } + + // In the future, the diff should be analyzed to know if the + // history presented by the peer is correct or not. + + if (!pimpl_->repository_->merge(remoteHead)) { + JAMI_ERR("Could not merge history with %s", uri.c_str()); + return false; + } + JAMI_DBG("Successfully merge history with %s", uri.c_str()); + return true; +} + +} // namespace jami \ No newline at end of file diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h new file mode 100644 index 0000000000000000000000000000000000000000..075415e4d5bec2e4c24f0c4b502c2683911df99c --- /dev/null +++ b/src/jamidht/conversation.h @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2014-2019 Savoir-faire Linux Inc. + * + * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 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 <https://www.gnu.org/licenses/>. + */ +#pragma once + +#include <string> +#include <vector> +#include <map> +#include <memory> + +namespace jami { + +class JamiAccount; +class ConversationRepository; + +class Conversation +{ +public: + Conversation(const std::weak_ptr<JamiAccount>& account, const std::string& conversationId = ""); + Conversation(const std::weak_ptr<JamiAccount>& account, + const std::string& remoteDevice, + const std::string& conversationId); + ~Conversation(); + + std::string id() const; + + // Member management + /** + * Add conversation member + * @param uri Member to add + * @return Commit id or empty if fails + */ + std::string addMember(const std::string& contactUri); + bool removeMember(const std::string& contactUri); + /** + * @return a vector of member details: + * { + * "uri":"xxx", + * "role":"member/admin", + * "lastRead":"id" + * ... + * } + */ + std::vector<std::map<std::string, std::string>> getMembers(); + + /** + * Join a conversation + * @return commit id to send + */ + std::string join(); + + /** + * Test if an URI is a member + * @param uri URI to test + * @return true if uri is a member + */ + bool isMember(const std::string& uri, bool includInvited = false); + + // Message send + std::string sendMessage(const std::string& message, + const std::string& type = "text/plain", + const std::string& parent = ""); + /** + * Get a range of messages + * @param from The most recent message ("" = last (default)) + * @param n Number of messages to get (0 = no limit (default)) + * @return the range of messages + */ + std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "", + size_t n = 0); + /** + * Get a range of messages + * @param fromMessage The most recent message ("" = last (default)) + * @param toMessage The oldest message ("" = last (default)), no limit + * @return the range of messages + */ + std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "", + const std::string& toMessage = ""); + + /** + * Get new messages from peer + * @param uri the peer + * @return if the operation was successful + */ + bool fetchFrom(const std::string& uri); + + /** + * Analyze if merge is possible and merge history + * @param uri the peer + * @return if the operation was successful + */ + bool mergeHistory(const std::string& uri); + +private: + class Impl; + std::unique_ptr<Impl> pimpl_; +}; + +} // namespace jami diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index 1ffb887086a6e09b196f698afc2df0b29fd94163..ce0619240b5627862d9292eec80d0fd5832983bc 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -30,6 +30,7 @@ using random_device = dht::crypto::random_device; #include <fstream> #include <json/json.h> #include <regex> +#include <exception> using namespace std::string_view_literals; constexpr auto DIFF_REGEX = " +\\| +[0-9]+.*"sv; @@ -45,15 +46,13 @@ public: { auto shared = account.lock(); if (!shared) - return; + throw std::logic_error("No account detected when loading conversation"); auto path = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + id_; git_repository* repo = nullptr; // TODO share this repo with GitServer - if (git_repository_open(&repo, path.c_str()) != 0) { - JAMI_WARN("Couldn't open %s", path.c_str()); - return; - } + if (git_repository_open(&repo, path.c_str()) != 0) + throw std::logic_error("Couldn't open " + path); repository_ = {std::move(repo), git_repository_free}; } ~Impl() = default; @@ -68,6 +67,8 @@ public: GitDiff diff(const std::string& idNew, const std::string& idOld) const; std::string diffStats(const GitDiff& diff) const; + std::vector<ConversationCommit> log(const std::string& from, const std::string& to, unsigned n); + std::weak_ptr<JamiAccount> account_; const std::string id_; GitRepository repository_ {nullptr, git_repository_free}; @@ -93,6 +94,27 @@ create_empty_repository(const std::string& path) return {std::move(repo), git_repository_free}; } +/** + * Add all files to index + * @param repo + * @return if operation is successful + */ +bool +git_add_all(git_repository* repo) +{ + // git add -A + git_index* index_ptr = nullptr; + git_strarray array = {0}; + if (git_repository_index(&index_ptr, repo) < 0) { + JAMI_ERR("Could not open repository index"); + return false; + } + GitIndex index {index_ptr, git_index_free}; + git_index_add_all(index.get(), &array, 0, nullptr, nullptr); + git_index_write(index.get()); + return true; +} + /** * Adds initial files. This adds the certificate of the account in the /admins directory * the device's key in /devices and the CRLs in /CRLs. @@ -172,19 +194,10 @@ add_initial_files(GitRepository& repo, const std::shared_ptr<JamiAccount>& accou file.close(); } - // git add -A - git_index* index_ptr = nullptr; - git_strarray array = {0}; - - if (git_repository_index(&index_ptr, repo.get()) < 0) { - JAMI_ERR("Could not open repository index"); + if (!git_add_all(repo.get())) { return false; } - GitIndex index {index_ptr, git_index_free}; - git_index_add_all(index.get(), &array, 0, nullptr, nullptr); - git_index_write(index.get()); - JAMI_INFO("Initial files added in %s", repoPath.c_str()); return true; } @@ -529,7 +542,27 @@ ConversationRepository::Impl::commit(const std::string& msg) } GitSignature sig {sig_ptr, git_signature_free}; - // Retrieve current HEAD + // Retrieve current index + git_index* index_ptr = nullptr; + if (git_repository_index(&index_ptr, repository_.get()) < 0) { + JAMI_ERR("Could not open repository index"); + return {}; + } + GitIndex index {index_ptr, git_index_free}; + + git_oid tree_id; + if (git_index_write_tree(&tree_id, index.get()) < 0) { + JAMI_ERR("Unable to write initial tree from index"); + return {}; + } + + git_tree* tree_ptr = nullptr; + if (git_tree_lookup(&tree_ptr, repository_.get(), &tree_id) < 0) { + JAMI_ERR("Could not look up initial tree"); + return {}; + } + GitTree tree = {tree_ptr, git_tree_free}; + git_oid commit_id; if (git_reference_name_to_id(&commit_id, repository_.get(), "HEAD") < 0) { JAMI_ERR("Cannot get reference for HEAD"); @@ -543,13 +576,6 @@ ConversationRepository::Impl::commit(const std::string& msg) } GitCommit head_commit {head_ptr, git_commit_free}; - git_tree* tree_ptr = nullptr; - if (git_commit_tree(&tree_ptr, head_commit.get()) < 0) { - JAMI_ERR("Could not look up initial tree"); - return {}; - } - GitTree tree {tree_ptr, git_tree_free}; - git_buf to_sign = {}; const git_commit* head_ref[1] = {head_commit.get()}; if (git_commit_create_buffer(&to_sign, @@ -605,7 +631,6 @@ ConversationRepository::Impl::diff(const std::string& idNew, const std::string& git_oid oid; git_commit* commitNew = nullptr; if (idNew == "HEAD") { - JAMI_ERR("@@@ HEAD"); if (git_reference_name_to_id(&oid, repository_.get(), "HEAD") < 0) { JAMI_ERR("Cannot get reference for HEAD"); return {nullptr, git_diff_free}; @@ -664,6 +689,89 @@ ConversationRepository::Impl::diff(const std::string& idNew, const std::string& return {diff_ptr, git_diff_free}; } +std::vector<ConversationCommit> +ConversationRepository::Impl::log(const std::string& from, const std::string& to, unsigned n) +{ + std::vector<ConversationCommit> commits {}; + + git_oid oid; + if (from.empty()) { + if (git_reference_name_to_id(&oid, repository_.get(), "HEAD") < 0) { + JAMI_ERR("Cannot get reference for HEAD"); + return commits; + } + } else { + if (git_oid_fromstr(&oid, from.c_str()) < 0) { + JAMI_ERR("Cannot get reference for commit %s", from.c_str()); + return commits; + } + } + + git_revwalk* walker_ptr = nullptr; + if (git_revwalk_new(&walker_ptr, repository_.get()) < 0 + || git_revwalk_push(walker_ptr, &oid) < 0) { + JAMI_ERR("Couldn't init revwalker for conversation %s", id_.c_str()); + return commits; + } + GitRevWalker walker {walker_ptr, git_revwalk_free}; + git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL); + + auto x = git_oid_tostr_s(&oid); + for (auto idx = 0; !git_revwalk_next(&oid, walker.get()); ++idx) { + if (n != 0 && idx == n) { + break; + } + git_commit* commit_ptr = nullptr; + std::string id = git_oid_tostr_s(&oid); + if (git_commit_lookup(&commit_ptr, repository_.get(), &oid) < 0) { + JAMI_WARN("Failed to look up commit %s", id.c_str()); + break; + } + GitCommit commit {commit_ptr, git_commit_free}; + if (id == to) { + break; + } + + const git_signature* sig = git_commit_author(commit.get()); + GitAuthor author; + author.name = sig->name; + author.email = sig->email; + std::vector<std::string> parents; + auto parentsCount = git_commit_parentcount(commit.get()); + for (unsigned int p = 0; p < parentsCount; ++p) { + std::string parent {}; + const git_oid* pid = git_commit_parent_id(commit.get(), p); + if (pid) { + parent = git_oid_tostr_s(pid); + parents.emplace_back(parent); + } + } + + auto cc = commits.emplace(commits.end(), ConversationCommit {}); + cc->id = std::move(id); + cc->commit_msg = git_commit_message(commit.get()); + cc->author = std::move(author); + cc->parents = std::move(parents); + git_buf signature = {}, signed_data = {}; + if (git_commit_extract_signature(&signature, + &signed_data, + repository_.get(), + &oid, + "signature") + < 0) { + JAMI_WARN("Could not extract signature for commit %s", id.c_str()); + } else { + cc->signature = base64::decode( + std::string(signature.ptr, signature.ptr + signature.size)); + cc->signed_content = std::vector<uint8_t>(signed_data.ptr, + signed_data.ptr + signed_data.size); + } + cc->timestamp = git_commit_time(commit.get()); + } + + return commits; +} + std::string ConversationRepository::Impl::diffStats(const GitDiff& diff) const { @@ -779,6 +887,49 @@ ConversationRepository::id() const return pimpl_->id_; } +std::string +ConversationRepository::addMember(const std::string& uri) +{ + auto account = pimpl_->account_.lock(); + if (!account) + return {}; + auto deviceId = account->currentDeviceId(); + auto name = account->getUsername(); + if (name.empty()) + name = deviceId; + + // First, we need to add the member file to the repository if not present + std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + + std::string invitedPath = repoPath + "invited"; + if (!fileutils::recursive_mkdir(invitedPath, 0700)) { + JAMI_ERR("Error when creating %s.", invitedPath.c_str()); + return {}; + } + std::string devicePath = invitedPath + DIR_SEPARATOR_STR + uri; + if (fileutils::isFile(devicePath)) { + JAMI_WARN("Member %s already present!", uri.c_str()); + return {}; + } + + auto file = fileutils::ofstream(devicePath, std::ios::trunc | std::ios::binary); + if (!file.is_open()) { + JAMI_ERR("Could not write data to %s", devicePath.c_str()); + return {}; + } + std::string path = "invited/" + uri; + if (!pimpl_->add(path.c_str())) + return {}; + Json::Value json; + json["action"] = "add"; + json["uri"] = uri; + json["type"] = "member"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + return pimpl_->commit(Json::writeString(wbuilder, json)); +} + bool ConversationRepository::fetch(const std::string& remoteDeviceId) { @@ -846,20 +997,17 @@ ConversationRepository::remoteHead(const std::string& remoteDeviceId, const std: } std::string -ConversationRepository::sendMessage(const std::string& msg) +ConversationRepository::commitMessage(const std::string& msg) { auto account = pimpl_->account_.lock(); if (!account) return {}; auto deviceId = std::string(account->currentDeviceId()); - auto name = account->getDisplayName(); - if (name.empty()) - name = deviceId; // First, we need to add device file to the repository if not present std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); - std::string devicePath = repoPath + DIR_SEPARATOR_STR + "devices" + DIR_SEPARATOR_STR + deviceId - + ".crt"; + std::string path = std::string("devices") + DIR_SEPARATOR_STR + deviceId + ".crt"; + std::string devicePath = repoPath + path; if (!fileutils::isFile(devicePath)) { auto file = fileutils::ofstream(devicePath, std::ios::trunc | std::ios::binary); if (!file.is_open()) { @@ -871,170 +1019,23 @@ ConversationRepository::sendMessage(const std::string& msg) file << deviceCert; file.close(); - // git add - git_index* index_ptr = nullptr; - if (git_repository_index(&index_ptr, pimpl_->repository_.get()) < 0) { - JAMI_ERR("Could not open repository index"); - return {}; - } - GitIndex index {index_ptr, git_index_free}; - - git_index_add_bypath(index.get(), devicePath.c_str()); - git_index_write(index.get()); + if (!pimpl_->add(path)) + JAMI_WARN("Couldn't add file %s", devicePath.c_str()); } - git_signature* sig_ptr = nullptr; - // Sign commit's buffer - if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) { - JAMI_ERR("Unable to create a commit signature."); - return {}; - } - GitSignature sig {sig_ptr, git_signature_free}; - - // Retrieve current HEAD - git_oid commit_id; - if (git_reference_name_to_id(&commit_id, pimpl_->repository_.get(), "HEAD") < 0) { - JAMI_ERR("Cannot get reference for HEAD"); - return {}; - } - - git_commit* head_ptr = nullptr; - if (git_commit_lookup(&head_ptr, pimpl_->repository_.get(), &commit_id) < 0) { - JAMI_ERR("Could not look up HEAD commit"); - return {}; - } - GitCommit head_commit {head_ptr, git_commit_free}; - - git_tree* tree_ptr = nullptr; - if (git_commit_tree(&tree_ptr, head_commit.get()) < 0) { - JAMI_ERR("Could not look up initial tree"); - return {}; - } - GitTree tree {tree_ptr, git_tree_free}; - - git_buf to_sign = {}; - const git_commit* head_ref[1] = {head_commit.get()}; - if (git_commit_create_buffer(&to_sign, - pimpl_->repository_.get(), - sig.get(), - sig.get(), - nullptr, - msg.c_str(), - tree.get(), - 1, - &head_ref[0]) - < 0) { - JAMI_ERR("Could not create commit buffer"); - return {}; - } - - // git commit -S - auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size); - auto signed_buf = account->identity().first->sign(to_sign_vec); - std::string signed_str = base64::encode(signed_buf); - if (git_commit_create_with_signature(&commit_id, - pimpl_->repository_.get(), - to_sign.ptr, - signed_str.c_str(), - "signature") - < 0) { - JAMI_ERR("Could not sign commit"); - return {}; - } - - // Move commit to master branch - git_reference* ref_ptr = nullptr; - if (git_reference_create(&ref_ptr, - pimpl_->repository_.get(), - "refs/heads/master", - &commit_id, - true, - nullptr) - < 0) { - JAMI_WARN("Could not move commit to master"); - } - git_reference_free(ref_ptr); - - auto commit_str = git_oid_tostr_s(&commit_id); - if (commit_str) { - JAMI_INFO("New message added with id: %s", commit_str); - } - return commit_str ? commit_str : ""; + return pimpl_->commit(msg); } std::vector<ConversationCommit> -ConversationRepository::log(const std::string& last, unsigned n) +ConversationRepository::logN(const std::string& last, unsigned n) { - std::vector<ConversationCommit> commits {}; - - git_oid oid; - if (last.empty()) { - if (git_reference_name_to_id(&oid, pimpl_->repository_.get(), "HEAD") < 0) { - JAMI_ERR("Cannot get reference for HEAD"); - return commits; - } - } else { - if (git_oid_fromstr(&oid, last.c_str()) < 0) { - JAMI_ERR("Cannot get reference for commit %s", last.c_str()); - return commits; - } - } - - git_revwalk* walker_ptr = nullptr; - if (git_revwalk_new(&walker_ptr, pimpl_->repository_.get()) < 0 - || git_revwalk_push(walker_ptr, &oid) < 0) { - JAMI_ERR("Couldn't init revwalker for conversation %s", pimpl_->id_.c_str()); - return commits; - } - GitRevWalker walker {walker_ptr, git_revwalk_free}; - git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL); - - auto x = git_oid_tostr_s(&oid); - for (auto idx = 0; !git_revwalk_next(&oid, walker.get()); ++idx) { - if (n != 0 && idx == n) { - break; - } - git_commit* commit_ptr = nullptr; - std::string id = git_oid_tostr_s(&oid); - if (git_commit_lookup(&commit_ptr, pimpl_->repository_.get(), &oid) < 0) { - JAMI_WARN("Failed to look up commit %s", id.c_str()); - break; - } - GitCommit commit {commit_ptr, git_commit_free}; - - const git_signature* sig = git_commit_author(commit.get()); - GitAuthor author; - author.name = sig->name; - author.email = sig->email; - std::string parent {}; - const git_oid* pid = git_commit_parent_id(commit.get(), 0); - if (pid) { - parent = git_oid_tostr_s(pid); - } - - auto cc = commits.emplace(commits.end(), ConversationCommit {}); - cc->id = std::move(id); - cc->commit_msg = git_commit_message(commit.get()); - cc->author = std::move(author); - cc->parent = std::move(parent); - git_buf signature = {}, signed_data = {}; - if (git_commit_extract_signature(&signature, - &signed_data, - pimpl_->repository_.get(), - &oid, - "signature") - < 0) { - JAMI_WARN("Could not extract signature for commit %s", id.c_str()); - } else { - cc->signature = base64::decode( - std::string(signature.ptr, signature.ptr + signature.size)); - cc->signed_content = std::vector<uint8_t>(signed_data.ptr, - signed_data.ptr + signed_data.size); - } - cc->timestamp = git_commit_time(commit.get()); - } + return pimpl_->log(last, "", n); +} - return commits; +std::vector<ConversationCommit> +ConversationRepository::log(const std::string& from, const std::string& to) +{ + return pimpl_->log(from, to, 0); } bool @@ -1153,4 +1154,57 @@ ConversationRepository::changedFiles(const std::string_view& diffStats) return changedFiles; } +std::string +ConversationRepository::join() +{ + if (!pimpl_) + return {}; + // Check that not already member + std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + auto account = pimpl_->account_.lock(); + if (!account) + return {}; + auto cert = account->identity().second; + auto parentCert = cert->issuer; + if (!parentCert) { + JAMI_ERR("Parent cert is null!"); + return {}; + } + auto uri = parentCert->getId().toString(); + std::string membersPath = repoPath + "members" + DIR_SEPARATOR_STR + uri + ".crt"; + std::string memberFile = membersPath + DIR_SEPARATOR_STR + uri + ".crt"; + std::string adminsPath = repoPath + "admins" + DIR_SEPARATOR_STR + uri + ".crt"; + if (fileutils::isFile(memberFile) or fileutils::isFile(adminsPath)) { + // Already member, nothing to commit + return {}; + } + // Remove invited/uri.crt + std::string invitedPath = repoPath + "invited"; + fileutils::remove(fileutils::getFullPath(invitedPath, uri + ".crt")); + // Add members/uri.crt + if (!fileutils::recursive_mkdir(membersPath, 0700)) { + JAMI_ERR("Error when creating %s. Abort create conversations", membersPath.c_str()); + return {}; + } + auto file = fileutils::ofstream(memberFile, std::ios::trunc | std::ios::binary); + if (!file.is_open()) { + JAMI_ERR("Could not write data to %s", memberFile.c_str()); + return {}; + } + file << parentCert->toString(true); + file.close(); + // git add -A + if (!git_add_all(pimpl_->repository_.get())) { + return {}; + } + Json::Value json; + json["action"] = "join"; + json["uri"] = uri; + json["type"] = "member"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + return pimpl_->commit(Json::writeString(wbuilder, json)); +} + } // namespace jami diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index afcd6a9b2746226dfa432a7e14c673d96294acd9..c8aa376c0efd9691dd4d11422c2bef9154032e01 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -19,9 +19,9 @@ #include <git2.h> #include <memory> +#include <opendht/default_types.h> #include <string> #include <vector> -#include <git2.h> #include "def.h" @@ -56,7 +56,7 @@ struct GitAuthor struct ConversationCommit { std::string id {}; - std::string parent {}; + std::vector<std::string> parents {}; GitAuthor author {}; std::vector<uint8_t> signed_content {}; std::vector<uint8_t> signature {}; @@ -99,6 +99,13 @@ public: ConversationRepository(const std::weak_ptr<JamiAccount>& account, const std::string& id); ~ConversationRepository(); + /** + * Write the certificate in /members and commit the change + * @param uri Member to add + * @return the commit id if successful + */ + std::string addMember(const std::string& uri); + /** * Fetch a remote repository via the given socket * @note This will use the socket registered for the conversation with JamiAccount::addGitSocket() @@ -123,10 +130,10 @@ public: /** * Add a new commit to the conversation - * @param msg The msg to send + * @param msg The commit message of the commit * @return <empty> on failure, else the message id */ - std::string sendMessage(const std::string& msg); + std::string commitMessage(const std::string& msg); /** * Get commits from [last-n, last] @@ -134,7 +141,8 @@ public: * @param n Max commits number to get (default: 0) * @return a list of commits */ - std::vector<ConversationCommit> log(const std::string& last = "", unsigned n = 0); + std::vector<ConversationCommit> logN(const std::string& last = "", unsigned n = 0); + std::vector<ConversationCommit> log(const std::string& from = "", const std::string& to = ""); /** * Merge another branch into the main branch @@ -159,6 +167,12 @@ public: */ static std::vector<std::string> changedFiles(const std::string_view& diffStats); + /** + * Join a repository + * @return commit Id + */ + std::string join(); + private: ConversationRepository() = delete; class Impl; diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index c6c30568469e6394911cc0fbc51934a81f516b32..65ce0082bda5542cb4e5a3b7e3570983f457857b 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -74,6 +74,7 @@ #include "array_size.h" #include "archiver.h" #include "data_transfer.h" +#include "conversation.h" #include "config/yamlparser.h" #include "security/certstore.h" @@ -192,6 +193,12 @@ struct JamiAccount::PendingCall std::shared_ptr<dht::crypto::Certificate> from_cert; }; +struct JamiAccount::PendingConversationFetch +{ + bool ready {false}; + std::string deviceId {}; +}; + struct JamiAccount::PendingMessage { std::set<dht::InfoHash> to; @@ -334,6 +341,13 @@ JamiAccount::~JamiAccount() void JamiAccount::shutdownConnections() { + { + std::lock_guard<std::mutex> lk(gitServersMtx_); + for (auto& [_id, gs] : gitServers_) { + gs->stop(); + } + gitServers_.clear(); + } connectionManager_.reset(); dhtPeerConnector_.reset(); std::lock_guard<std::mutex> lk(sipConnsMtx_); @@ -1873,6 +1887,21 @@ JamiAccount::doRegister() JAMI_DBG("[Account %s] Starting account..", getAccountID().c_str()); + JAMI_INFO("[Account %s] Start loading conversations…", getAccountID().c_str()); + auto conversationsRepositories = fileutils::readDirectory(idPath_ + DIR_SEPARATOR_STR + + "conversations"); + for (const auto& repository : conversationsRepositories) { + try { + conversations_[repository] = std::move( + std::make_unique<Conversation>(weak(), repository)); + } catch (const std::logic_error& e) { + JAMI_WARN("[Account %s] Conversations not loaded : %s", + getAccountID().c_str(), + e.what()); + } + } + JAMI_INFO("[Account %s] Conversations loaded!", getAccountID().c_str()); + // invalid state transitions: // INITIALIZING: generating/loading certificates, can't register // NEED_MIGRATION: old account detected, user needs to migrate @@ -2261,7 +2290,10 @@ JamiAccount::doRegister_() connectionManager_->onChannelRequest([this](const DeviceId&, const std::string& name) { auto isFile = name.substr(0, 7) == "file://"; auto isVCard = name.substr(0, 8) == "vcard://"; - if (name == "sip") { + if (name.find("git://") == 0) { + // TODO + return true; + } else if (name == "sip") { return true; } else if (isFile or isVCard) { auto tid_str = isFile ? name.substr(7) : name.substr(8); @@ -2311,6 +2343,40 @@ JamiAccount::doRegister_() tid, std::move(channel), std::move(cb)); + } else if (name.find("git://") == 0) { + auto conversationId = name.substr(name.find_last_of("/") + 1); + { + std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); + if (pendingConversationsFetch_.find(conversationId) + != pendingConversationsFetch_.end()) { + // Currently cloning, so we can't offer a server. + return; + } + } + if (conversations_.find(conversationId) == conversations_.end()) { + JAMI_WARN("Git server requested, but for a non existing conversation (%s)", + conversationId.c_str()); + return; + } + if (gitSocket(deviceId.toString(), conversationId) == channel) { + // The onConnectionReady is already used as client (for retrieving messages) + // So it's not the server socket + return; + } + auto accountId = this->accountID_; + auto gs = std::make_unique<GitServer>(accountId, conversationId, channel); + const dht::Value::Id serverId = ValueIdDist()(rand); + { + std::lock_guard<std::mutex> lk(gitServersMtx_); + gitServers_[serverId] = std::move(gs); + } + channel->onShutdown([w = weak(), serverId]() { + auto shared = w.lock(); + if (!shared) + return; + std::lock_guard<std::mutex> lk(shared->gitServersMtx_); + shared->gitServers_.erase(serverId); + }); } } }); @@ -2375,7 +2441,24 @@ JamiAccount::doRegister_() if (!dhtPeerConnector_) dhtPeerConnector_ = std::make_unique<DhtPeerConnector>(*this); - std::lock_guard<std::mutex> bLock(buddyInfoMtx); + dht_->listen<ConversationRequest>( + inboxDeviceKey, [this, inboxDeviceKey](ConversationRequest&& req) { + // TODO it's a trust request, we need to confirm incoming device + JAMI_INFO("Receive a new conversation request for conversation %s", + req.conversationId.c_str()); + auto convId = req.conversationId; + std::map<std::string, std::string> metadatas = req.metadatas; + { + std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); + conversationsRequests_[convId] = std::move(req); + } + emitSignal<DRing::ConversationSignal::ConversationRequestReceived>(accountID_, + convId, + metadatas); + return true; + }); + + std::lock_guard<std::mutex> lock(buddyInfoMtx); for (auto& buddy : trackedBuddies_) { buddy.second.devices_cnt = 0; trackPresence(buddy.first, buddy.second); @@ -3208,6 +3291,11 @@ JamiAccount::sendTextMessage(const std::string& to, if (devices.find(dev) != devices.end()) { return; } + // TODO do not use getAccountDetails(), accountInfo + if (dev.toString() + == getAccountDetails()[DRing::Account::ConfProperties::RING_DEVICE_ID]) { + return; + } // Else, ask for a channel and send a DHT message requestSIPConnection(to, dev); @@ -3524,6 +3612,339 @@ JamiAccount::setActiveCodecs(const std::vector<unsigned>& list) } } +std::string +JamiAccount::startConversation() +{ + // Create the conversation object + auto conversation = std::make_unique<Conversation>(weak()); + auto convId = conversation->id(); + conversations_[convId] = std::move(conversation); + + // TODO + // And send an invite to others devices to sync the conversation between device + // Via getMemebers + return convId; +} + +void +JamiAccount::acceptConversationRequest(const std::string& conversationId) +{ + // TODO DRT to optimize connections + // For all conversation members, try to open a git channel with this conversation ID + std::unique_lock<std::mutex> lk(conversationsRequestsMtx_); + auto request = conversationsRequests_.find(conversationId); + if (request == conversationsRequests_.end()) { + JAMI_WARN("Request not found for conversation %s", conversationId.c_str()); + return; + } + { + std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); + pendingConversationsFetch_[request->first] = PendingConversationFetch {}; + } + for (const auto& member : request->second.members) { + auto memberHash = dht::InfoHash(member); + if (!memberHash) { + // TODO check why some members are 000000 + continue; + } + // Avoid to connect to self for now + if (username_.find(member) != std::string::npos) + continue; + // TODO cf sync between devices + forEachDevice(memberHash, [this, request = request->second](const dht::InfoHash& dev) { + connectionManager().connectDevice( + dev, + "git://" + dev.toString() + "/" + request.conversationId, + [this, dev, request](std::shared_ptr<ChannelSocket> socket, const DeviceId&) { + if (socket) { + std::unique_lock<std::mutex> lk(pendingConversationsFetchMtx_); + auto& pending = pendingConversationsFetch_[request.conversationId]; + if (!pending.ready) { + pending.ready = true; + pending.deviceId = dev.toString(); + lk.unlock(); + // Save the git socket + addGitSocket(dev.toString(), request.conversationId, socket); + // TODO when do we remove the gitSocket? + } else { + lk.unlock(); + socket->shutdown(); + } + } + }); + }); + } + conversationsRequests_.erase(conversationId); + lk.unlock(); + checkConversationsEvents(); +} + +void +JamiAccount::checkConversationsEvents() +{ + bool hasHandler = conversationsEventHandler and not conversationsEventHandler->isCancelled(); + std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); + if (not pendingConversationsFetch_.empty() and not hasHandler) { + conversationsEventHandler = Manager::instance().scheduler().scheduleAtFixedRate( + [w = weak()] { + if (auto this_ = w.lock()) + return this_->handlePendingConversations(); + return false; + }, + std::chrono::milliseconds(10)); + } else if (pendingConversationsFetch_.empty() and hasHandler) { + conversationsEventHandler->cancel(); + conversationsEventHandler.reset(); + } +} + +bool +JamiAccount::handlePendingConversations() +{ + std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); + for (auto it = pendingConversationsFetch_.begin(); it != pendingConversationsFetch_.end();) { + if (it->second.ready) { + // Clone and store conversation + try { + auto conversationId = it->first; + auto conversation = std::make_unique<Conversation>(weak(), + it->second.deviceId, + conversationId); + if (conversation) { + conversations_.emplace(conversationId, std::move(conversation)); + // Inform user that the conversation is ready + emitSignal<DRing::ConversationSignal::ConversationReady>(accountID_, + conversationId); + } + } catch (const std::exception& e) { + JAMI_WARN("Something went wrong when cloning conversation: %s", e.what()); + } + it = pendingConversationsFetch_.erase(it); + } else { + ++it; + } + } + return !pendingConversationsFetch_.empty(); +} + +void +JamiAccount::declineConversationRequest(const std::string& conversationId) +{} + +bool +JamiAccount::removeConversation(const std::string& conversationId) +{ + return true; +} + +std::vector<std::string> +JamiAccount::getConversations() +{ + return {}; // TODO +} + +std::vector<std::map<std::string, std::string>> +getConversationRequests() +{ + return {}; // TODO +} + +// Member management +bool +JamiAccount::addConversationMember(const std::string& conversationId, const std::string& contactUri) +{ + // Add a new member in the conversation + if (conversations_[conversationId]->addMember(contactUri).empty()) { + JAMI_WARN("Couldn't add %s to %s", contactUri.c_str(), conversationId.c_str()); + return false; + } + // Invite the new member to the conversation + auto toH = dht::InfoHash(contactUri); + ConversationRequest req; + req.conversationId = conversationId; + auto convMembers = conversations_[conversationId]->getMembers(); + for (const auto& member : convMembers) + req.members.emplace_back(member.at("uri")); + req.metadatas = {/* TODO */}; + // TODO message engine + forEachDevice(toH, [this, toH, req](const dht::InfoHash& dev) { + JAMI_INFO("Sending conversation invite %s / %s", + toH.toString().c_str(), + dev.toString().c_str()); + dht_->putEncrypted(dht::InfoHash::get("inbox:" + dev.toString()), dev, req); + }); + return true; +} + +bool +JamiAccount::removeConversationMember(const std::string& conversationId, + const std::string& contactUri) +{ + conversations_[conversationId]->removeMember(contactUri); + return true; +} + +std::vector<std::map<std::string, std::string>> +JamiAccount::getConversationMembers(const std::string& conversationId) +{ + auto conversation = conversations_.find(conversationId); + if (conversation != conversations_.end() && conversation->second) + return conversation->second->getMembers(); + return {}; +} + +// Message send/load +void +JamiAccount::sendMessage(const std::string& conversationId, + const std::string& message, + const std::string& parent, + const std::string& type) +{ + auto conversation = conversations_.find(conversationId); + if (conversation != conversations_.end() && conversation->second) { + auto commitId = conversation->second->sendMessage(message, type, parent); + // TODO make async/non blocking + auto messages = conversation->second->loadMessages(commitId, 1); + if (!messages.empty()) { + emitSignal<DRing::ConversationSignal::MessageReceived>(getAccountID(), + conversationId, + messages.front()); + } + if (!commitId.empty()) { + Json::Value message; + message["id"] = conversationId; + message["commit"] = commitId; + message["deviceId"] = getAccountDetails()[DRing::Account::ConfProperties::RING_DEVICE_ID]; + Json::StreamWriterBuilder builder; + const auto text = Json::writeString(builder, message); + for (const auto& members : conversation->second->getMembers()) { + auto uri = members.at("uri"); + if (username_.find(uri) != std::string::npos) + continue; + // Announce to all members that a new message is sent + sendTextMessage(uri, {{"application/im-gitmessage-id", text}}); + } + } else { + JAMI_ERR("Failed to send message to conversation %s", conversationId.c_str()); + } + } +} + +void +JamiAccount::loadConversationMessages(const std::string& conversationId, + const std::string& fromMessage, + size_t n) +{ + // loadMessages will perform a git log that can take quite some time, so to avoid any lock, run + // it the threadpool + dht::ThreadPool::io().run([this, conversationId, fromMessage, n] { + auto conversation = conversations_.find(conversationId); + if (conversation != conversations_.end() && conversation->second) { + auto messages = conversation->second->loadMessages(fromMessage, n); + emitSignal<DRing::ConversationSignal::ConversationLoaded>(accountID_, + conversationId, + messages); + } + }); +} + +void +JamiAccount::onNewGitCommit(const std::string& peer, + const std::string& deviceId, + const std::string& conversationId, + const std::string& commitId) +{ + JAMI_DBG("on new commit notification from %s, for %s, commit %s", + peer.c_str(), + conversationId.c_str(), + commitId.c_str()); + auto conversation = conversations_.find(conversationId); + if (conversation != conversations_.end() && conversation->second) { + if (!conversation->second->isMember(peer)) { + JAMI_WARN("%s is not a member of %s", peer.c_str(), conversationId.c_str()); + return; + } + + // Retrieve current last message + std::string lastMessageId = ""; + auto lastMessage = conversation->second->loadMessages("", 1); + if (lastMessage.empty()) + JAMI_ERR("No message detected. This is a bug"); + else + lastMessageId = lastMessage.front().at("id"); + + auto announceMessages = [w = weak(), conversationId, lastMessageId]() { + auto shared = w.lock(); + if (!shared) + return; + auto conversation = shared->conversations_.find(conversationId); + if (conversation != shared->conversations_.end() && conversation->second) { + // Do a diff between last message, and current new message when merged + auto messages = conversation->second->loadMessages("", lastMessageId); + for (const auto& message : messages) { + JAMI_DBG("New message received for conversation %s with id %s", + conversationId.c_str(), + message.at("id").c_str()); + emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), + conversationId, + message); + } + } else { + JAMI_WARN("Unknown conversation %s", conversationId.c_str()); + } + }; + + if (gitSocket(deviceId, conversationId)) { + // If the git socket exists, we can fetch from it + if (!conversation->second->fetchFrom(deviceId)) { + JAMI_WARN("Could not fetch new commit from %s for %s", + deviceId.c_str(), + conversationId.c_str()); + removeGitSocket(deviceId, conversationId); + } + auto merged = conversation->second->mergeHistory(deviceId); + if (merged) + announceMessages(); + } else { + // Else we need to add a new gitSocket + { + std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); + pendingConversationsFetch_[conversationId] = PendingConversationFetch {}; + } + connectionManager().connectDevice( + DeviceId(deviceId), + "git://" + deviceId + "/" + conversationId, + [this, + deviceId, + conversation, + conversationId, + announceMessages = std::move( + announceMessages)](std::shared_ptr<ChannelSocket> socket, const DeviceId&) { + if (socket) { + addGitSocket(deviceId, conversationId, socket); + if (!conversation->second->fetchFrom(deviceId)) + JAMI_WARN("Could not fetch new commit from %s for %s", + deviceId.c_str(), + conversationId.c_str()); + auto merged = conversation->second->mergeHistory(deviceId); + if (merged) + announceMessages(); + } else { + JAMI_ERR("Couldn't open a new git channel with %s for conversation %s", + deviceId.c_str(), + conversationId.c_str()); + } + { + std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); + pendingConversationsFetch_.erase(conversationId); + } + }); + } + } else { + JAMI_WARN("Could not find conversation %s", conversationId.c_str()); + } +} + void JamiAccount::cacheTurnServers() { diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 476ab63621703d60e7f9c4ef68e2c946c87ccfbe..005cb7516b08f5cd6cc0e794b663190fd04aa8e2 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -4,6 +4,7 @@ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com> * Author: Simon Désaulniers <simon.desaulniers@gmail.com> * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,6 +39,7 @@ #include "security/certstore.h" #include "scheduled_executor.h" #include "connectionmanager.h" +#include "gitserver.h" #include <opendht/dhtrunner.h> #include <opendht/default_types.h> @@ -80,6 +82,23 @@ class AccountManager; struct AccountInfo; class SipTransport; class ChanneledOutgoingTransfer; +class Conversation; + +/** + * A ConversationRequest is a request which corresponds to a trust request, but for conversations + * It's signed by the sender and contains the members list, the conversationId, and the metadatas + * such as the conversation's vcard, etc. (TODO determine) + * Transmitted via the UDP DHT + */ +struct ConversationRequest : public dht::EncryptedValue<ConversationRequest> +{ + static const constexpr dht::ValueType& TYPE = dht::ValueType::USER_DATA; + dht::Value::Id id = dht::Value::INVALID_ID; + std::string conversationId; + std::vector<std::string> members; + std::map<std::string, std::string> metadatas; + MSGPACK_DEFINE_MAP(id, conversationId, members, metadatas) +}; using SipConnectionKey = std::pair<std::string /* accountId */, DeviceId>; using GitSocketList = std::map<std::string, /* device Id */ @@ -131,7 +150,7 @@ public: */ JamiAccount(const std::string& accountID, bool presenceEnabled); - ~JamiAccount(); + ~JamiAccount() noexcept; /** * Serialize internal state of this account for configuration @@ -468,6 +487,34 @@ public: } std::string_view currentDeviceId() const; + // Conversation management + std::string startConversation(); + void acceptConversationRequest(const std::string& conversationId); + void declineConversationRequest(const std::string& conversationId); + bool removeConversation(const std::string& conversationId); + std::vector<std::string> getConversations(); + std::vector<std::map<std::string, std::string>> getConversationRequests(); + + // Member management + bool addConversationMember(const std::string& conversationId, const std::string& contactUri); + bool removeConversationMember(const std::string& conversationId, const std::string& contactUri); + std::vector<std::map<std::string, std::string>> getConversationMembers( + const std::string& conversationId); + + // Message send/load + void sendMessage(const std::string& conversationId, + const std::string& message, + const std::string& parent = "", + const std::string& type = "text/plain"); + void loadConversationMessages(const std::string& conversationId, + const std::string& fromMessage = "", + size_t n = 0); + + // Received a new commit notification + void onNewGitCommit(const std::string& peer, + const std::string& deviceId, + const std::string& conversationId, + const std::string& commitId) override; private: NON_COPYABLE(JamiAccount); @@ -479,6 +526,7 @@ private: * Private structures */ struct PendingCall; + struct PendingConversationFetch; struct PendingMessage; struct BuddyInfo; struct DiscoveredPeer; @@ -654,6 +702,9 @@ private: mutable std::mutex buddyInfoMtx; std::map<dht::InfoHash, BuddyInfo> trackedBuddies_; + /** Conversations */ + std::map<std::string, std::unique_ptr<Conversation>> conversations_; + mutable std::mutex dhtValuesMtx_; bool dhtPublicInCalls_ {true}; @@ -807,6 +858,19 @@ private: * @param deviceId Device that will receive the profile */ void sendProfile(const std::string& deviceId); + + // Conversations + std::mutex conversationsRequestsMtx_ {}; + std::map<std::string, ConversationRequest> conversationsRequests_ {}; + std::mutex pendingConversationsFetchMtx_ {}; + std::map<std::string, PendingConversationFetch> pendingConversationsFetch_; + + std::mutex gitServersMtx_ {}; + std::map<dht::Value::Id, std::unique_ptr<GitServer>> gitServers_ {}; + + std::shared_ptr<RepeatedTask> conversationsEventHandler {}; + void checkConversationsEvents(); + bool handlePendingConversations(); }; static inline std::ostream& diff --git a/src/manager.cpp b/src/manager.cpp index b970afcbbf547ac0e715a93c4efde94cf33d10f3..eb6a3f8d2ccefda4317089d2aa01e4c670c51e2e 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -750,7 +750,8 @@ Manager::init(const std::string& config_file) git_libgit2_init(); auto res = git_transport_register("git", p2p_transport_cb, nullptr); if (res < 0) { - JAMI_ERR("Unable to initialize git transport"); + const git_error* error = giterr_last(); + JAMI_ERR("Unable to initialize git transport %s", error ? error->message : "(unknown)"); } #ifndef WIN32 diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp index 08f08e27c1452f64b2ac09d9e8f581506e7f63a7..35a8b85d0c7f32b3b1b676f6f0a13db6f0ff4151 100644 --- a/src/sip/sipaccountbase.cpp +++ b/src/sip/sipaccountbase.cpp @@ -46,6 +46,7 @@ #include <type_traits> #include <regex> +#include <json/json.h> #include <ctime> #include "manager.h" @@ -56,6 +57,7 @@ namespace jami { static constexpr const char MIME_TYPE_IMDN[] {"message/imdn+xml"}; +static constexpr const char MIME_TYPE_GIT[] {"application/im-gitmessage-id"}; static constexpr const char MIME_TYPE_IM_COMPOSING[] {"application/im-iscomposing+xml"}; static constexpr std::chrono::steady_clock::duration COMPOSING_TIMEOUT {std::chrono::seconds(12)}; @@ -561,6 +563,23 @@ SIPAccountBase::onTextMessage(const std::string& id, } catch (const std::exception& e) { JAMI_WARN("Error parsing display notification: %s", e.what()); } + } else if (m.first == MIME_TYPE_GIT) { + Json::Value json; + std::string err; + Json::CharReaderBuilder rbuilder; + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + if (!reader->parse(m.second.data(), m.second.data() + m.second.size(), &json, &err)) { + JAMI_ERR("Can't parse server response: %s", err.c_str()); + return; + } + + JAMI_WARN("Received indication for new commit available in conversation %s", + json["id"].asString().c_str()); + + onNewGitCommit(from, + json["deviceId"].asString(), + json["id"].asString(), + json["commit"].asString()); } } diff --git a/src/sip/sipaccountbase.h b/src/sip/sipaccountbase.h index 6fb154c6e937de2abf58a60bb16c10810f12fa8a..e263ed0702daee21882fba5acedaec061aad4328 100644 --- a/src/sip/sipaccountbase.h +++ b/src/sip/sipaccountbase.h @@ -129,7 +129,7 @@ public: */ SIPAccountBase(const std::string& accountID); - virtual ~SIPAccountBase(); + virtual ~SIPAccountBase() noexcept; /** * Create incoming SIPCall. diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am index 02e38e33c3e22860d6500e306a71d76bfebf8753..25b8756d2a989a3334e871ce6df1d2fb3a9aff8c 100644 --- a/test/unitTest/Makefile.am +++ b/test/unitTest/Makefile.am @@ -140,4 +140,10 @@ ut_fileTransfer_SOURCES = fileTransfer/fileTransfer.cpp check_PROGRAMS += ut_conversationRepository ut_conversationRepository_SOURCES = conversationRepository/conversationRepository.cpp +# +# conversation +# +check_PROGRAMS += ut_conversation +ut_conversation_SOURCES = conversation/conversation.cpp + TESTS = $(check_PROGRAMS) diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b0c7e1758cdd3da17085565001d53cf323e70e2c --- /dev/null +++ b/test/unitTest/conversation/conversation.cpp @@ -0,0 +1,488 @@ +/* + * Copyright (C) 2017-2019 Savoir-faire Linux Inc. + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 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 <https://www.gnu.org/licenses/>. + */ + +#include <cppunit/TestAssert.h> +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> + +#include <condition_variable> +#include <string> +#include <fstream> +#include <streambuf> +#include <filesystem> + +#include "manager.h" +#include "jamidht/conversation.h" +#include "jamidht/jamiaccount.h" +#include "../../test_runner.h" +#include "dring.h" +#include "base64.h" +#include "fileutils.h" +#include "account_const.h" + +#include <git2.h> + +using namespace std::string_literals; +using namespace DRing::Account; + +namespace jami { +namespace test { + +class ConversationTest : public CppUnit::TestFixture +{ +public: + ~ConversationTest() { DRing::fini(); } + static std::string name() { return "Conversation"; } + void setUp(); + void tearDown(); + + std::string aliceId; + std::string bobId; + +private: + void testCreateConversation(); + void testGetConversation(); + void testAddMember(); + void testGetMembers(); + void testSendMessage(); + void testSendMessageTriggerMessageReceived(); + void testMergeTwoDifferentHeads(); + + CPPUNIT_TEST_SUITE(ConversationTest); + CPPUNIT_TEST(testCreateConversation); + CPPUNIT_TEST(testGetConversation); + CPPUNIT_TEST(testAddMember); + CPPUNIT_TEST(testGetMembers); + CPPUNIT_TEST(testSendMessage); + CPPUNIT_TEST(testSendMessageTriggerMessageReceived); + CPPUNIT_TEST_SUITE_END(); +}; + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(ConversationTest, ConversationTest::name()); + +void +ConversationTest::setUp() +{ + // Init daemon + DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG)); + if (not Manager::instance().initialized) + CPPUNIT_ASSERT(DRing::start("dring-sample.yml")); + + std::map<std::string, std::string> details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "ALICE"; + details[ConfProperties::ALIAS] = "ALICE"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = ""; + aliceId = Manager::instance().addAccount(details); + + details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "BOB"; + details[ConfProperties::ALIAS] = "BOB"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = ""; + bobId = Manager::instance().addAccount(details); + + JAMI_INFO("Initialize account..."); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + bool ready = false; + auto details = aliceAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + ready = (daemonStatus == "REGISTERED"); + details = bobAccount->getVolatileAccountDetails(); + daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + ready &= (daemonStatus == "REGISTERED"); + })); + DRing::registerSignalHandlers(confHandlers); + cv.wait_for(lk, std::chrono::seconds(30)); + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::tearDown() +{ + auto currentAccSize = Manager::instance().getAccountList().size(); + Manager::instance().removeAccount(aliceId, true); + Manager::instance().removeAccount(bobId, true); + // Because cppunit is not linked with dbus, just poll if removed + for (int i = 0; i < 40; ++i) { + if (Manager::instance().getAccountList().size() <= currentAccSize - 2) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} + +void +ConversationTest::testCreateConversation() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceDeviceId = aliceAccount + ->getAccountDetails()[DRing::Account::ConfProperties::RING_DEVICE_ID]; + auto uri = aliceAccount->getAccountDetails()[DRing::Account::ConfProperties::USERNAME]; + if (uri.find("ring:") == 0) + uri = uri.substr(std::string("ring:").size()); + auto convId = aliceAccount->startConversation(); + + // Assert that repository exists + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); + // Check created files + auto adminCrt = repoPath + DIR_SEPARATOR_STR + "admins" + DIR_SEPARATOR_STR + uri + ".crt"; + CPPUNIT_ASSERT(fileutils::isFile(adminCrt)); + auto crt = std::ifstream(adminCrt); + std::string adminCrtStr((std::istreambuf_iterator<char>(crt)), std::istreambuf_iterator<char>()); + auto cert = aliceAccount->identity().second; + auto deviceCert = cert->toString(false); + auto parentCert = cert->issuer->toString(true); + CPPUNIT_ASSERT(adminCrtStr == parentCert); + auto deviceCrt = repoPath + DIR_SEPARATOR_STR + "devices" + DIR_SEPARATOR_STR + aliceDeviceId + + ".crt"; + CPPUNIT_ASSERT(fileutils::isFile(deviceCrt)); + crt = std::ifstream(deviceCrt); + std::string deviceCrtStr((std::istreambuf_iterator<char>(crt)), + std::istreambuf_iterator<char>()); + CPPUNIT_ASSERT(deviceCrtStr == deviceCert); +} + +void +ConversationTest::testGetConversation() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceDeviceId = aliceAccount + ->getAccountDetails()[DRing::Account::ConfProperties::RING_DEVICE_ID]; + auto uri = aliceAccount->getAccountDetails()[DRing::Account::ConfProperties::USERNAME]; + if (uri.find("ring:") == 0) + uri = uri.substr(std::string("ring:").size()); + auto convId = aliceAccount->startConversation(); + + auto conversations = aliceAccount->getConversations(); + CPPUNIT_ASSERT(conversations.size() == 1); + CPPUNIT_ASSERT(conversations.front() == convId); +} + +void +ConversationTest::testAddMember() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getAccountDetails()[ConfProperties::USERNAME]; + if (bobUri.find("ring:") == 0) + bobUri = bobUri.substr(std::string("ring:").size()); + auto convId = aliceAccount->startConversation(); + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool conversationReady = false, requestReceived = false, memberMessageGenerated = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "member") { + memberMessageGenerated = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(memberMessageGenerated); + // Assert that repository exists + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); + // Check created files + auto bobInvited = repoPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri + + ".crt"; + CPPUNIT_ASSERT(fileutils::isFile(bobInvited)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; })); + auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath)); + bobInvited = clonedPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri + ".crt"; + CPPUNIT_ASSERT(!fileutils::isFile(bobInvited)); +} + +void +ConversationTest::testGetMembers() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getAccountDetails()[ConfProperties::USERNAME]; + auto aliceUri = aliceAccount->getAccountDetails()[ConfProperties::USERNAME]; + if (bobUri.find("ring:") == 0) + bobUri = bobUri.substr(std::string("ring:").size()); + if (aliceUri.find("ring:") == 0) + aliceUri = aliceUri.substr(std::string("ring:").size()); + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto messageReceived = false; + bool requestReceived = false; + bool conversationReady = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/) { + if (accountId == aliceId) { + messageReceived = true; + cv.notify_one(); + } + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + // Start a conversation and add member + auto convId = aliceAccount->startConversation(); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + + // Assert that repository exists + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); + + auto members = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 1); + CPPUNIT_ASSERT(members[0]["uri"] + == aliceAccount->getAccountDetails()[ConfProperties::USERNAME].substr( + std::string("ring:").size())); + CPPUNIT_ASSERT(members[0]["role"] == "admin"); + + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }); + messageReceived = false; + bobAccount->acceptConversationRequest(convId); + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }); + members = bobAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 2); + cv.wait_for(lk, std::chrono::seconds(60), [&]() { return messageReceived; }); + members = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 2); + auto hasBob = members[0]["uri"] == bobUri || members[1]["uri"] == bobUri; + auto hasAlice = members[0]["uri"] == aliceUri || members[1]["uri"] == aliceUri; + CPPUNIT_ASSERT(hasAlice && hasBob); +} + +void +ConversationTest::testSendMessage() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getAccountDetails()[ConfProperties::USERNAME]; + if (bobUri.find("ring:") == 0) + bobUri = bobUri.substr(std::string("ring:").size()); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto messageBobReceived = 0, messageAliceReceived = 0; + bool requestReceived = false; + bool conversationReady = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/) { + if (accountId == bobId) { + messageBobReceived += 1; + } else { + messageAliceReceived += 1; + } + cv.notify_one(); + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& /*accountId*/, const std::string& /* conversationId */) { + conversationReady = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + auto convId = aliceAccount->startConversation(); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(requestReceived); + + bobAccount->acceptConversationRequest(convId); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(conversationReady); + + // Assert that repository exists + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); + // Wait that alice sees Bob + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; }); + + aliceAccount->sendMessage(convId, "hi"); + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; }); + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testSendMessageTriggerMessageReceived() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto messageReceived = 0; + bool conversationReady = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/) { + messageReceived += 1; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& /* accountId */, const std::string& /* conversationId */) { + conversationReady = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + auto convId = aliceAccount->startConversation(); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(conversationReady); + + aliceAccount->sendMessage(convId, "hi"); + cv.wait_for(lk, std::chrono::seconds(30), [&] { return messageReceived == 1; }); + CPPUNIT_ASSERT(messageReceived == 1); + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testMergeTwoDifferentHeads() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + aliceAccount->trackBuddyPresence(carlaUri, true); + auto convId = aliceAccount->startConversation(); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool conversationReady = false, carlaGotMessage = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == carlaId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == carlaId && conversationId == convId) { + carlaGotMessage = true; + } + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri, false)); + + // Cp conversations & convInfo + auto repoPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + + aliceAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations"; + auto repoPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations"; + std::filesystem::copy(repoPathAlice, repoPathCarla, std::filesystem::copy_options::recursive); + auto ciPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "convInfo"; + auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + + DIR_SEPARATOR_STR + "convInfo"; + std::filesystem::copy(ciPathAlice, ciPathCarla); + + // Accept for alice and makes different heads + ConversationRepository repo(carlaAccount, convId); + repo.join(); + + aliceAccount->sendMessage(convId, "hi"); + aliceAccount->sendMessage(convId, "sup"); + aliceAccount->sendMessage(convId, "jami"); + + // Start Carla, should merge and all messages should be there + Manager::instance().sendRegister(carlaId, true); + carlaGotMessage = false; + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return carlaGotMessage; })); + DRing::unregisterSignalHandlers(); +} + +} // namespace test +} // namespace jami + +RING_TEST_RUNNER(jami::test::ConversationTest::name()) diff --git a/test/unitTest/conversationRepository/conversationRepository.cpp b/test/unitTest/conversationRepository/conversationRepository.cpp index 4da580e76d3e7f8fef5845c1633a329218ed4628..f10834d0823271f7bbaa01fbe4ed2503c21c0170 100644 --- a/test/unitTest/conversationRepository/conversationRepository.cpp +++ b/test/unitTest/conversationRepository/conversationRepository.cpp @@ -118,23 +118,27 @@ ConversationRepositoryTest::setUp() details[ConfProperties::ARCHIVE_PATH] = ""; bobId = Manager::instance().addAccount(details); + JAMI_INFO("Initialize account..."); auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); - - bool ready = false; - bool idx = 0; - while (!ready && idx < 100) { - auto details = aliceAccount->getVolatileAccountDetails(); - auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; - ready = (daemonStatus == "REGISTERED"); - details = bobAccount->getVolatileAccountDetails(); - daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; - ready &= (daemonStatus == "REGISTERED"); - if (!ready) { - idx += 1; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - } + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + bool ready = false; + auto details = aliceAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + ready = (daemonStatus == "REGISTERED"); + details = bobAccount->getVolatileAccountDetails(); + daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + ready &= (daemonStatus == "REGISTERED"); + })); + DRing::registerSignalHandlers(confHandlers); + cv.wait_for(lk, std::chrono::seconds(30)); + DRing::unregisterSignalHandlers(); } void @@ -253,7 +257,7 @@ ConversationRepositoryTest::testCloneViaChannelSocket() aliceAccount->connectionManager().connectDevice(DeviceId(bobDeviceId), "git://*", - [&](std::shared_ptr<ChannelSocket> socket) { + [&](std::shared_ptr<ChannelSocket> socket, const DeviceId&) { if (socket) { successfullyConnected = true; sendSocket = socket; @@ -269,13 +273,11 @@ ConversationRepositoryTest::testCloneViaChannelSocket() bobAccount->addGitSocket(aliceDeviceId, repository->id(), channelSocket); GitServer gs(aliceId, repository->id(), sendSocket); - std::thread sendT = std::thread([&]() { gs.run(); }); auto cloned = ConversationRepository::cloneConversation(bobAccount->weak(), aliceDeviceId, repository->id()); gs.stop(); - sendT.join(); CPPUNIT_ASSERT(cloned != nullptr); CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath)); @@ -337,19 +339,19 @@ ConversationRepositoryTest::testAddSomeMessages() auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); auto repository = ConversationRepository::createConversation(aliceAccount->weak()); - auto id1 = repository->sendMessage("Commit 1"); - auto id2 = repository->sendMessage("Commit 2"); - auto id3 = repository->sendMessage("Commit 3"); + auto id1 = repository->commitMessage("Commit 1"); + auto id2 = repository->commitMessage("Commit 2"); + auto id3 = repository->commitMessage("Commit 3"); auto messages = repository->log(); CPPUNIT_ASSERT(messages.size() == 4 /* 3 + initial */); CPPUNIT_ASSERT(messages[0].id == id3); - CPPUNIT_ASSERT(messages[0].parent == id2); + CPPUNIT_ASSERT(messages[0].parents.front() == id2); CPPUNIT_ASSERT(messages[0].commit_msg == "Commit 3"); CPPUNIT_ASSERT(messages[0].author.name == messages[3].author.name); CPPUNIT_ASSERT(messages[0].author.email == messages[3].author.email); CPPUNIT_ASSERT(messages[1].id == id2); - CPPUNIT_ASSERT(messages[1].parent == id1); + CPPUNIT_ASSERT(messages[1].parents.front() == id1); CPPUNIT_ASSERT(messages[1].commit_msg == "Commit 2"); CPPUNIT_ASSERT(messages[1].author.name == messages[3].author.name); CPPUNIT_ASSERT(messages[1].author.email == messages[3].author.email); @@ -357,7 +359,7 @@ ConversationRepositoryTest::testAddSomeMessages() CPPUNIT_ASSERT(messages[2].commit_msg == "Commit 1"); CPPUNIT_ASSERT(messages[2].author.name == messages[3].author.name); CPPUNIT_ASSERT(messages[2].author.email == messages[3].author.email); - CPPUNIT_ASSERT(messages[2].parent == repository->id()); + CPPUNIT_ASSERT(messages[2].parents.front() == repository->id()); // Check sig CPPUNIT_ASSERT( aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content, @@ -376,18 +378,18 @@ ConversationRepositoryTest::testLogMessages() auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); auto repository = ConversationRepository::createConversation(aliceAccount->weak()); - auto id1 = repository->sendMessage("Commit 1"); - auto id2 = repository->sendMessage("Commit 2"); - auto id3 = repository->sendMessage("Commit 3"); + auto id1 = repository->commitMessage("Commit 1"); + auto id2 = repository->commitMessage("Commit 2"); + auto id3 = repository->commitMessage("Commit 3"); - auto messages = repository->log(repository->id(), 1); + auto messages = repository->logN(repository->id(), 1); CPPUNIT_ASSERT(messages.size() == 1); CPPUNIT_ASSERT(messages[0].id == repository->id()); - messages = repository->log(id2, 2); + messages = repository->logN(id2, 2); CPPUNIT_ASSERT(messages.size() == 2); CPPUNIT_ASSERT(messages[0].id == id2); CPPUNIT_ASSERT(messages[1].id == id1); - messages = repository->log(repository->id(), 3); + messages = repository->logN(repository->id(), 3); CPPUNIT_ASSERT(messages.size() == 1); CPPUNIT_ASSERT(messages[0].id == repository->id()); } @@ -436,7 +438,7 @@ ConversationRepositoryTest::testFetch() aliceAccount->connectionManager().connectDevice(DeviceId(bobDeviceId), "git://*", - [&](std::shared_ptr<ChannelSocket> socket) { + [&](std::shared_ptr<ChannelSocket> socket, const DeviceId&) { if (socket) { successfullyConnected = true; sendSocket = socket; @@ -453,25 +455,23 @@ ConversationRepositoryTest::testFetch() bobAccount->addGitSocket(aliceDeviceId, repository->id(), channelSocket); GitServer gs(aliceId, repository->id(), sendSocket); - std::thread sendT = std::thread([&]() { gs.run(); }); // Clone repository - auto id1 = repository->sendMessage("Commit 1"); + auto id1 = repository->commitMessage("Commit 1"); auto cloned = ConversationRepository::cloneConversation(bobAccount->weak(), aliceDeviceId, repository->id()); gs.stop(); - sendT.join(); bobAccount->removeGitSocket(aliceDeviceId, repository->id()); // Add some new messages to fetch - auto id2 = repository->sendMessage("Commit 2"); - auto id3 = repository->sendMessage("Commit 3"); + auto id2 = repository->commitMessage("Commit 2"); + auto id3 = repository->commitMessage("Commit 3"); // Open a new channel to simulate the fact that we are later aliceAccount->connectionManager().connectDevice(DeviceId(bobDeviceId), "git://*", - [&](std::shared_ptr<ChannelSocket> socket) { + [&](std::shared_ptr<ChannelSocket> socket, const DeviceId&) { if (socket) { successfullyConnected = true; sendSocket = socket; @@ -484,24 +484,22 @@ ConversationRepositoryTest::testFetch() ccv.wait_for(lk, std::chrono::seconds(10)); bobAccount->addGitSocket(aliceDeviceId, repository->id(), channelSocket); GitServer gs2(aliceId, repository->id(), sendSocket); - std::thread sendT2 = std::thread([&]() { gs2.run(); }); CPPUNIT_ASSERT(cloned->fetch(aliceDeviceId)); CPPUNIT_ASSERT(id3 == cloned->remoteHead(aliceDeviceId)); gs2.stop(); bobAccount->removeGitSocket(aliceDeviceId, repository->id()); - sendT2.join(); auto messages = cloned->log(id3); CPPUNIT_ASSERT(messages.size() == 4 /* 3 + initial */); CPPUNIT_ASSERT(messages[0].id == id3); - CPPUNIT_ASSERT(messages[0].parent == id2); + CPPUNIT_ASSERT(messages[0].parents.front() == id2); CPPUNIT_ASSERT(messages[0].commit_msg == "Commit 3"); CPPUNIT_ASSERT(messages[0].author.name == messages[3].author.name); CPPUNIT_ASSERT(messages[0].author.email == messages[3].author.email); CPPUNIT_ASSERT(messages[1].id == id2); - CPPUNIT_ASSERT(messages[1].parent == id1); + CPPUNIT_ASSERT(messages[1].parents.front() == id1); CPPUNIT_ASSERT(messages[1].commit_msg == "Commit 2"); CPPUNIT_ASSERT(messages[1].author.name == messages[3].author.name); CPPUNIT_ASSERT(messages[1].author.email == messages[3].author.email); @@ -509,7 +507,7 @@ ConversationRepositoryTest::testFetch() CPPUNIT_ASSERT(messages[2].commit_msg == "Commit 1"); CPPUNIT_ASSERT(messages[2].author.name == messages[3].author.name); CPPUNIT_ASSERT(messages[2].author.email == messages[3].author.email); - CPPUNIT_ASSERT(messages[2].parent == repository->id()); + CPPUNIT_ASSERT(messages[2].parents.front() == repository->id()); // Check sig CPPUNIT_ASSERT( aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content, @@ -685,9 +683,9 @@ ConversationRepositoryTest::testDiff() auto uri = aliceAccount->getUsername(); auto repository = ConversationRepository::createConversation(aliceAccount->weak()); - auto id1 = repository->sendMessage("Commit 1"); - auto id2 = repository->sendMessage("Commit 2"); - auto id3 = repository->sendMessage("Commit 3"); + auto id1 = repository->commitMessage("Commit 1"); + auto id2 = repository->commitMessage("Commit 2"); + auto id3 = repository->commitMessage("Commit 3"); auto diff = repository->diffStats(id2, id1); CPPUNIT_ASSERT(ConversationRepository::changedFiles(diff).empty());