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