From cfa1732c73aa4cf1bcc5c0244ebbbb36e2955e10 Mon Sep 17 00:00:00 2001
From: Leopold Chappuis <leopold.chappuis@savoirfairelinux.com>
Date: Mon, 4 Nov 2024 13:26:51 -0500
Subject: [PATCH] updateProfile: New API for Setting displayName and Avatar

This new API integrates vCard logic within the daemon, allowing clients to only provide a path. It sends the updated data directly to other connected peers to synchronize information (cached connections). The API can also be used to remove a displayName or avatar, meaning you must always supply either a displayName or an avatar to use it.

Change-Id: I6d9bdb29ce86ce3721911cf5cf7eb836ef976110
---
 .../cx.ring.Ring.ConfigurationManager.xml     |  9 ++
 bin/dbus/dbusconfigurationmanager.hpp         |  6 ++
 bin/jni/configurationmanager.i                |  1 +
 bin/nodejs/configurationmanager.i             |  1 +
 src/account.h                                 |  2 +
 src/client/configurationmanager.cpp           |  6 ++
 src/jami/configurationmanager_interface.h     |  1 +
 src/jamidht/conversationrepository.cpp        |  4 +-
 src/jamidht/jamiaccount.cpp                   | 81 ++++++++++++++++
 src/jamidht/jamiaccount.h                     |  4 +
 src/manager.cpp                               |  8 ++
 src/manager.h                                 |  2 +
 src/vcard.cpp                                 | 39 ++++++++
 src/vcard.h                                   | 97 ++++++++++---------
 14 files changed, 213 insertions(+), 48 deletions(-)

diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
index 796661a13..20f7f46fc 100644
--- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
+++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
@@ -575,6 +575,15 @@
             </tp:docstring>
         </method>
 
+        <method name="updateProfile" tp:name-for-bindings="updateProfile">
+            <arg type="s" name="accountId" direction="in"/>
+            <arg type="s" name="displayName" direction="in"/>
+            <arg type="s" name="avatarPath" direction="in"/>
+            <tp:docstring>
+               Update the profile of the account and send it to peers.
+            </tp:docstring>
+        </method>
+
         <method name="getMessageStatus" tp:name-for-bindings="getMessageStatus">
             <arg type="t" name="id" direction="in"/>
             <arg type="i" name="status" direction="out">
diff --git a/bin/dbus/dbusconfigurationmanager.hpp b/bin/dbus/dbusconfigurationmanager.hpp
index 7363f1740..797cb5446 100644
--- a/bin/dbus/dbusconfigurationmanager.hpp
+++ b/bin/dbus/dbusconfigurationmanager.hpp
@@ -216,6 +216,12 @@ public:
         return libjami::getNearbyPeers(accountID);
     }
 
+    void
+    updateProfile(const std::string& accountID,const std::string& displayName, const std::string& avatarPath)
+    {
+        libjami::updateProfile(accountID,displayName, avatarPath);
+    }
+
     auto
     getMessageStatus(const uint64_t& id)
         -> decltype(libjami::getMessageStatus(id))
diff --git a/bin/jni/configurationmanager.i b/bin/jni/configurationmanager.i
index ca208c892..dcbfd8000 100644
--- a/bin/jni/configurationmanager.i
+++ b/bin/jni/configurationmanager.i
@@ -90,6 +90,7 @@ std::vector<std::map<std::string, std::string>> getConnectionList(const std::str
 std::vector<std::map<std::string, std::string>> getChannelList(const std::string& accountId, const std::string& connectionId);
 std::string addAccount(const std::map<std::string, std::string>& details);
 void removeAccount(const std::string& accountId);
+void updateProfile(const std::string& accountId,const std::string& displayName, const std::string& avatarPath);
 std::vector<std::string> getAccountList();
 void sendRegister(const std::string& accountId, bool enable);
 void registerAllAccounts(void);
diff --git a/bin/nodejs/configurationmanager.i b/bin/nodejs/configurationmanager.i
index e6b5e16ed..dc9c704e0 100644
--- a/bin/nodejs/configurationmanager.i
+++ b/bin/nodejs/configurationmanager.i
@@ -86,6 +86,7 @@ std::vector<std::map<std::string, std::string>> getConnectionList(const std::str
 std::vector<std::map<std::string, std::string>> getChannelList(const std::string& accountId, const std::string& connectionId);
 std::string addAccount(const std::map<std::string, std::string>& details);
 void removeAccount(const std::string& accountId);
+void updateProfile(const std::string& accountId,const std::string& displayName, const std::string& avatarPath);
 std::vector<std::string> getAccountList();
 void sendRegister(const std::string& accountId, bool enable);
 void registerAllAccounts(void);
diff --git a/src/account.h b/src/account.h
index d2dd9bcaf..02861e97b 100644
--- a/src/account.h
+++ b/src/account.h
@@ -208,6 +208,8 @@ public:
 
     virtual std::map<std::string, std::string> getNearbyPeers() const { return {}; }
 
+    virtual void updateProfile(const std::string& /*displayName*/,  const std::string& /*avatarPath*/) {}
+
     /**
      * Return the status corresponding to the token.
      */
diff --git a/src/client/configurationmanager.cpp b/src/client/configurationmanager.cpp
index 1997a8467..a7076a5bd 100644
--- a/src/client/configurationmanager.cpp
+++ b/src/client/configurationmanager.cpp
@@ -293,6 +293,12 @@ getNearbyPeers(const std::string& accountId)
     return jami::Manager::instance().getNearbyPeers(accountId);
 }
 
+void
+updateProfile(const std::string& accountId,const std::string& displayName, const std::string& avatarPath)
+{
+    jami::Manager::instance().updateProfile(accountId, displayName, avatarPath);
+}
+
 int
 getMessageStatus(uint64_t messageId)
 {
diff --git a/src/jami/configurationmanager_interface.h b/src/jami/configurationmanager_interface.h
index c53cdf3a2..d49dd0c10 100644
--- a/src/jami/configurationmanager_interface.h
+++ b/src/jami/configurationmanager_interface.h
@@ -104,6 +104,7 @@ LIBJAMI_PUBLIC bool cancelMessage(const std::string& accountId, uint64_t message
 LIBJAMI_PUBLIC std::vector<Message> getLastMessages(const std::string& accountId,
                                                     const uint64_t& base_timestamp);
 LIBJAMI_PUBLIC std::map<std::string, std::string> getNearbyPeers(const std::string& accountId);
+LIBJAMI_PUBLIC void updateProfile(const std::string& accountId,const std::string& displayName, const std::string& avatarPath);
 LIBJAMI_PUBLIC int getMessageStatus(uint64_t id);
 LIBJAMI_PUBLIC int getMessageStatus(const std::string& accountId, uint64_t id);
 LIBJAMI_PUBLIC void setIsComposing(const std::string& accountId,
diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp
index 93a58a9e5..c6f4a2a09 100644
--- a/src/jamidht/conversationrepository.cpp
+++ b/src/jamidht/conversationrepository.cpp
@@ -3903,7 +3903,7 @@ ConversationRepository::updateInfos(const std::map<std::string, std::string>& pr
     }
 
     auto addKey = [&](auto property, auto key) {
-        auto it = infosMap.find(key);
+        auto it = infosMap.find(std::string(key));
         if (it != infosMap.end()) {
             file << property;
             file << ":";
@@ -3922,7 +3922,7 @@ ConversationRepository::updateInfos(const std::map<std::string, std::string>& pr
     file << vCard::Property::PHOTO;
     file << vCard::Delimiter::SEPARATOR_TOKEN;
     file << vCard::Property::BASE64;
-    auto avatarIt = infosMap.find(vCard::Value::AVATAR);
+    auto avatarIt = infosMap.find(std::string(vCard::Value::AVATAR));
     if (avatarIt != infosMap.end()) {
         // TODO type=png? store another way?
         file << ":";
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index 10213070a..f0c1b259c 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -3373,6 +3373,87 @@ JamiAccount::getNearbyPeers() const
     return discoveredPeerMap_;
 }
 
+void
+JamiAccount::sendProfileToPeers()
+{
+    if (!connectionManager_)
+        return;
+    const auto& accountUri = accountManager_->getInfo()->accountId;
+    for (const auto& connection : connectionManager_->getConnectionList()) {
+        const auto& device = connection.at("device");
+        const auto& peer = connection.at("peer");
+        if(peer == accountUri){
+            sendProfile("", accountUri, device);
+            continue;
+        }
+        const auto& conversationId = convModule()->getOneToOneConversation(peer);
+        if (!conversationId.empty()) {
+            sendProfile(conversationId, peer, device);
+        }
+    }
+}
+
+std::map<std::string, std::string>
+JamiAccount::getProfileVcard() const {
+    const auto& path = idPath_ / "profile.vcf";
+
+    if (!std::filesystem::exists(path)) {
+        return {};
+    }
+
+    return vCard::utils::toMap(fileutils::loadTextFile(path));
+}
+
+void
+JamiAccount::updateProfile(const std::string& displayName, const std::filesystem::path& avatarPath){
+
+    const auto& accountUri = accountManager_->getInfo()->accountId;
+    const auto& path = profilePath();
+    const auto& vCardPath = idPath_ / "profiles" / fmt::format("{}.vcf", base64::encode(accountUri));
+    const std::filesystem::path& tmpPath = vCardPath.string() + ".tmp";
+
+    auto profile = getProfileVcard();
+    if(profile.empty()){
+        profile = vCard::utils::initVcard();
+    }
+
+    profile["FN"] = displayName;
+    editConfig([&](JamiAccountConfig& config) { config.displayName = displayName; });
+    emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(),
+                                                                    getAccountDetails());
+    if(std::filesystem::exists(avatarPath)){
+        try {
+            const auto& base64 = jami::base64::encode(fileutils::loadFile(avatarPath));
+            profile["PHOTO;ENCODING=BASE64;TYPE=PNG"] = base64;
+        } catch (const std::exception& e) {
+            JAMI_ERROR("Failed to load avatar: {}", e.what());
+        }
+    }else if(avatarPath.empty()){
+        profile["PHOTO;ENCODING=BASE64;TYPE=PNG"] = "";
+    }
+    // nothing happens to the profile photo if the avatarPath is invalid
+    // and not empty. So far it seems to be the best default behavior.
+
+    const std::string& vCard = vCard::utils::toString(profile);
+
+    try {
+        std::ofstream file(tmpPath);
+        if (file.is_open()) {
+            file << vCard;
+            file.close();
+            std::filesystem::rename(tmpPath, vCardPath);
+            fileutils::createFileLink(path,vCardPath);
+            sendProfileToPeers();
+            emitSignal<libjami::ConfigurationSignal::ProfileReceived>(getAccountID(), accountUri, path.string());
+        } else {
+            JAMI_ERROR("Unable to open file for writing: {}", tmpPath.string());
+        }
+    } catch (const std::exception& e) {
+        JAMI_ERROR("Error writing profile: {}", e.what());
+    }
+}
+
+
 void
 JamiAccount::setActiveCodecs(const std::vector<unsigned>& list)
 {
diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h
index a038893b6..d5d0fa9bc 100644
--- a/src/jamidht/jamiaccount.h
+++ b/src/jamidht/jamiaccount.h
@@ -420,6 +420,10 @@ public:
      */
     std::map<std::string, std::string> getNearbyPeers() const override;
 
+    std::map<std::string, std::string> getProfileVcard() const;
+    void sendProfileToPeers();
+    void updateProfile(const std::string& displayName, const std::filesystem::path& avatarPath);
+
 #ifdef LIBJAMI_TESTABLE
     dhtnet::ConnectionManager& connectionManager() { return *connectionManager_; }
 
diff --git a/src/manager.cpp b/src/manager.cpp
index a04b13369..d4878b97d 100644
--- a/src/manager.cpp
+++ b/src/manager.cpp
@@ -3264,6 +3264,14 @@ Manager::getNearbyPeers(const std::string& accountID)
     return {};
 }
 
+void
+Manager::updateProfile(const std::string& accountID,const std::string& displayName, const std::string& avatarPath)
+{
+    if (const auto acc = getAccount<JamiAccount>(accountID))
+        acc->updateProfile(displayName,avatarPath);
+}
+
+
 void
 Manager::setDefaultModerator(const std::string& accountID, const std::string& peerURI, bool state)
 {
diff --git a/src/manager.h b/src/manager.h
index cc4fab98a..13dc5a6c7 100644
--- a/src/manager.h
+++ b/src/manager.h
@@ -811,6 +811,8 @@ public:
 
     std::map<std::string, std::string> getNearbyPeers(const std::string& accountID);
 
+    void updateProfile(const std::string& accountID,const std::string& displayName,const std::string& avatarPath);
+
 #ifdef ENABLE_VIDEO
     /**
      * Create a new SinkClient instance, store it in an internal cache as a weak_ptr
diff --git a/src/vcard.cpp b/src/vcard.cpp
index 4a86e4578..a99319684 100644
--- a/src/vcard.cpp
+++ b/src/vcard.cpp
@@ -37,6 +37,45 @@ toMap(std::string_view content)
     }
     return vCard;
 }
+
+std::map<std::string, std::string>
+initVcard()
+{
+    return {
+        {std::string(Property::VCARD_VERSION), "2.1"},
+        {std::string(Property::FORMATTED_NAME), ""},
+        {std::string(Property::PHOTO_PNG), ""},
+    };
+}
+
+
+std::string
+toString(const std::map<std::string, std::string>& vCard)
+{
+    size_t estimatedSize = 0;
+    for (const auto& [key, value] : vCard) {
+        if (Delimiter::BEGIN_TOKEN_KEY == key || Delimiter::END_TOKEN_KEY == key)
+            continue;
+        estimatedSize += key.size() + value.size() + 2;
+    }
+    std::string result;
+    result.reserve(estimatedSize + Delimiter::BEGIN_TOKEN.size() + Delimiter::END_LINE_TOKEN.size() + Delimiter::END_TOKEN.size() + Delimiter::END_LINE_TOKEN.size());
+
+    result += Delimiter::BEGIN_TOKEN;
+    result += Delimiter::END_LINE_TOKEN;
+
+    for (const auto& [key, value] : vCard) {
+        if (Delimiter::BEGIN_TOKEN_KEY == key || Delimiter::END_TOKEN_KEY == key)
+            continue;
+        result += key + ':' + value + '\n';
+    }
+
+    result += Delimiter::END_TOKEN;
+    result += Delimiter::END_LINE_TOKEN;
+
+    return result;
+}
+
 } // namespace utils
 
 } // namespace vCard
diff --git a/src/vcard.h b/src/vcard.h
index eb13c7473..322e77926 100644
--- a/src/vcard.h
+++ b/src/vcard.h
@@ -24,59 +24,61 @@ namespace vCard {
 
 struct Delimiter
 {
-    constexpr static const char* SEPARATOR_TOKEN = ";";
-    constexpr static const char* END_LINE_TOKEN = "\n";
-    constexpr static const char* BEGIN_TOKEN = "BEGIN:VCARD";
-    constexpr static const char* END_TOKEN = "END:VCARD";
-};
+    constexpr static std::string_view SEPARATOR_TOKEN = ";";
+    constexpr static std::string_view END_LINE_TOKEN = "\n";
+    constexpr static std::string_view BEGIN_TOKEN = "BEGIN:VCARD";
+    constexpr static std::string_view END_TOKEN = "END:VCARD";
+    constexpr static std::string_view BEGIN_TOKEN_KEY = "BEGIN";
+    constexpr static std::string_view END_TOKEN_KEY = "END";
+};;
 
 struct Property
 {
-    constexpr static const char* UID = "UID";
-    constexpr static const char* VCARD_VERSION = "VERSION";
-    constexpr static const char* ADDRESS = "ADR";
-    constexpr static const char* AGENT = "AGENT";
-    constexpr static const char* BIRTHDAY = "BDAY";
-    constexpr static const char* CATEGORIES = "CATEGORIES";
-    constexpr static const char* CLASS = "CLASS";
-    constexpr static const char* DELIVERY_LABEL = "LABEL";
-    constexpr static const char* EMAIL = "EMAIL";
-    constexpr static const char* FORMATTED_NAME = "FN";
-    constexpr static const char* GEOGRAPHIC_POSITION = "GEO";
-    constexpr static const char* KEY = "KEY";
-    constexpr static const char* LOGO = "LOGO";
-    constexpr static const char* MAILER = "MAILER";
-    constexpr static const char* NAME = "N";
-    constexpr static const char* NICKNAME = "NICKNAME";
-    constexpr static const char* DESCRIPTION = "DESCRIPTION";
-    constexpr static const char* NOTE = "NOTE";
-    constexpr static const char* ORGANIZATION = "ORG";
-    constexpr static const char* PHOTO = "PHOTO";
-    constexpr static const char* PRODUCT_IDENTIFIER = "PRODID";
-    constexpr static const char* REVISION = "REV";
-    constexpr static const char* ROLE = "ROLE";
-    constexpr static const char* SORT_STRING = "SORT-STRING";
-    constexpr static const char* SOUND = "SOUND";
-    constexpr static const char* TELEPHONE = "TEL";
-    constexpr static const char* TIME_ZONE = "TZ";
-    constexpr static const char* TITLE = "TITLE";
-    constexpr static const char* RDV_ACCOUNT = "RDV_ACCOUNT";
-    constexpr static const char* RDV_DEVICE = "RDV_DEVICE";
-    constexpr static const char* URL = "URL";
-    constexpr static const char* BASE64 = "ENCODING=BASE64";
-    constexpr static const char* TYPE_PNG = "TYPE=PNG";
-    constexpr static const char* TYPE_JPEG = "TYPE=JPEG";
-    constexpr static const char* PHOTO_PNG = "PHOTO;ENCODING=BASE64;TYPE=PNG";
-    constexpr static const char* PHOTO_JPEG = "PHOTO;ENCODING=BASE64;TYPE=JPEG";
+    constexpr static std::string_view UID = "UID";
+    constexpr static std::string_view VCARD_VERSION = "VERSION";
+    constexpr static std::string_view ADDRESS = "ADR";
+    constexpr static std::string_view AGENT = "AGENT";
+    constexpr static std::string_view BIRTHDAY = "BDAY";
+    constexpr static std::string_view CATEGORIES = "CATEGORIES";
+    constexpr static std::string_view CLASS = "CLASS";
+    constexpr static std::string_view DELIVERY_LABEL = "LABEL";
+    constexpr static std::string_view EMAIL = "EMAIL";
+    constexpr static std::string_view FORMATTED_NAME = "FN";
+    constexpr static std::string_view GEOGRAPHIC_POSITION = "GEO";
+    constexpr static std::string_view KEY = "KEY";
+    constexpr static std::string_view LOGO = "LOGO";
+    constexpr static std::string_view MAILER = "MAILER";
+    constexpr static std::string_view NAME = "N";
+    constexpr static std::string_view NICKNAME = "NICKNAME";
+    constexpr static std::string_view DESCRIPTION = "DESCRIPTION";
+    constexpr static std::string_view NOTE = "NOTE";
+    constexpr static std::string_view ORGANIZATION = "ORG";
+    constexpr static std::string_view PHOTO = "PHOTO";
+    constexpr static std::string_view PRODUCT_IDENTIFIER = "PRODID";
+    constexpr static std::string_view REVISION = "REV";
+    constexpr static std::string_view ROLE = "ROLE";
+    constexpr static std::string_view SORT_STRING = "SORT-STRING";
+    constexpr static std::string_view SOUND = "SOUND";
+    constexpr static std::string_view TELEPHONE = "TEL";
+    constexpr static std::string_view TIME_ZONE = "TZ";
+    constexpr static std::string_view TITLE = "TITLE";
+    constexpr static std::string_view RDV_ACCOUNT = "RDV_ACCOUNT";
+    constexpr static std::string_view RDV_DEVICE = "RDV_DEVICE";
+    constexpr static std::string_view URL = "URL";
+    constexpr static std::string_view BASE64 = "ENCODING=BASE64";
+    constexpr static std::string_view TYPE_PNG = "TYPE=PNG";
+    constexpr static std::string_view TYPE_JPEG = "TYPE=JPEG";
+    constexpr static std::string_view PHOTO_PNG = "PHOTO;ENCODING=BASE64;TYPE=PNG";
+    constexpr static std::string_view PHOTO_JPEG = "PHOTO;ENCODING=BASE64;TYPE=JPEG";
 };
 
 struct Value
 {
-    constexpr static const char* TITLE = "title";
-    constexpr static const char* DESCRIPTION = "description";
-    constexpr static const char* AVATAR = "avatar";
-    constexpr static const char* RDV_ACCOUNT = "rdvAccount";
-    constexpr static const char* RDV_DEVICE = "rdvDevice";
+    constexpr static std::string_view TITLE = "title";
+    constexpr static std::string_view DESCRIPTION = "description";
+    constexpr static std::string_view AVATAR = "avatar";
+    constexpr static std::string_view RDV_ACCOUNT = "rdvAccount";
+    constexpr static std::string_view RDV_DEVICE = "rdvDevice";
 };
 
 namespace utils {
@@ -86,6 +88,9 @@ namespace utils {
  * @return the vCard representation
  */
 std::map<std::string, std::string> toMap(std::string_view content);
+std::map<std::string, std::string> initVcard();
+std::string toString(const std::map<std::string, std::string>& vCard);
+
 } // namespace utils
 
 } // namespace vCard
-- 
GitLab