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