diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 796661a13f2956709b51b939f8c4af774cb2307a..20f7f46fc5324a75d867cbd3601db4ce65ac1ba5 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 7363f1740f43d2f25cae5eabb5eaaa0694775898..797cb5446ccf9b8d29d037a6fa2c5b13127c7882 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 ca208c8923e93f4d291dc168e8228cefd38b1fb6..dcbfd80000651e67de5c47e743b411b95247323c 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 e6b5e16edc76bbedf014a48f92d9b985c007827e..dc9c704e0e51adbd702dcef1b03fbdc94949e68e 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 d2dd9bcaf4b8065027a6e9cc2eb5159145b64af3..02861e97b1f30b1aaed4a25f790d41f04fa57a42 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 1997a8467aa15489c9e64cba53cf25da7f033c44..a7076a5bd42811ef438fa6ff56745a7d4081173f 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 c53cdf3a231f9a874a8e3f44674070bd8b3562d9..d49dd0c10121c0bbdac0ac38cc8e89db717b1626 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 93a58a9e590f55da97d80935c7afecb940a2ae5b..c6f4a2a091cc6e56ff1b2b456088e86e35eb448d 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 10213070ae6e752fa2ca3cc3ff40979d59c4450f..f0c1b259c8c159e560f33fe888d991eb21e3fdf8 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 a038893b6e524a58660554468a23fff3aca8bb3d..d5d0fa9bc6c803b90b1117736360caf180721b2e 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 a04b1336958819dd2509a9722e44983fde7aaaf6..d4878b97d553985563decd71bfdf791676ad34eb 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 cc4fab98a0fa29faac9526d33a227487e7b68e33..13dc5a6c701465cbff507d1763d4e286722e8d25 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 4a86e45789186c1224c2f43f5bd31d04afb45b87..a9931968422e433d30b871ab43e271d72910205a 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 eb13c7473834e39a82b778a72b9e55d64d1e01eb..322e779269888d0717a722f6cae4ed51fffb9cb3 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