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