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 707 : 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 1078 : bool affectsList() const
68 : {
69 1078 : 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 689 : ~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 : #endif
108 :
109 : void monitor();
110 :
111 : /**
112 : * Bootstrap swarm managers to other peers
113 : */
114 : void bootstrap(const std::string& convId = "");
115 :
116 : /**
117 : * Clear not removed fetch
118 : */
119 : void clearPendingFetch();
120 :
121 : /**
122 : * Reload requests from file
123 : */
124 : void reloadRequests();
125 :
126 : /**
127 : * Return all conversation's id (including syncing ones)
128 : */
129 : std::vector<std::string> getConversations() const;
130 :
131 : /**
132 : * Get related conversation with member
133 : * @param uri The member to search for
134 : * @return the conversation id if found else empty
135 : */
136 : std::string getOneToOneConversation(const std::string& uri) const noexcept;
137 :
138 : /**
139 : * Replace linked conversation in contact's details
140 : * @param uri Of the contact
141 : * @param oldConv Current conversation
142 : * @param newConv
143 : * @return if replaced
144 : */
145 : bool updateConvForContact(const std::string& uri, const std::string& oldConv, const std::string& newConv);
146 :
147 : /**
148 : * Return conversation's requests
149 : */
150 : std::vector<std::map<std::string, std::string>> getConversationRequests() const;
151 :
152 : /**
153 : * Called when detecting a new trust request with linked one to one
154 : * @param uri Sender's URI
155 : * @param conversationId Related conversation's id
156 : * @param payload VCard
157 : * @param received Received time
158 : */
159 : void onTrustRequest(const std::string& uri,
160 : const std::string& conversationId,
161 : const std::vector<uint8_t>& payload,
162 : TimePoint received);
163 :
164 : /**
165 : * Called when receiving a new conversation's request
166 : * @param from Sender
167 : * @param value Conversation's request
168 : */
169 : void onConversationRequest(const std::string& from, const Json::Value& value);
170 :
171 : /**
172 : * Retrieve author of a conversation request
173 : * @param convId Conversation's id
174 : * @return the author of the conversation request
175 : */
176 : std::string peerFromConversationRequest(const std::string& convId) const;
177 :
178 : /**
179 : * Called when a peer needs an invite for a conversation (generally after that they received
180 : * a commit notification for a conversation they don't have yet)
181 : * @param from
182 : * @param conversationId
183 : */
184 : void onNeedConversationRequest(const std::string& from, const std::string& conversationId);
185 :
186 : /**
187 : * Accept a conversation's request
188 : * @param convId
189 : * @param deviceId If a trust request is accepted from a device (can help to sync)
190 : */
191 : void acceptConversationRequest(const std::string& conversationId, const std::string& deviceId = "");
192 :
193 : /**
194 : * Decline a conversation's request
195 : * @param convId
196 : */
197 : void declineConversationRequest(const std::string& conversationId);
198 :
199 : /**
200 : * Clone conversation from a member
201 : * @param conversationId
202 : * @param uri
203 : */
204 : void cloneConversationFrom(const std::string& conversationId, const std::string& uri);
205 :
206 : /**
207 : * Starts a new conversation
208 : * @param mode Wanted mode
209 : * @param otherMember If needed (one to one)
210 : * @return conversation's id
211 : */
212 : std::string startConversation(ConversationMode mode = ConversationMode::INVITES_ONLY,
213 : const dht::InfoHash& otherMember = {});
214 :
215 : void createCommit(const std::string& conversationId,
216 : CommitMessage&& commitMessage,
217 : bool announce = true,
218 : OnCommitCb&& onCommit = {},
219 : OnDoneCb&& cb = {});
220 :
221 : void sendMessage(const std::string& conversationId,
222 : std::string message,
223 : const std::string& replyTo = "",
224 : bool announce = true,
225 : OnCommitCb&& onCommit = {},
226 : OnDoneCb&& cb = {});
227 :
228 : void editMessage(const std::string& conversationId, const std::string& newBody, const std::string& editedId);
229 : void reactToMessage(const std::string& conversationId, const std::string& newBody, const std::string& reactToId);
230 :
231 : /**
232 : * Add to the related conversation the call history message
233 : * @param uri Peer number
234 : * @param duration_ms The call duration in ms
235 : * @param reason
236 : */
237 : void addCallHistoryMessage(const std::string& uri, uint64_t duration_ms, const std::string& reason);
238 :
239 : // Received that a peer displayed a message
240 : bool onMessageDisplayed(const std::string& peer,
241 : const std::string& conversationId,
242 : const std::string& interactionId);
243 : std::map<std::string, std::map<std::string, std::map<std::string, std::string>>> convMessageStatus() const;
244 :
245 : /**
246 : * Load conversation's messages and emit SwarmLoaded.
247 : * If plugin chat handlers are registered, body-overwrite transforms are applied
248 : * after loading and SwarmMessageUpdated is emitted for each affected message.
249 : * @param conversationId Conversation to load
250 : * @param fromMessage Start loading from this message id (empty = latest)
251 : * @param n Max interactions to load (0 = default)
252 : * @return id of the operation
253 : */
254 : uint32_t loadConversation(const std::string& conversationId, const std::string& fromMessage = "", size_t n = 0);
255 : uint32_t loadSwarmUntil(const std::string& conversationId,
256 : const std::string& fromMessage,
257 : const std::string& toMessage);
258 : /**
259 : * Clear loaded interactions
260 : * @param conversationId
261 : */
262 : void clearCache(const std::string& conversationId);
263 :
264 : // File transfer
265 : /**
266 : * Returns related transfer manager
267 : * @param id Conversation's id
268 : * @return nullptr if not found, else the manager
269 : */
270 : std::shared_ptr<TransferManager> dataTransfer(const std::string& id) const;
271 :
272 : /**
273 : * Choose if we can accept channel request
274 : * @param member Member to check
275 : * @param fileId File transfer to check (needs to be waiting)
276 : * @param verifyShaSum For debug only
277 : * @return if we accept the channel request
278 : */
279 : bool onFileChannelRequest(const std::string& conversationId,
280 : const std::string& member,
281 : const std::string& fileId,
282 : bool verifyShaSum = true) const;
283 :
284 : /**
285 : * Ask conversation's members to send a file to this device
286 : * @param conversationId Related conversation
287 : * @param interactionId Related interaction
288 : * @param fileId Related fileId
289 : * @param path where to download the file
290 : */
291 : bool downloadFile(const std::string& conversationId,
292 : const std::string& interactionId,
293 : const std::string& fileId,
294 : const std::string& path);
295 :
296 : // Sync
297 : /**
298 : * Sync conversations with detected peer
299 : */
300 : void syncConversations(const std::string& peer, const std::string& deviceId);
301 :
302 : /**
303 : * Detect new conversations and request from other devices
304 : * @param msg Received data
305 : * @param peerId Sender
306 : * @param deviceId
307 : */
308 : void onSyncData(const SyncMsg& msg, const std::string& peerId, const std::string& deviceId);
309 :
310 : /**
311 : * Check if we need to share infos with a contact
312 : * @param memberUri
313 : * @param deviceId
314 : */
315 : bool needsSyncingWith(const std::string& memberUri) const;
316 :
317 : /**
318 : * Notify that a peer fetched a commit
319 : * @note: this definitely remove the repository when needed (when we left and someone fetched
320 : * the information)
321 : * @param conversationId Related conv
322 : * @param deviceId Device who synced
323 : * @param commit HEAD synced
324 : */
325 : void setFetched(const std::string& conversationId, const std::string& deviceId, const std::string& commit);
326 :
327 : /**
328 : * Launch fetch on new commit
329 : * @param peer Who sent the notification
330 : * @param deviceId Who sent the notification
331 : * @param conversationId Related conversation
332 : * @param commitId Commit to retrieve
333 : */
334 : void fetchNewCommits(const std::string& peer,
335 : const std::string& deviceId,
336 : const std::string& conversationId,
337 : const std::string& commitId);
338 :
339 : // Conversation's member
340 : /**
341 : * Adds a new member to a conversation (this will triggers a member event + new message on success)
342 : * @param conversationId
343 : * @param contactUri
344 : * @param sendRequest If we need to inform the peer (used for tests)
345 : */
346 : void addConversationMember(const std::string& conversationId,
347 : const dht::InfoHash& contactUri,
348 : bool sendRequest = true);
349 : /**
350 : * Remove a member from a conversation (this will trigger a member event + new message on success)
351 : * @param conversationId
352 : * @param contactUri
353 : * @param isDevice
354 : */
355 : void removeConversationMember(const std::string& conversationId,
356 : const dht::InfoHash& contactUri,
357 : bool isDevice = false);
358 : /**
359 : * Get members
360 : * @param conversationId
361 : * @param includeBanned
362 : * @return a map of members with their role and details
363 : */
364 : std::vector<std::map<std::string, std::string>> getConversationMembers(const std::string& conversationId,
365 : bool includeBanned = false) const;
366 : /**
367 : * Retrieve the number of interactions from interactionId to HEAD
368 : * @param convId
369 : * @param interactionId "" for getting the whole history
370 : * @param authorUri Stop when detect author
371 : * @return number of interactions since interactionId
372 : */
373 : uint32_t countInteractions(const std::string& convId,
374 : const std::string& toId,
375 : const std::string& fromId,
376 : const std::string& authorUri) const;
377 :
378 : /**
379 : * Search in conversations via a filter
380 : * @param req Id of the request
381 : * @param convId Leave empty to search in all conversation, else add the conversation's id
382 : * @param filter Parameters for the search
383 : * @note triggers messagesFound
384 : */
385 : void search(uint32_t req, const std::string& convId, const Filter& filter) const;
386 :
387 : // Conversation's infos management
388 : /**
389 : * Update metadatas from conversations (like title, avatar, etc)
390 : * @param conversationId
391 : * @param infos
392 : * @param sync If we need to sync with others (used for tests)
393 : */
394 : void updateConversationInfos(const std::string& conversationId,
395 : const std::map<std::string, std::string>& infos,
396 : bool sync = true);
397 : std::map<std::string, std::string> conversationInfos(const std::string& conversationId) const;
398 : /**
399 : * Update user's preferences (like color, notifications, etc) to be synced across devices
400 : * @param conversationId
401 : * @param preferences
402 : */
403 : void setConversationPreferences(const std::string& conversationId, const std::map<std::string, std::string>& prefs);
404 : std::map<std::string, std::string> getConversationPreferences(const std::string& conversationId,
405 : bool includeCreated = false) const;
406 : /**
407 : * Retrieve all conversation preferences to sync with other devices
408 : */
409 : std::map<std::string, std::map<std::string, std::string>> convPreferences() const;
410 : // Get the map into a VCard format for storing
411 : std::vector<uint8_t> conversationVCard(const std::string& conversationId) const;
412 :
413 : bool isMemberBanned(const std::string& convId, const std::string& uri) const;
414 : bool isDeviceBanned(const std::string& convId, const std::string& deviceId) const;
415 : bool isPeerAuthorized(const std::string& convId,
416 : const std::string& uri,
417 : const std::string& deviceId,
418 : bool includeInvited = false) const;
419 :
420 : // Remove swarm
421 : /**
422 : * Remove one to one conversations related to a contact
423 : * @param uri Of the contact
424 : * @param ban If banned
425 : */
426 : void removeContact(const std::string& uri, bool ban);
427 :
428 : /**
429 : * Remove a conversation, but not the contact
430 : * @param conversationId
431 : * @return if successfully removed
432 : */
433 : bool removeConversation(const std::string& conversationId);
434 : /**
435 : * Search for an existing one-to-one conversation
436 : * that exactly matches the given set of member URIs.
437 : * @param excludedConversationId Conversation ID to be ignored during the search.
438 : * @param targetUris The set of member URIs that must match exactly.
439 : * @return The ID of the matching conversation if found, otherwise an empty string.
440 : */
441 : std::string findMatchingOneToOneConversation(const std::string& excludedConversationId,
442 : const std::set<std::string>& targetUris) const;
443 : /**
444 : * Check if we're hosting a specific conference
445 : * @param conversationId (empty to search all conv)
446 : * @param confId
447 : * @return true if hosting this conference
448 : */
449 : bool isHosting(const std::string& conversationId, const std::string& confId) const;
450 : /**
451 : * Return active calls
452 : * @param convId Which conversation to choose
453 : * @return {{"id":id}, {"uri":uri}, {"device":device}}
454 : */
455 : std::vector<std::map<std::string, std::string>> getActiveCalls(const std::string& conversationId) const;
456 : /**
457 : * Call the conversation
458 : * @param url Url to call (swarm:conversation or swarm:conv/account/device/conf to join)
459 : * @param mediaList The media list
460 : * @param cb Callback to pass which device to call (called in the same thread)
461 : * @return call if a call is started, else nullptr
462 : */
463 : std::shared_ptr<SIPCall> call(
464 : const std::string& url,
465 : const std::vector<libjami::MediaMap>& mediaList,
466 : std::function<void(const std::string&, const DeviceId&, const std::shared_ptr<SIPCall>&)>&& cb);
467 : void hostConference(const std::string& conversationId,
468 : const std::string& confId,
469 : const std::string& callId,
470 : const std::vector<libjami::MediaMap>& mediaList = {});
471 :
472 : // The following methods modify what is stored on the disk
473 : static void saveConvInfos(const std::string& accountId, const std::map<std::string, ConvInfo>& conversations);
474 : static void saveConvInfosToPath(const std::filesystem::path& path,
475 : const std::map<std::string, ConvInfo>& conversations);
476 : static void saveConvRequests(const std::string& accountId,
477 : const std::map<std::string, ConversationRequest>& conversationsRequests);
478 : static void saveConvRequestsToPath(const std::filesystem::path& path,
479 : const std::map<std::string, ConversationRequest>& conversationsRequests);
480 :
481 : static std::map<std::string, ConvInfo> convInfos(const std::string& accountId);
482 : static std::map<std::string, ConvInfo> convInfosFromPath(const std::filesystem::path& path);
483 : static std::map<std::string, ConversationRequest> convRequests(const std::string& accountId);
484 : static std::map<std::string, ConversationRequest> convRequestsFromPath(const std::filesystem::path& path);
485 : void addConvInfo(const ConvInfo& info);
486 :
487 : /**
488 : * Get a conversation
489 : * @param convId
490 : */
491 : std::shared_ptr<Conversation> getConversation(const std::string& convId);
492 : /**
493 : * Return current git socket used for a conversation
494 : * @param deviceId Related device
495 : * @param conversationId Related conversation
496 : * @return the related socket
497 : */
498 : std::shared_ptr<dhtnet::ChannelSocket> gitSocket(std::string_view deviceId, std::string_view convId) const;
499 : void removeGitSocket(std::string_view deviceId, std::string_view convId);
500 : void addGitSocket(std::string_view deviceId,
501 : std::string_view convId,
502 : const std::shared_ptr<dhtnet::ChannelSocket>& channel);
503 : /**
504 : * Clear all connection (swarm channels)
505 : */
506 : void shutdownConnections();
507 : /**
508 : * Add a swarm connection
509 : * @param conversationId
510 : * @param socket
511 : */
512 : void addSwarmChannel(const std::string& conversationId, std::shared_ptr<dhtnet::ChannelSocket> socket);
513 : /**
514 : * Notify conversations that a new device is connected.
515 : * This allows the DRT to decide whether to open a swarm channel.
516 : * @param peerUri The URI of the peer who owns the device
517 : * @param deviceId The device that connected
518 : */
519 : void addKnownDevice(const std::string& peerUri, const DeviceId& deviceId);
520 :
521 : /**
522 : * Triggers a bucket maintainance for DRTs
523 : */
524 : void connectivityChanged();
525 :
526 : /**
527 : * Get Typers object for a conversation
528 : * @param convId
529 : * @return the Typer object
530 : */
531 : std::shared_ptr<Typers> getTypers(const std::string& convId);
532 :
533 : private:
534 : class Impl;
535 : std::shared_ptr<Impl> pimpl_;
536 : };
537 :
538 : } // namespace jami
|