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