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