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