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