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 : #pragma once
18 :
19 : #include "jami.h"
20 : #include "jamidht/account_manager.h"
21 : #include "jamidht/conversation.h"
22 : #include "jamidht/conversationrepository.h"
23 : #include "jamidht/jami_contact.h"
24 :
25 : #include <mutex>
26 : #include <optional>
27 : #include <msgpack.hpp>
28 :
29 : namespace jami {
30 : static constexpr const char MIME_TYPE_INVITE[] {"application/invite"};
31 : static constexpr const char MIME_TYPE_GIT[] {"application/im-gitmessage-id"};
32 :
33 : class SIPCall;
34 :
35 : struct SyncMsg
36 : {
37 : DeviceSync ds;
38 : std::map<std::string, ConvInfo> c;
39 : std::map<std::string, ConversationRequest> cr;
40 : // p is conversation's preferences. It's not stored in c, as
41 : // we can update the preferences without touching any confInfo.
42 : std::map<std::string, std::map<std::string, std::string>> p;
43 : // Last displayed messages [[deprecated]]
44 : std::map<std::string, std::map<std::string, std::string>> ld;
45 : // Read & fetched status
46 : /*
47 : * {{ convId,
48 : * { memberUri,, {
49 : * {"fetch", "commitId"},
50 : * {"fetched_ts", "timestamp"},
51 : * {"read", "commitId"},
52 : * {"read_ts", "timestamp"}
53 : * }
54 : * }}
55 : */
56 : std::map<std::string, std::map<std::string, std::map<std::string, std::string>>> ms;
57 :
58 844 : MSGPACK_DEFINE(ds, c, cr, p, ld, ms)
59 :
60 : /**
61 : * Whether this message carries contact/conversation *list* state, i.e.
62 : * contacts, devices, trust requests, conversations or conversation
63 : * requests. Propagating such a change may require (re)connecting to
64 : * devices that are not currently connected. Metadata-only messages
65 : * (preferences `p`, read/fetched status `ms`, deprecated last-displayed
66 : * `ld`) return false and only need to ride existing connections.
67 : */
68 1859 : bool affectsList() const
69 : {
70 1859 : return !ds.devices.empty() || !ds.peers.empty() || !ds.trust_requests.empty() || !c.empty() || !cr.empty();
71 : }
72 : };
73 :
74 : using ChannelCb = std::function<bool(const std::shared_ptr<dhtnet::ChannelSocket>&)>;
75 : using NeedSocketCb
76 : = std::function<void(const std::string&, const std::string&, ChannelCb&&, const std::string&, bool noNewSocket)>;
77 : using SengMsgCb
78 : = std::function<uint64_t(const std::string&, const DeviceId&, std::map<std::string, std::string>, uint64_t)>;
79 : using NeedsSyncingCb = std::function<void(std::shared_ptr<SyncMsg>&&)>;
80 : using OneToOneRecvCb = std::function<void(const std::string&, const std::string&)>;
81 :
82 : class ConversationModule
83 : {
84 : public:
85 : ConversationModule(std::shared_ptr<JamiAccount> account,
86 : std::shared_ptr<AccountManager> accountManager,
87 : NeedsSyncingCb&& needsSyncingCb,
88 : SengMsgCb&& sendMsgCb,
89 : NeedSocketCb&& onNeedSocket,
90 : NeedSocketCb&& onNeedSwarmSocket,
91 : OneToOneRecvCb&& oneToOneRecvCb,
92 : bool autoLoadConversations = true);
93 873 : ~ConversationModule() = default;
94 :
95 : void setAccountManager(std::shared_ptr<AccountManager> accountManager);
96 :
97 : /**
98 : * Refresh information about conversations
99 : */
100 : void loadConversations();
101 :
102 : void initPresence();
103 :
104 : void loadSingleConversation(const std::string& convId);
105 :
106 : #ifdef LIBJAMI_TEST
107 : void onBootstrapStatus(const std::function<void(std::string, Conversation::BootstrapStatus)>& cb);
108 : void onFetchCompleted(const std::function<void(const std::string&, const std::string&, bool)>& cb);
109 : void onCloneReady(const std::function<void(const std::string&)>& cb);
110 : #endif
111 :
112 : void monitor();
113 :
114 : /**
115 : * Bootstrap swarm managers to other peers
116 : */
117 : void bootstrap(const std::string& convId = "");
118 :
119 : /**
120 : * Clear not removed fetch
121 : */
122 : void clearPendingFetch();
123 :
124 : /**
125 : * Reload requests from file
126 : */
127 : void reloadRequests();
128 :
129 : /**
130 : * Return all conversation's id (including syncing ones)
131 : */
132 : std::vector<std::string> getConversations() const;
133 :
134 : /**
135 : * Get related conversation with member
136 : * @param uri The member to search for
137 : * @return the conversation id if found else empty
138 : */
139 : std::string getOneToOneConversation(const std::string& uri) const noexcept;
140 :
141 : /**
142 : * Replace linked conversation in contact's details
143 : * @param uri Of the contact
144 : * @param oldConv Current conversation
145 : * @param newConv
146 : * @return if replaced
147 : */
148 : bool updateConvForContact(const std::string& uri, const std::string& oldConv, const std::string& newConv);
149 :
150 : /**
151 : * Return conversation's requests
152 : */
153 : std::vector<std::map<std::string, std::string>> getConversationRequests() const;
154 :
155 : /**
156 : * Called when detecting a new trust request with linked one to one
157 : * @param uri Sender's URI
158 : * @param conversationId Related conversation's id
159 : * @param payload VCard
160 : * @param received Received time
161 : * @param invited Sender-embedded invite timestamp (default: unset, meaning unknown)
162 : */
163 : void onTrustRequest(const std::string& uri,
164 : const std::string& conversationId,
165 : const std::vector<uint8_t>& payload,
166 : TimePoint received,
167 : TimePoint invited = {});
168 :
169 : /**
170 : * Called when receiving a new conversation's request
171 : * @param from Sender
172 : * @param value Conversation's request
173 : */
174 : void onConversationRequest(const std::string& from, const Json::Value& value);
175 :
176 : /**
177 : * Retrieve author of a conversation request
178 : * @param convId Conversation's id
179 : * @return the author of the conversation request
180 : */
181 : std::string peerFromConversationRequest(const std::string& convId) const;
182 :
183 : /**
184 : * Called when a peer needs an invite for a conversation (generally after that they received
185 : * a commit notification for a conversation they don't have yet)
186 : * @param from
187 : * @param conversationId
188 : */
189 : void onNeedConversationRequest(const std::string& from, const std::string& conversationId);
190 :
191 : /**
192 : * Accept a conversation's request
193 : * @param convId
194 : * @param deviceId If a trust request is accepted from a device (can help to sync)
195 : */
196 : void acceptConversationRequest(const std::string& conversationId, const std::string& deviceId = "");
197 :
198 : /**
199 : * Decline a conversation's request
200 : * @param convId
201 : */
202 : void declineConversationRequest(const std::string& conversationId);
203 :
204 : /**
205 : * Clone conversation from a member
206 : * @param conversationId
207 : * @param uri
208 : */
209 : void cloneConversationFrom(const std::string& conversationId, const std::string& uri);
210 :
211 : /**
212 : * Starts a new conversation
213 : * @param mode Wanted mode
214 : * @param otherMember If needed (one to one)
215 : * @return conversation's id
216 : */
217 : std::string startConversation(ConversationMode mode = ConversationMode::INVITES_ONLY,
218 : const dht::InfoHash& otherMember = {});
219 :
220 : /**
221 : * Create a collaborative document: a swarm repository of its own (mode
222 : * DOCUMENT), announced in @p parentConversationId, holding CRDT
223 : * checkpoints instead of messages. The creator is its only member and
224 : * admin; other members join when they receive its announcement.
225 : * @param parentConversationId the conversation that announces it
226 : * @param mimeType media type of what the document will hold
227 : * @return the document's repository id, empty on failure
228 : */
229 : std::string startDocument(const std::string& parentConversationId, const std::string& mimeType);
230 :
231 : /**
232 : * Clone a collaborative document from a member's devices.
233 : * The serving holder writes the `add` commit
234 : * at serve time, so the clone this device receives already contains its
235 : * invitation; the standard pending-conversation path then writes `join`.
236 : * Completion is reported through CollaborativeEditing::onRepositoryUpdated
237 : * rather than ConversationReady.
238 : * Every candidate is recorded as an authorized clone source, but the
239 : * fetch is only initiated from the first few: the fallback rounds walk
240 : * the rest, with backoff, if those fail.
241 : * @param documentId the document's repository id
242 : * @param parentConversationId active conversation that announced the document
243 : * @param candidates members to clone from, in order of preference
244 : * @param reopen allow an explicit open to restore a locally removed replica
245 : * @return false if automatic replication was declined or there is no source
246 : */
247 : bool cloneDocumentFrom(const std::string& parentConversationId,
248 : const std::string& documentId,
249 : const std::vector<std::string>& candidates,
250 : bool reopen = true);
251 :
252 : /**
253 : * Whether a clone of this document is in progress (not waiting for a retry).
254 : * @param documentId the document's repository id
255 : */
256 : bool isCloningDocument(const std::string& documentId);
257 :
258 : /**
259 : * Drop this device's replica of a collaborative document.The repository and
260 : * its swarm connections are torn down and the document is recorded as
261 : * removed so it is not recloned on restart.
262 : * @param documentId the document's repository id
263 : */
264 : void removeDocumentReplica(const std::string& documentId);
265 :
266 : /**
267 : * Store an attachment in a held collaborative document and notify the
268 : * swarm of the commit that carries it.
269 : * @param documentId the document's repository id
270 : * @param data the attachment content
271 : * @return the attachment id (content hash), empty on failure
272 : */
273 : std::string addDocumentAttachment(const std::string& documentId, const std::vector<uint8_t>& data);
274 :
275 : void createCommit(const std::string& conversationId,
276 : CommitMessage&& commitMessage,
277 : bool announce = true,
278 : OnCommitCb&& onCommit = {},
279 : OnDoneCb&& cb = {});
280 :
281 : void sendMessage(const std::string& conversationId,
282 : std::string message,
283 : const std::string& replyTo = "",
284 : bool announce = true,
285 : OnCommitCb&& onCommit = {},
286 : OnDoneCb&& cb = {});
287 :
288 : void editMessage(const std::string& conversationId, const std::string& newBody, const std::string& editedId);
289 : void reactToMessage(const std::string& conversationId, const std::string& newBody, const std::string& reactToId);
290 :
291 : /**
292 : * Add to the related conversation the call history message
293 : * @param uri Peer number
294 : * @param duration_ms The call duration in ms
295 : * @param reason
296 : */
297 : void addCallHistoryMessage(const std::string& uri, uint64_t duration_ms, const std::string& reason);
298 :
299 : // Received that a peer displayed a message
300 : bool onMessageDisplayed(const std::string& peer,
301 : const std::string& conversationId,
302 : const std::string& interactionId);
303 : std::map<std::string, std::map<std::string, std::map<std::string, std::string>>> convMessageStatus() const;
304 :
305 : /**
306 : * Load conversation's messages and emit SwarmLoaded.
307 : * If plugin chat handlers are registered, body-overwrite transforms are applied
308 : * after loading and SwarmMessageUpdated is emitted for each affected message.
309 : * @param conversationId Conversation to load
310 : * @param fromMessage Start loading from this message id (empty = latest)
311 : * @param n Max interactions to load (0 = default)
312 : * @return id of the operation
313 : */
314 : uint32_t loadConversation(const std::string& conversationId, const std::string& fromMessage = "", size_t n = 0);
315 : uint32_t loadSwarmUntil(const std::string& conversationId,
316 : const std::string& fromMessage,
317 : const std::string& toMessage);
318 : /**
319 : * Clear loaded interactions
320 : * @param conversationId
321 : */
322 : void clearCache(const std::string& conversationId);
323 :
324 : // File transfer
325 : /**
326 : * Returns related transfer manager
327 : * @param id Conversation's id
328 : * @return nullptr if not found, else the manager
329 : */
330 : std::shared_ptr<TransferManager> dataTransfer(const std::string& id) const;
331 :
332 : /**
333 : * Get current transfer info
334 : * @return the TransferManager::info() result, or nullopt if the conversation is not loaded yet
335 : */
336 : std::optional<bool> fileTransferInfo(const std::string& conversationId,
337 : const std::string& fileId,
338 : std::string& path,
339 : int64_t& total,
340 : int64_t& progress);
341 :
342 : /**
343 : * Choose if we can accept channel request
344 : * @param member Member to check
345 : * @param fileId File transfer to check (needs to be waiting)
346 : * @param verifyShaSum For debug only
347 : * @return if we accept the channel request
348 : */
349 : bool onFileChannelRequest(const std::string& conversationId,
350 : const std::string& member,
351 : const std::string& fileId,
352 : bool verifyShaSum = true) const;
353 :
354 : /**
355 : * Ask conversation's members to send a file to this device
356 : * @param conversationId Related conversation
357 : * @param interactionId Related interaction
358 : * @param fileId Related fileId
359 : * @param path where to download the file
360 : */
361 : bool downloadFile(const std::string& conversationId,
362 : const std::string& interactionId,
363 : const std::string& fileId,
364 : const std::string& path);
365 :
366 : // Sync
367 : /**
368 : * Sync conversations with detected peer
369 : */
370 : void syncConversations(const std::string& peer, const std::string& deviceId);
371 :
372 : /**
373 : * Detect new conversations and request from other devices
374 : * @param msg Received data
375 : * @param peerId Sender
376 : * @param deviceId
377 : */
378 : void onSyncData(const SyncMsg& msg, const std::string& peerId, const std::string& deviceId);
379 :
380 : /**
381 : * Check if we need to share infos with a contact
382 : * @param memberUri
383 : * @param deviceId
384 : */
385 : bool needsSyncingWith(const std::string& memberUri) const;
386 :
387 : /**
388 : * Notify that a peer fetched a commit
389 : * @note: this definitely remove the repository when needed (when we left and someone fetched
390 : * the information)
391 : * @param conversationId Related conv
392 : * @param deviceId Device who synced
393 : * @param commit HEAD synced
394 : */
395 : void setFetched(const std::string& conversationId, const std::string& deviceId, const std::string& commit);
396 :
397 : /**
398 : * Launch fetch on new commit
399 : * @param peer Who sent the notification
400 : * @param deviceId Who sent the notification
401 : * @param conversationId Related conversation
402 : * @param commitId Commit to retrieve
403 : */
404 : void fetchNewCommits(const std::string& peer,
405 : const std::string& deviceId,
406 : const std::string& conversationId,
407 : const std::string& commitId);
408 :
409 : // Conversation's member
410 : /**
411 : * Adds a new member to a conversation (this will triggers a member event + new message on success)
412 : * @param conversationId
413 : * @param contactUri
414 : * @param sendRequest If we need to inform the peer (used for tests)
415 : */
416 : void addConversationMember(const std::string& conversationId,
417 : const dht::InfoHash& contactUri,
418 : bool sendRequest = true);
419 : /**
420 : * Remove a member from a conversation (this will trigger a member event + new message on success)
421 : * @param conversationId
422 : * @param contactUri
423 : * @param isDevice
424 : */
425 : void removeConversationMember(const std::string& conversationId,
426 : const dht::InfoHash& contactUri,
427 : bool isDevice = false);
428 : /**
429 : * Get members
430 : * @param conversationId
431 : * @param includeBanned
432 : * @return a map of members with their role and details
433 : */
434 : std::vector<std::map<std::string, std::string>> getConversationMembers(const std::string& conversationId,
435 : bool includeBanned = false) const;
436 : /**
437 : * Get the permissions granted to the account in a conversation
438 : * @param conversationId
439 : * @return permission names, empty if the conversation is unknown
440 : */
441 : std::vector<std::string> getConversationPermissions(const std::string& conversationId) const;
442 : /**
443 : * Retrieve the number of interactions from interactionId to HEAD
444 : * @param convId
445 : * @param interactionId "" for getting the whole history
446 : * @param authorUri Stop when detect author
447 : * @return number of interactions since interactionId
448 : */
449 : uint32_t countInteractions(const std::string& convId,
450 : const std::string& toId,
451 : const std::string& fromId,
452 : const std::string& authorUri) const;
453 :
454 : /**
455 : * Search in conversations via a filter
456 : * @param req Id of the request
457 : * @param convId Leave empty to search in all conversation, else add the conversation's id
458 : * @param filter Parameters for the search
459 : * @note triggers messagesFound
460 : */
461 : void search(uint32_t req, const std::string& convId, const Filter& filter) const;
462 :
463 : // Conversation's infos management
464 : /**
465 : * Update metadatas from conversations (like title, avatar, etc)
466 : * @param conversationId
467 : * @param infos
468 : * @param sync If we need to sync with others (used for tests)
469 : */
470 : void updateConversationInfos(const std::string& conversationId,
471 : const std::map<std::string, std::string>& infos,
472 : bool sync = true);
473 : std::map<std::string, std::string> conversationInfos(const std::string& conversationId) const;
474 : /**
475 : * Update user's preferences (like color, notifications, etc) to be synced across devices
476 : * @param conversationId
477 : * @param preferences
478 : */
479 : void setConversationPreferences(const std::string& conversationId, const std::map<std::string, std::string>& prefs);
480 : std::map<std::string, std::string> getConversationPreferences(const std::string& conversationId,
481 : bool includeCreated = false) const;
482 : /**
483 : * Retrieve all conversation preferences to sync with other devices
484 : */
485 : std::map<std::string, std::map<std::string, std::string>> convPreferences() const;
486 : // Get the map into a VCard format for storing
487 : std::vector<uint8_t> conversationVCard(const std::string& conversationId) const;
488 :
489 : bool isMemberBanned(const std::string& convId, const std::string& uri) const;
490 : bool isDeviceBanned(const std::string& convId, const std::string& deviceId) const;
491 : bool isPeerAuthorized(const std::string& convId,
492 : const std::string& uri,
493 : const std::string& deviceId,
494 : bool includeInvited = false) const;
495 :
496 : /**
497 : * Check whether a device certificate belongs to a confirmed member of any active,
498 : * locally loaded conversation.
499 : */
500 : bool isPeerAuthorizedByConversation(const dht::crypto::Certificate& certificate) const;
501 :
502 : /**
503 : * The read-only half of authorizeDocumentPeer(): whether this device could
504 : * vouch for the peer, without writing anything. This is what the channel
505 : * pre-check asks — accepting the channel commits us to nothing, the add
506 : * commit is only written when the clone is actually served.
507 : */
508 : bool mayServeDocument(const std::string& documentId, const std::string& uri, const std::string& deviceId) const;
509 :
510 : /**
511 : * Serve-time admission for collaborative documents, tried after
512 : * isPeerAuthorized() said no. A peer in good standing in the document's
513 : * parent conversation may open the document even though no member invited
514 : * it yet: the serving holder vouches for it by writing the `add` commit
515 : * itself, and only then lets the clone proceed. Peers banned from the
516 : * document, or documents this device does not hold, are refused.
517 : * @param documentId the document's repository id
518 : * @param uri requesting peer
519 : * @param deviceId requesting device
520 : * @param cb called with whether the peer may be served
521 : */
522 : void authorizeDocumentPeer(const std::string& documentId,
523 : const std::string& uri,
524 : const std::string& deviceId,
525 : std::function<void(bool)>&& cb);
526 :
527 : // Remove swarm
528 : /**
529 : * Remove one to one conversations related to a contact
530 : * @param uri Of the contact
531 : * @param ban If banned
532 : */
533 : void removeContact(const std::string& uri, bool ban);
534 :
535 : /**
536 : * Remove a conversation, but not the contact
537 : * @param conversationId
538 : * @return if successfully removed
539 : */
540 : bool removeConversation(const std::string& conversationId);
541 : /**
542 : * Search for an existing one-to-one conversation
543 : * that exactly matches the given set of member URIs.
544 : * @param excludedConversationId Conversation ID to be ignored during the search.
545 : * @param targetUris The set of member URIs that must match exactly.
546 : * @return The ID of the matching conversation if found, otherwise an empty string.
547 : */
548 : std::string findMatchingOneToOneConversation(const std::string& excludedConversationId,
549 : const std::set<std::string>& targetUris) const;
550 : /**
551 : * Check if we're hosting a specific conference
552 : * @param conversationId (empty to search all conv)
553 : * @param confId
554 : * @return true if hosting this conference
555 : */
556 : bool isHosting(const std::string& conversationId, const std::string& confId) const;
557 : /**
558 : * Return active calls
559 : * @param convId Which conversation to choose
560 : * @return {{"id":id}, {"uri":uri}, {"device":device}}
561 : */
562 : std::vector<std::map<std::string, std::string>> getActiveCalls(const std::string& conversationId) const;
563 : /**
564 : * Call the conversation
565 : * @param url Url to call (swarm:conversation or swarm:conv/account/device/conf to join)
566 : * @param mediaList The media list
567 : * @param cb Callback to pass which device to call (called in the same thread)
568 : * @return call if a call is started, else nullptr
569 : */
570 : std::shared_ptr<SIPCall> call(
571 : const std::string& url,
572 : const std::vector<libjami::MediaMap>& mediaList,
573 : std::function<void(const std::string&, const DeviceId&, const std::shared_ptr<SIPCall>&)>&& cb);
574 : void hostConference(const std::string& conversationId,
575 : const std::string& confId,
576 : const std::string& callId,
577 : const std::vector<libjami::MediaMap>& mediaList = {});
578 :
579 : // The following methods modify what is stored on the disk
580 : static void saveConvInfos(const std::string& accountId, const std::map<std::string, ConvInfo>& conversations);
581 : static void saveConvInfosToPath(const std::filesystem::path& path,
582 : const std::map<std::string, ConvInfo>& conversations);
583 : static void saveConvRequests(const std::string& accountId,
584 : const std::map<std::string, ConversationRequest>& conversationsRequests);
585 : static void saveConvRequestsToPath(const std::filesystem::path& path,
586 : const std::map<std::string, ConversationRequest>& conversationsRequests);
587 :
588 : static std::map<std::string, ConvInfo> convInfos(const std::string& accountId);
589 : static std::map<std::string, ConvInfo> convInfosFromPath(const std::filesystem::path& path);
590 : static std::map<std::string, ConversationRequest> convRequests(const std::string& accountId);
591 : static std::map<std::string, ConversationRequest> convRequestsFromPath(const std::filesystem::path& path);
592 : void addConvInfo(const ConvInfo& info);
593 :
594 : /**
595 : * Get a conversation
596 : * @param convId
597 : */
598 : std::shared_ptr<Conversation> getConversation(const std::string& convId);
599 : /**
600 : * Return current git socket used for a conversation
601 : * @param deviceId Related device
602 : * @param conversationId Related conversation
603 : * @return the related socket
604 : */
605 : std::shared_ptr<dhtnet::ChannelSocket> gitSocket(std::string_view deviceId, std::string_view convId) const;
606 : /** If @p expected is set, only remove the socket if it is still the registered one. */
607 : void removeGitSocket(std::string_view deviceId,
608 : std::string_view convId,
609 : const std::shared_ptr<dhtnet::ChannelSocket>& expected = {});
610 : /**
611 : * Clear all connection (swarm channels)
612 : */
613 : void shutdownConnections();
614 : /**
615 : * Add a swarm connection
616 : * @param conversationId
617 : * @param socket
618 : */
619 : void addSwarmChannel(const std::string& conversationId, std::shared_ptr<dhtnet::ChannelSocket> socket);
620 : /**
621 : * Notify conversations that a new device is connected.
622 : * This allows the DRT to decide whether to open a swarm channel.
623 : * @param peerUri The URI of the peer who owns the device
624 : * @param deviceId The device that connected
625 : */
626 : void addKnownDevice(const std::string& peerUri, const DeviceId& deviceId);
627 :
628 : /**
629 : * Triggers a bucket maintainance for DRTs
630 : */
631 : void connectivityChanged();
632 :
633 : /**
634 : * Get Typers object for a conversation
635 : * @param convId
636 : * @return the Typer object
637 : */
638 : std::shared_ptr<Typers> getTypers(const std::string& convId);
639 :
640 : private:
641 : class Impl;
642 : std::shared_ptr<Impl> pimpl_;
643 : };
644 :
645 : } // namespace jami
|