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