LCOV - code coverage report
Current view: top level - foo/src/jamidht - conversation.h (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 23 24 95.8 %
Date: 2026-02-28 10:41:24 Functions: 17 18 94.4 %

          Line data    Source code
       1             : /*
       2             :  * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
       3             :  *
       4             :  * This program is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 3 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * This program is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with this program. If not, see <https://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #pragma once
      19             : 
      20             : #include "jamidht/conversationrepository.h"
      21             : #include "conversationrepository.h"
      22             : #include "swarm/swarm_protocol.h"
      23             : #include "jami/conversation_interface.h"
      24             : #include "jamidht/typers.h"
      25             : #include "string_utils.h"
      26             : 
      27             : #include <json/json.h>
      28             : #include <msgpack.hpp>
      29             : 
      30             : #include <functional>
      31             : #include <string>
      32             : #include <vector>
      33             : #include <map>
      34             : #include <memory>
      35             : #include <set>
      36             : 
      37             : #include <asio.hpp>
      38             : 
      39             : namespace dhtnet {
      40             : class ChannelSocket;
      41             : } // namespace dhtnet
      42             : 
      43             : namespace jami {
      44             : 
      45             : namespace ConversationMapKeys {
      46             : static constexpr const char* ID {"id"};
      47             : static constexpr const char* CREATED {"created"};
      48             : static constexpr const char* REMOVED {"removed"};
      49             : static constexpr const char* ERASED {"erased"};
      50             : static constexpr const char* MEMBERS {"members"};
      51             : static constexpr const char* LAST_DISPLAYED {"lastDisplayed"};
      52             : static constexpr const char* RECEIVED {"received"};
      53             : static constexpr const char* DECLINED {"declined"};
      54             : static constexpr const char* FROM {"from"};
      55             : static constexpr const char* CONVERSATIONID {"conversationId"};
      56             : static constexpr const char* METADATAS {"metadatas"};
      57             : } // namespace ConversationMapKeys
      58             : 
      59             : namespace ConversationDirectories {
      60             : static constexpr std::string_view PREFERENCES {"preferences"};
      61             : static constexpr std::string_view STATUS {"status"};
      62             : static constexpr std::string_view SENDING {"sending"};
      63             : static constexpr std::string_view FETCHED {"fetched"};
      64             : static constexpr std::string_view ACTIVE_CALLS {"activeCalls"};
      65             : static constexpr std::string_view HOSTED_CALLS {"hostedCalls"};
      66             : static constexpr std::string_view CACHED {"cached"};
      67             : } // namespace ConversationDirectories
      68             : 
      69             : namespace ConversationPreferences {
      70             : static constexpr const char* HOST_CONFERENCES = "hostConferences";
      71             : }
      72             : 
      73             : class JamiAccount;
      74             : class ConversationRepository;
      75             : class TransferManager;
      76             : enum class ConversationMode;
      77             : 
      78             : /**
      79             :  * A ConversationRequest is a request which corresponds to a trust request, but for conversations
      80             :  * It's signed by the sender and contains the members list, the conversationId, and the metadatas
      81             :  * such as the conversation's vcard, etc. (TODO determine)
      82             :  * Transmitted via the UDP DHT
      83             :  */
      84             : struct ConversationRequest
      85             : {
      86             :     std::string conversationId;
      87             :     std::string from;
      88             :     std::map<std::string, std::string> metadatas;
      89             : 
      90             :     time_t received {0};
      91             :     time_t declined {0};
      92             : 
      93         323 :     ConversationRequest() = default;
      94             :     ConversationRequest(const Json::Value& json);
      95             : 
      96             :     Json::Value toJson() const;
      97             :     std::map<std::string, std::string> toMap() const;
      98             : 
      99             :     bool operator==(const ConversationRequest& o) const
     100             :     {
     101             :         auto m = toMap();
     102             :         auto om = o.toMap();
     103             :         return m.size() == om.size() && std::equal(m.begin(), m.end(), om.begin());
     104             :     }
     105             : 
     106         462 :     bool isOneToOne() const
     107             :     {
     108             :         try {
     109         598 :             return metadatas.at("mode") == "0";
     110          68 :         } catch (...) {
     111          68 :         }
     112          68 :         return true;
     113             :     }
     114             : 
     115         174 :     ConversationMode mode() const
     116             :     {
     117             :         try {
     118         276 :             return to_enum<ConversationMode>(metadatas.at("mode"));
     119          51 :         } catch (...) {
     120          51 :         }
     121          51 :         return ConversationMode::ONE_TO_ONE;
     122             :     }
     123             : 
     124         295 :     MSGPACK_DEFINE_MAP(from, conversationId, metadatas, received, declined)
     125             : };
     126             : 
     127             : struct ConvInfo
     128             : {
     129             :     std::string id {};
     130             :     time_t created {0};
     131             :     time_t removed {0};
     132             :     time_t erased {0};
     133             :     std::set<std::string> members;
     134             :     std::string lastDisplayed {};
     135             :     ConversationMode mode {0};
     136             : 
     137         663 :     ConvInfo() = default;
     138          27 :     ConvInfo(const ConvInfo&) = default;
     139           0 :     ConvInfo(ConvInfo&&) = default;
     140         552 :     ConvInfo(const std::string& id)
     141         552 :         : id(id) {};
     142             :     explicit ConvInfo(const Json::Value& json);
     143             : 
     144       13321 :     bool isRemoved() const { return removed >= created; }
     145             : 
     146        2802 :     ConvInfo& operator=(const ConvInfo&) = default;
     147         187 :     ConvInfo& operator=(ConvInfo&&) = default;
     148             : 
     149             :     Json::Value toJson() const;
     150             : 
     151        3257 :     MSGPACK_DEFINE_MAP(id, created, removed, erased, members, lastDisplayed, mode)
     152             : };
     153             : 
     154             : using OnPullCb = std::function<void(bool fetchOk)>;
     155             : using OnLoadMessages = std::function<void(std::vector<libjami::SwarmMessage>&& messages)>;
     156             : using OnCommitCb = std::function<void(const std::string&)>;
     157             : using OnDoneCb = std::function<void(bool, const std::string&)>;
     158             : using OnMultiDoneCb = std::function<void(const std::vector<std::string>&)>;
     159             : using OnMembersChanged = std::function<void(const std::set<std::string>&)>;
     160             : using DeviceId = dht::PkId;
     161             : using GitSocketList = std::map<DeviceId, std::shared_ptr<dhtnet::ChannelSocket>>;
     162             : using ChannelCb = std::function<bool(const std::shared_ptr<dhtnet::ChannelSocket>&)>;
     163             : using NeedSocketCb = std::function<void(const std::string&, const std::string&, ChannelCb&&, const std::string&)>;
     164             : 
     165             : class Conversation : public std::enable_shared_from_this<Conversation>
     166             : {
     167             : public:
     168             :     Conversation(const std::shared_ptr<JamiAccount>& account,
     169             :                  ConversationMode mode,
     170             :                  const std::string& otherMember = "");
     171             :     Conversation(const std::shared_ptr<JamiAccount>& account, const std::string& conversationId = "");
     172             :     Conversation(const std::shared_ptr<JamiAccount>& account,
     173             :                  const std::string& remoteDevice,
     174             :                  const std::string& conversationId);
     175             :     ~Conversation();
     176             : 
     177             :     /**
     178             :      * Print the state of the DRT linked to the conversation
     179             :      */
     180             :     void monitor();
     181             : 
     182             : #ifdef LIBJAMI_TEST
     183             :     enum class BootstrapStatus { FAILED, FALLBACK, SUCCESS };
     184             :     /**
     185             :      * Used by the tests to get whenever the DRT is connected/disconnected
     186             :      */
     187             :     void onBootstrapStatus(const std::function<void(std::string, BootstrapStatus)>& cb);
     188             : 
     189             :     std::vector<libjami::SwarmMessage> loadMessagesSync(const LogOptions& options);
     190             :     void announce(const std::vector<std::map<std::string, std::string>>& commits, bool commitFromSelf = false);
     191             :     void announce(const std::string& commitId, bool commitFromSelf = false);
     192             : #endif
     193             : 
     194             :     /**
     195             :      * Bootstrap swarm manager to other peers
     196             :      * @param onBootstrapped     Callback called when connection is established successfully
     197             :      * @param knownDevices       List of account's known devices
     198             :      */
     199             :     void bootstrap(std::function<void()> onBootstrapped, const std::vector<DeviceId>& knownDevices);
     200             : 
     201             :     /**
     202             :      * Refresh active calls.
     203             :      * @note: If the host crash during a call, when initializing, we need to update
     204             :      * and commit all the crashed calls
     205             :      * @return  Commits added
     206             :      */
     207             :     std::vector<std::string> commitsEndedCalls();
     208             : 
     209             :     void onMembersChanged(OnMembersChanged&& cb);
     210             : 
     211             :     /**
     212             :      * Set the callback that will be called whenever a new socket will be needed
     213             :      * @param cb
     214             :      */
     215             :     void onNeedSocket(NeedSocketCb cb);
     216             :     /**
     217             :      * Add swarm connection to the DRT
     218             :      * @param channel       Related channel
     219             :      */
     220             :     void addSwarmChannel(std::shared_ptr<dhtnet::ChannelSocket> channel);
     221             : 
     222             :     /**
     223             :      * Get conversation's id
     224             :      * @return conversation Id
     225             :      */
     226             :     std::string id() const;
     227             : 
     228             :     // Member management
     229             :     /**
     230             :      * Add conversation member
     231             :      * @param uri   Member to add
     232             :      * @param cb    On done cb
     233             :      */
     234             :     void addMember(const std::string& contactUri, const OnDoneCb& cb = {});
     235             :     void removeMember(const std::string& contactUri, bool isDevice, const OnDoneCb& cb = {});
     236             :     /**
     237             :      * @param includeInvited        If we want invited members
     238             :      * @param includeLeft           If we want left members
     239             :      * @param includeBanned         If we want banned members
     240             :      * @return a vector of member details:
     241             :      * {
     242             :      *  "uri":"xxx",
     243             :      *  "role":"member/admin/invited",
     244             :      *  "lastDisplayed":"id"
     245             :      *  ...
     246             :      * }
     247             :      */
     248             :     std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited = false,
     249             :                                                                bool includeLeft = false,
     250             :                                                                bool includeBanned = false) const;
     251             : 
     252             :     /**
     253             :      * @param filter           If we want to remove one member
     254             :      * @param filteredRoles    If we want to ignore some roles
     255             :      * @return members' uris
     256             :      */
     257             :     std::set<std::string> memberUris(std::string_view filter = {},
     258             :                                      const std::set<MemberRole>& filteredRoles = {MemberRole::INVITED,
     259             :                                                                                   MemberRole::LEFT,
     260             :                                                                                   MemberRole::BANNED}) const;
     261             : 
     262             :     /**
     263             :      * Get peers to sync with. This is mostly managed by the DRT
     264             :      * @return some mobile nodes and all connected nodes
     265             :      */
     266             :     std::vector<NodeId> peersToSyncWith() const;
     267             :     /**
     268             :      * Check if we're at least connected to one node
     269             :      * @return if the DRT is connected
     270             :      */
     271             :     bool isBootstrapped() const;
     272             :     /**
     273             :      * Retrieve the uri from a deviceId
     274             :      * @note used by swarm manager (peersToSyncWith)
     275             :      * @param deviceId
     276             :      * @return corresponding issuer
     277             :      */
     278             :     std::string uriFromDevice(const std::string& deviceId) const;
     279             : 
     280             :     /**
     281             :      * Join a conversation
     282             :      * @return commit id to send
     283             :      */
     284             :     std::string join();
     285             : 
     286             :     /**
     287             :      * Test if an URI is a member
     288             :      * @param uri       URI to test
     289             :      * @return true if uri is a member
     290             :      */
     291             :     bool isMember(const std::string& uri, bool includeInvited = false) const;
     292             :     bool isBanned(const std::string& uri) const;
     293             : 
     294             :     // Message send
     295             :     void sendMessage(std::string&& message,
     296             :                      const std::string& type = "text/plain",
     297             :                      const std::string& replyTo = "",
     298             :                      OnCommitCb&& onCommit = {},
     299             :                      OnDoneCb&& cb = {});
     300             :     void sendMessage(Json::Value&& message,
     301             :                      const std::string& replyTo = "",
     302             :                      OnCommitCb&& onCommit = {},
     303             :                      OnDoneCb&& cb = {});
     304             :     // Note: used for replay. Should not be used by clients
     305             :     void sendMessages(std::vector<Json::Value>&& messages, OnMultiDoneCb&& cb = {});
     306             :     /**
     307             :      * Get a range of messages
     308             :      * @param cb        The callback when loaded
     309             :      * @param options   The log options
     310             :      */
     311             :     void loadMessages(const OnLoadMessages& cb, const LogOptions& options);
     312             :     /**
     313             :      * Clear all cached messages
     314             :      */
     315             :     void clearCache();
     316             :     /**
     317             :      * Check if a commit exists in the repository
     318             :      * @param commitId The commit id to check
     319             :      * @return true if the commit was found, false if not or if an error occurred
     320             :      */
     321             :     bool hasCommit(const std::string& commitId) const;
     322             :     /**
     323             :      * Retrieve one commit
     324             :      * @param   commitId
     325             :      * @return  The commit if found
     326             :      */
     327             :     std::optional<std::map<std::string, std::string>> getCommit(const std::string& commitId) const;
     328             :     /**
     329             :      * Get last commit id
     330             :      * @return last commit id
     331             :      */
     332             :     std::string lastCommitId() const;
     333             : 
     334             :     /**
     335             :      * Fetch and merge from peer
     336             :      * @param deviceId  Peer device
     337             :      * @param cb        On pulled callback
     338             :      * @param commitId  Commit id that triggered this fetch
     339             :      * @return true if callback will be called later
     340             :      */
     341             :     bool pull(const std::string& deviceId, OnPullCb&& cb, std::string commitId = "");
     342             :     /**
     343             :      * Fetch new commits and re-ask for waiting files
     344             :      * @param member
     345             :      * @param deviceId
     346             :      * @param cb        cf pull()
     347             :      * @param commitId  cf pull()
     348             :      */
     349             :     void sync(const std::string& member, const std::string& deviceId, OnPullCb&& cb, std::string commitId = "");
     350             : 
     351             :     /**
     352             :      * Generate an invitation to send to new contacts
     353             :      * @return the invite to send
     354             :      */
     355             :     std::map<std::string, std::string> generateInvitation() const;
     356             : 
     357             :     /**
     358             :      * Leave a conversation
     359             :      * @return commit id to send
     360             :      */
     361             :     std::string leave();
     362             : 
     363             :     /**
     364             :      * Set a conversation as removing (when loading convInfo and still not sync)
     365             :      * @todo: not a big fan to see this here. can be set in the constructor
     366             :      * cause it's used by jamiaccount when loading conversations
     367             :      */
     368             :     void setRemovingFlag();
     369             : 
     370             :     /**
     371             :      * Check if we are removing the conversation
     372             :      * @return true if left the room
     373             :      */
     374             :     bool isRemoving();
     375             : 
     376             :     /**
     377             :      * Erase all related datas
     378             :      */
     379             :     void erase();
     380             : 
     381             :     /**
     382             :      * Get conversation's mode
     383             :      * @return the mode
     384             :      */
     385             :     ConversationMode mode() const;
     386             : 
     387             :     /**
     388             :      * One to one util, get initial members
     389             :      * @return initial members
     390             :      */
     391             :     std::vector<std::string> getInitialMembers() const;
     392             :     bool isInitialMember(const std::string& uri) const;
     393             : 
     394             :     /**
     395             :      * Change repository's infos
     396             :      * @param map       New infos (supported keys: title, description, avatar)
     397             :      * @param cb        On commited
     398             :      */
     399             :     void updateInfos(const std::map<std::string, std::string>& map, const OnDoneCb& cb = {});
     400             : 
     401             :     /**
     402             :      * Change user's preferences
     403             :      * @param map       New preferences
     404             :      */
     405             :     void updatePreferences(const std::map<std::string, std::string>& map);
     406             : 
     407             :     /**
     408             :      * Retrieve current infos (title, description, avatar, mode)
     409             :      * @return infos
     410             :      */
     411             :     std::map<std::string, std::string> infos() const;
     412             :     /**
     413             :      * Retrieve current preferences (color, notification, etc)
     414             :      * @param includeLastModified       If we want to know when the preferences were modified
     415             :      * @return preferences
     416             :      */
     417             :     std::map<std::string, std::string> preferences(bool includeLastModified) const;
     418             :     std::vector<uint8_t> vCard() const;
     419             : 
     420             :     /////// File transfer
     421             : 
     422             :     /**
     423             :      * Access to transfer manager
     424             :      */
     425             :     std::shared_ptr<TransferManager> dataTransfer() const;
     426             : 
     427             :     /**
     428             :      * Choose if we can accept channel request
     429             :      * @param member        member to check
     430             :      * @param fileId        file transfer to check (needs to be waiting)
     431             :      * @param verifyShaSum  for debug only
     432             :      * @return if we accept the channel request
     433             :      */
     434             :     bool onFileChannelRequest(const std::string& member,
     435             :                               const std::string& fileId,
     436             :                               std::filesystem::path& path,
     437             :                               std::string& sha3sum) const;
     438             :     /**
     439             :      * Adds a file to the waiting list and ask members
     440             :      * @param interactionId     Related interaction id
     441             :      * @param fileId            Related id
     442             :      * @param path              Destination
     443             :      * @param member            Member if we know from who to pull file
     444             :      * @param deviceId          Device if we know from who to pull file
     445             :      * @return id of the file
     446             :      */
     447             :     bool downloadFile(const std::string& interactionId,
     448             :                       const std::string& fileId,
     449             :                       const std::string& path,
     450             :                       const std::string& member = "",
     451             :                       const std::string& deviceId = "");
     452             : 
     453             :     /**
     454             :      * Reset fetched information
     455             :      */
     456             :     void clearFetched();
     457             :     /**
     458             :      * Store information about who fetch or not. This simplify sync (sync when a device without the
     459             :      * last fetch is detected)
     460             :      * @param deviceId
     461             :      * @param commitId
     462             :      */
     463             :     void hasFetched(const std::string& deviceId, const std::string& commitId);
     464             : 
     465             :     /**
     466             :      * Store last read commit (returned in getMembers)
     467             :      * @param uri               Of the member
     468             :      * @param interactionId     Last interaction displayed
     469             :      * @return if updated
     470             :      */
     471             :     bool setMessageDisplayed(const std::string& uri, const std::string& interactionId);
     472             :     /**
     473             :      * Retrieve last displayed and fetch status per member
     474             :      * @return A map with the following structure:
     475             :      * {uri, {
     476             :      *          {"fetch", "commitId"},
     477             :      *          {"fetched_ts", "timestamp"},
     478             :      *          {"read", "commitId"},
     479             :      *          {"read_ts", "timestamp"}
     480             :      *       }
     481             :      * }
     482             :      */
     483             :     std::map<std::string, std::map<std::string, std::string>> messageStatus() const;
     484             :     /**
     485             :      * Update fetch/read status
     486             :      * @param messageStatus     A map with the following structure:
     487             :      * {uri, {
     488             :      *          {"fetch", "commitId"},
     489             :      *          {"fetched_ts", "timestamp"},
     490             :      *          {"read", "commitId"},
     491             :      *          {"read_ts", "timestamp"}
     492             :      *       }
     493             :      * }
     494             :      */
     495             :     void updateMessageStatus(const std::map<std::string, std::map<std::string, std::string>>& messageStatus);
     496             :     void onMessageStatusChanged(
     497             :         const std::function<void(const std::map<std::string, std::map<std::string, std::string>>&)>& cb);
     498             :     /**
     499             :      * Retrieve how many interactions there is from HEAD to interactionId
     500             :      * @param toId      "" for getting the whole history
     501             :      * @param fromId    "" => HEAD
     502             :      * @param authorURI author to stop counting
     503             :      * @return number of interactions since interactionId
     504             :      */
     505             :     uint32_t countInteractions(const std::string& toId,
     506             :                                const std::string& fromId = "",
     507             :                                const std::string& authorUri = "") const;
     508             :     /**
     509             :      * Search in the conversation via a filter
     510             :      * @param req       Id of the request
     511             :      * @param filter    Parameters for the search
     512             :      * @param flag      To check when search is finished
     513             :      * @note triggers messagesFound
     514             :      */
     515             :     void search(uint32_t req, const Filter& filter, const std::shared_ptr<std::atomic_int>& flag) const;
     516             :     /**
     517             :      * Host a conference in the conversation
     518             :      * @note the message must have "confId"
     519             :      * @note Update hostedCalls_ and commit in the conversation
     520             :      * @param message       message to commit
     521             :      * @param cb            callback triggered when committed
     522             :      */
     523             :     void hostConference(Json::Value&& message, OnDoneCb&& cb = {});
     524             :     /**
     525             :      * Announce the end of a call
     526             :      * @note the message must have "confId"
     527             :      * @note called when conference is finished
     528             :      * @param message       message to commit
     529             :      * @param cb            callback triggered when committed
     530             :      */
     531             :     void removeActiveConference(Json::Value&& message, OnDoneCb&& cb = {});
     532             :     /**
     533             :      * Check if we're currently hosting this conference
     534             :      * @param confId
     535             :      * @return true if hosting
     536             :      */
     537             :     bool isHosting(const std::string& confId) const;
     538             :     /**
     539             :      * Return current detected calls
     540             :      * @return a vector of map with the following keys: "id", "uri", "device"
     541             :      */
     542             :     std::vector<std::map<std::string, std::string>> currentCalls() const;
     543             : 
     544             :     /**
     545             :      * Git operations will need a ChannelSocket for cloning/fetching commits
     546             :      * Because libgit2 is a C library, we store the pointer in the corresponding conversation
     547             :      * and the GitTransport will inject to libgit2 whenever needed
     548             :      */
     549             :     std::shared_ptr<dhtnet::ChannelSocket> gitSocket(const DeviceId& deviceId) const;
     550             :     void addGitSocket(const DeviceId& deviceId, const std::shared_ptr<dhtnet::ChannelSocket>& socket);
     551             :     void removeGitSocket(const DeviceId& deviceId);
     552             : 
     553             :     /**
     554             :      * Stop SwarmManager, bootstrap and gitSockets
     555             :      */
     556             :     void shutdownConnections();
     557             :     /**
     558             :      * Used to avoid multiple connections, we just check if we got a swarm channel with a specific
     559             :      * device
     560             :      * @param deviceId
     561             :      */
     562             :     bool hasSwarmChannel(const std::string& deviceId);
     563             : 
     564             :     /**
     565             :      * If we change from one network to one another, we will need to update the state of the connections
     566             :      */
     567             :     void connectivityChanged();
     568             : 
     569             :     /**
     570             :      * @return getAllNodes()    Nodes that are linked to the conversation
     571             :      */
     572             :     std::vector<jami::DeviceId> getDeviceIdList() const;
     573             : 
     574             :     /**
     575             :      * Get Typers object
     576             :      * @return Typers object
     577             :      */
     578             :     std::shared_ptr<Typers> typers() const;
     579             : 
     580             : private:
     581             :     std::shared_ptr<Conversation> shared() { return std::static_pointer_cast<Conversation>(shared_from_this()); }
     582             :     std::shared_ptr<Conversation const> shared() const
     583             :     {
     584             :         return std::static_pointer_cast<Conversation const>(shared_from_this());
     585             :     }
     586        5293 :     std::weak_ptr<Conversation> weak() { return std::static_pointer_cast<Conversation>(shared_from_this()); }
     587           4 :     std::weak_ptr<Conversation const> weak() const
     588             :     {
     589           4 :         return std::static_pointer_cast<Conversation const>(shared_from_this());
     590             :     }
     591             : 
     592             :     // Private because of weak()
     593             :     /**
     594             :      * Used by bootstrap() to launch the fallback
     595             :      * @param ec
     596             :      * @param members       Members to try to connect
     597             :      */
     598             :     void checkBootstrapMember(const asio::error_code& ec, std::vector<std::map<std::string, std::string>> members);
     599             : 
     600             :     class Impl;
     601             :     std::unique_ptr<Impl> pimpl_;
     602             : };
     603             : 
     604             : } // namespace jami

Generated by: LCOV version 1.14