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 344 : 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 474 : bool isOneToOne() const
107 : {
108 : try {
109 624 : return metadatas.at("mode") == "0";
110 75 : } catch (...) {
111 75 : }
112 75 : return true;
113 : }
114 :
115 173 : ConversationMode mode() const
116 : {
117 : try {
118 271 : return to_enum<ConversationMode>(metadatas.at("mode"));
119 49 : } catch (...) {
120 49 : }
121 49 : return ConversationMode::ONE_TO_ONE;
122 : }
123 :
124 290 : 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 668 : ConvInfo() = default;
138 30 : 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 13587 : bool isRemoved() const { return removed >= created; }
145 :
146 2818 : ConvInfo& operator=(const ConvInfo&) = default;
147 186 : ConvInfo& operator=(ConvInfo&&) = default;
148 :
149 : Json::Value toJson() const;
150 :
151 3271 : 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 : * Add known devices to the swarm manager
202 : * @param devices
203 : * @param memberUri
204 : */
205 : void addKnownDevices(const std::vector<DeviceId>& devices, const std::string& memberUri);
206 :
207 : /**
208 : * Refresh active calls.
209 : * @note: If the host crash during a call, when initializing, we need to update
210 : * and commit all the crashed calls
211 : * @return Commits added
212 : */
213 : std::vector<std::string> commitsEndedCalls();
214 :
215 : void onMembersChanged(OnMembersChanged&& cb);
216 :
217 : /**
218 : * Set the callback that will be called whenever a new socket will be needed
219 : * @param cb
220 : */
221 : void onNeedSocket(NeedSocketCb cb);
222 : /**
223 : * Add swarm connection to the DRT
224 : * @param channel Related channel
225 : */
226 : void addSwarmChannel(std::shared_ptr<dhtnet::ChannelSocket> channel);
227 :
228 : /**
229 : * Get conversation's id
230 : * @return conversation Id
231 : */
232 : std::string id() const;
233 :
234 : // Member management
235 : /**
236 : * Add conversation member
237 : * @param uri Member to add
238 : * @param cb On done cb
239 : */
240 : void addMember(const std::string& contactUri, const OnDoneCb& cb = {});
241 : void removeMember(const std::string& contactUri, bool isDevice, const OnDoneCb& cb = {});
242 : /**
243 : * @param includeInvited If we want invited members
244 : * @param includeLeft If we want left members
245 : * @param includeBanned If we want banned members
246 : * @return a vector of member details:
247 : * {
248 : * "uri":"xxx",
249 : * "role":"member/admin/invited",
250 : * "lastDisplayed":"id"
251 : * ...
252 : * }
253 : */
254 : std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited = false,
255 : bool includeLeft = false,
256 : bool includeBanned = false) const;
257 :
258 : /**
259 : * @param filter If we want to remove one member
260 : * @param filteredRoles If we want to ignore some roles
261 : * @return members' uris
262 : */
263 : std::set<std::string> memberUris(std::string_view filter = {},
264 : const std::set<MemberRole>& filteredRoles = {MemberRole::INVITED,
265 : MemberRole::LEFT,
266 : MemberRole::BANNED}) const;
267 :
268 : std::vector<std::map<std::string, std::string>> getTrackedMembers() const;
269 :
270 : /**
271 : * Get peers to sync with. This is mostly managed by the DRT
272 : * @return some mobile nodes and all connected nodes
273 : */
274 : std::vector<NodeId> peersToSyncWith() const;
275 : /**
276 : * Check if we're at least connected to one node
277 : * @return if the DRT is connected
278 : */
279 : bool isBootstrapped() const;
280 : /**
281 : * Retrieve the uri from a deviceId
282 : * @note used by swarm manager (peersToSyncWith)
283 : * @param deviceId
284 : * @return corresponding issuer
285 : */
286 : std::string uriFromDevice(const std::string& deviceId) const;
287 :
288 : /**
289 : * Join a conversation
290 : * @return commit id to send
291 : */
292 : std::string join();
293 :
294 : /**
295 : * Test if an URI is a member
296 : * @param uri URI to test
297 : * @return true if uri is a member
298 : */
299 : bool isMember(const std::string& uri, bool includeInvited = false) const;
300 : bool isBanned(const std::string& uri) const;
301 :
302 : // Message send
303 : void sendMessage(Json::Value&& message,
304 : const std::string& replyTo = "",
305 : OnCommitCb&& onCommit = {},
306 : OnDoneCb&& cb = {});
307 : /**
308 : * Get a range of messages
309 : * @param cb The callback when loaded
310 : * @param options The log options
311 : */
312 : void loadMessages(const OnLoadMessages& cb, const LogOptions& options);
313 : /**
314 : * Clear all cached messages
315 : */
316 : void clearCache();
317 : /**
318 : * Check if a commit exists in the repository
319 : * @param commitId The commit id to check
320 : * @return true if the commit was found, false if not or if an error occurred
321 : */
322 : bool hasCommit(const std::string& commitId) const;
323 : /**
324 : * Retrieve one commit
325 : * @param commitId
326 : * @return The commit if found
327 : */
328 : std::optional<std::map<std::string, std::string>> getCommit(const std::string& commitId) const;
329 : /**
330 : * Get last commit id
331 : * @return last commit id
332 : */
333 : std::string lastCommitId() const;
334 :
335 : /**
336 : * Fetch and merge from peer
337 : * @param deviceId Peer device
338 : * @param cb On pulled callback
339 : * @param commitId Commit id that triggered this fetch
340 : * @return true if callback will be called later
341 : */
342 : bool pull(const std::string& deviceId, OnPullCb&& cb, std::string commitId = "");
343 : /**
344 : * Fetch new commits and re-ask for waiting files
345 : * @param member
346 : * @param deviceId
347 : * @param cb cf pull()
348 : * @param commitId cf pull()
349 : */
350 : void sync(const std::string& member, const std::string& deviceId, OnPullCb&& cb, std::string commitId = "");
351 :
352 : /**
353 : * Generate an invitation to send to new contacts
354 : * @return the invite to send
355 : */
356 : std::map<std::string, std::string> generateInvitation() const;
357 :
358 : /**
359 : * Leave a conversation
360 : * @return commit id to send
361 : */
362 : std::string leave();
363 :
364 : /**
365 : * Set a conversation as removing (when loading convInfo and still not sync)
366 : * @todo: not a big fan to see this here. can be set in the constructor
367 : * cause it's used by jamiaccount when loading conversations
368 : */
369 : void setRemovingFlag();
370 :
371 : /**
372 : * Check if we are removing the conversation
373 : * @return true if left the room
374 : */
375 : bool isRemoving();
376 :
377 : /**
378 : * Erase all related datas
379 : */
380 : void erase();
381 :
382 : /**
383 : * Get conversation's mode
384 : * @return the mode
385 : */
386 : ConversationMode mode() const;
387 :
388 : /**
389 : * One to one util, get initial members
390 : * @return initial members
391 : */
392 : std::vector<std::string> getInitialMembers() const;
393 : bool isInitialMember(const std::string& uri) const;
394 :
395 : /**
396 : * Change repository's infos
397 : * @param map New infos (supported keys: title, description, avatar)
398 : * @param cb On commited
399 : */
400 : void updateInfos(const std::map<std::string, std::string>& map, const OnDoneCb& cb = {});
401 :
402 : /**
403 : * Change user's preferences
404 : * @param map New preferences
405 : */
406 : void updatePreferences(const std::map<std::string, std::string>& map);
407 :
408 : /**
409 : * Retrieve current infos (title, description, avatar, mode)
410 : * @return infos
411 : */
412 : std::map<std::string, std::string> infos() const;
413 : /**
414 : * Retrieve current preferences (color, notification, etc)
415 : * @param includeLastModified If we want to know when the preferences were modified
416 : * @return preferences
417 : */
418 : std::map<std::string, std::string> preferences(bool includeLastModified) const;
419 : std::vector<uint8_t> vCard() const;
420 :
421 : /////// File transfer
422 :
423 : /**
424 : * Access to transfer manager
425 : */
426 : std::shared_ptr<TransferManager> dataTransfer() const;
427 :
428 : /**
429 : * Choose if we can accept channel request
430 : * @param member member to check
431 : * @param fileId file transfer to check (needs to be waiting)
432 : * @param verifyShaSum for debug only
433 : * @return if we accept the channel request
434 : */
435 : bool onFileChannelRequest(const std::string& member,
436 : const std::string& fileId,
437 : std::filesystem::path& path,
438 : std::string& sha3sum) const;
439 : /**
440 : * Adds a file to the waiting list and ask members
441 : * @param interactionId Related interaction id
442 : * @param fileId Related id
443 : * @param path Destination
444 : * @param member Member if we know from who to pull file
445 : * @param deviceId Device if we know from who to pull file
446 : * @return id of the file
447 : */
448 : bool downloadFile(const std::string& interactionId,
449 : const std::string& fileId,
450 : const std::string& path,
451 : const std::string& member = "",
452 : const std::string& deviceId = "");
453 :
454 : /**
455 : * Reset fetched information
456 : */
457 : void clearFetched();
458 : /**
459 : * Store information about who fetch or not. This simplify sync (sync when a device without the
460 : * last fetch is detected)
461 : * @param deviceId
462 : * @param commitId
463 : */
464 : void hasFetched(const std::string& deviceId, const std::string& commitId);
465 :
466 : /**
467 : * Store last read commit (returned in getMembers)
468 : * @param uri Of the member
469 : * @param interactionId Last interaction displayed
470 : * @return if updated
471 : */
472 : bool setMessageDisplayed(const std::string& uri, const std::string& interactionId);
473 : /**
474 : * Retrieve last displayed and fetch status per member
475 : * @return A map with the following structure:
476 : * {uri, {
477 : * {"fetch", "commitId"},
478 : * {"fetched_ts", "timestamp"},
479 : * {"read", "commitId"},
480 : * {"read_ts", "timestamp"}
481 : * }
482 : * }
483 : */
484 : std::map<std::string, std::map<std::string, std::string>> messageStatus() const;
485 : /**
486 : * Update fetch/read status
487 : * @param messageStatus A map with the following structure:
488 : * {uri, {
489 : * {"fetch", "commitId"},
490 : * {"fetched_ts", "timestamp"},
491 : * {"read", "commitId"},
492 : * {"read_ts", "timestamp"}
493 : * }
494 : * }
495 : */
496 : void updateMessageStatus(const std::map<std::string, std::map<std::string, std::string>>& messageStatus);
497 : void onMessageStatusChanged(
498 : const std::function<void(const std::map<std::string, std::map<std::string, std::string>>&)>& cb);
499 : /**
500 : * Retrieve how many interactions there is from HEAD to interactionId
501 : * @param toId "" for getting the whole history
502 : * @param fromId "" => HEAD
503 : * @param authorURI author to stop counting
504 : * @return number of interactions since interactionId
505 : */
506 : uint32_t countInteractions(const std::string& toId,
507 : const std::string& fromId = "",
508 : const std::string& authorUri = "") const;
509 : /**
510 : * Search in the conversation via a filter
511 : * @param req Id of the request
512 : * @param filter Parameters for the search
513 : * @param flag To check when search is finished
514 : * @note triggers messagesFound
515 : */
516 : void search(uint32_t req, const Filter& filter, const std::shared_ptr<std::atomic_int>& flag) const;
517 : /**
518 : * Host a conference in the conversation
519 : * @note the message must have "confId"
520 : * @note Update hostedCalls_ and commit in the conversation
521 : * @param message message to commit
522 : * @param cb callback triggered when committed
523 : */
524 : void hostConference(Json::Value&& message, OnDoneCb&& cb = {});
525 : /**
526 : * Announce the end of a call
527 : * @note the message must have "confId"
528 : * @note called when conference is finished
529 : * @param message message to commit
530 : * @param cb callback triggered when committed
531 : */
532 : void removeActiveConference(Json::Value&& message, OnDoneCb&& cb = {});
533 : /**
534 : * Check if we're currently hosting this conference
535 : * @param confId
536 : * @return true if hosting
537 : */
538 : bool isHosting(const std::string& confId) const;
539 : /**
540 : * Return current detected calls
541 : * @return a vector of map with the following keys: "id", "uri", "device"
542 : */
543 : std::vector<std::map<std::string, std::string>> currentCalls() const;
544 :
545 : /**
546 : * Git operations will need a ChannelSocket for cloning/fetching commits
547 : * Because libgit2 is a C library, we store the pointer in the corresponding conversation
548 : * and the GitTransport will inject to libgit2 whenever needed
549 : */
550 : std::shared_ptr<dhtnet::ChannelSocket> gitSocket(const DeviceId& deviceId) const;
551 : void addGitSocket(const DeviceId& deviceId, const std::shared_ptr<dhtnet::ChannelSocket>& socket);
552 : void removeGitSocket(const DeviceId& deviceId);
553 :
554 : /**
555 : * Stop SwarmManager, bootstrap and gitSockets
556 : */
557 : void shutdownConnections();
558 : /**
559 : * Used to avoid multiple connections, we just check if we got a swarm channel with a specific
560 : * device
561 : * @param deviceId
562 : */
563 : bool hasSwarmChannel(const std::string& deviceId);
564 :
565 : /**
566 : * If we change from one network to one another, we will need to update the state of the connections
567 : */
568 : void connectivityChanged();
569 :
570 : /**
571 : * @return getAllNodes() Nodes that are linked to the conversation
572 : */
573 : std::vector<jami::DeviceId> getDeviceIdList() const;
574 :
575 : /**
576 : * Get Typers object
577 : * @return Typers object
578 : */
579 : std::shared_ptr<Typers> typers() const;
580 :
581 : /**
582 : * Get connectivity information for the conversation
583 : * @return map of connectivity info
584 : */
585 : std::vector<std::map<std::string, std::string>> getConnectivity() const;
586 :
587 : private:
588 : std::shared_ptr<Conversation> shared() { return std::static_pointer_cast<Conversation>(shared_from_this()); }
589 : std::shared_ptr<Conversation const> shared() const
590 : {
591 : return std::static_pointer_cast<Conversation const>(shared_from_this());
592 : }
593 3660 : std::weak_ptr<Conversation> weak() { return std::static_pointer_cast<Conversation>(shared_from_this()); }
594 4 : std::weak_ptr<Conversation const> weak() const
595 : {
596 4 : return std::static_pointer_cast<Conversation const>(shared_from_this());
597 : }
598 :
599 : // Private because of weak()
600 : class Impl;
601 : std::unique_ptr<Impl> pimpl_;
602 : };
603 :
604 : } // namespace jami
|