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 : #pragma once
18 : #include "def.h"
19 : #include "vcard.h"
20 : #include "git_def.h"
21 :
22 : #include <opendht/default_types.h>
23 :
24 : #include <optional>
25 : #include <memory>
26 : #include <string>
27 : #include <vector>
28 :
29 : namespace jami {
30 :
31 : using DeviceId = dht::PkId;
32 :
33 : constexpr auto EFETCH = 1;
34 : constexpr auto EINVALIDMODE = 2;
35 : constexpr auto EVALIDFETCH = 3;
36 : constexpr auto EUNAUTHORIZED = 4;
37 : constexpr auto ECOMMIT = 5;
38 :
39 : class JamiAccount;
40 :
41 : struct LogOptions
42 : {
43 : std::string from {};
44 : std::string to {};
45 : uint64_t nbOfCommits {0}; // maximum number of commits wanted
46 : bool skipMerge {false}; // Do not include merge commits in the log. Used by the module to get
47 : // last interaction without potential merges
48 : bool includeTo {false}; // If we want or not the "to" commit [from-to] or [from-to)
49 : bool fastLog {false}; // Do not parse content, used mostly to count
50 : bool logIfNotFound {true}; // Add a warning in the log if commit is not found
51 :
52 : std::string authorUri {}; // filter commits from author
53 : };
54 :
55 : struct Filter
56 : {
57 : std::string author;
58 : std::string lastId;
59 : std::string regexSearch;
60 : std::string type;
61 : int64_t after {0};
62 : int64_t before {0};
63 : uint32_t maxResult {0};
64 : bool caseSensitive {false};
65 : };
66 :
67 : struct GitAuthor
68 : {
69 : std::string name {};
70 : std::string email {};
71 : };
72 :
73 : enum class ConversationMode : int { ONE_TO_ONE = 0, ADMIN_INVITES_ONLY, INVITES_ONLY, PUBLIC };
74 :
75 : struct ConversationCommit
76 : {
77 : std::string id {};
78 : std::vector<std::string> parents {};
79 : GitAuthor author {};
80 : std::vector<uint8_t> signed_content {};
81 : std::vector<uint8_t> signature {};
82 : std::string commit_msg {};
83 : std::string linearized_parent {};
84 : int64_t timestamp {0};
85 : };
86 :
87 : enum class MemberRole { ADMIN = 0, MEMBER, INVITED, BANNED, LEFT };
88 :
89 : namespace MemberPath {
90 :
91 : static const std::filesystem::path ADMINS {"admins"};
92 : static const std::filesystem::path MEMBERS {"members"};
93 : static const std::filesystem::path INVITED {"invited"};
94 : static const std::filesystem::path BANNED {"banned"};
95 : static const std::filesystem::path DEVICES {"devices"};
96 :
97 : } // namespace MemberPath
98 :
99 : struct ConversationMember
100 : {
101 : std::string uri;
102 : MemberRole role;
103 :
104 581 : std::map<std::string, std::string> map() const
105 : {
106 581 : std::string rolestr;
107 581 : if (role == MemberRole::ADMIN) {
108 421 : rolestr = "admin";
109 160 : } else if (role == MemberRole::MEMBER) {
110 110 : rolestr = "member";
111 50 : } else if (role == MemberRole::INVITED) {
112 44 : rolestr = "invited";
113 6 : } else if (role == MemberRole::BANNED) {
114 6 : rolestr = "banned";
115 0 : } else if (role == MemberRole::LEFT) {
116 0 : rolestr = "left"; // For one to one
117 : }
118 :
119 2324 : return {{"uri", uri}, {"role", rolestr}};
120 581 : }
121 13992 : MSGPACK_DEFINE(uri, role)
122 : };
123 :
124 : enum class CallbackResult { Skip, Break, Ok };
125 :
126 : using PreConditionCb = std::function<CallbackResult(const std::string&, const GitAuthor&, const GitCommit&)>;
127 : using PostConditionCb = std::function<bool(const std::string&, const GitAuthor&, ConversationCommit&)>;
128 : using OnMembersChanged = std::function<void(const std::set<std::string>&)>;
129 :
130 : /**
131 : * This class gives access to the git repository that represents the conversation
132 : */
133 : class LIBJAMI_TEST_EXPORT ConversationRepository
134 : {
135 : public:
136 : #ifdef LIBJAMI_TEST
137 : static bool DISABLE_RESET; // Some tests inject bad files so resetHard() will break the test
138 :
139 : // If true, clone and fetch operations will be performed directly using the target repo's path,
140 : // avoiding the need for setting up a GitServer and a DHTNet connection.
141 : static bool FETCH_FROM_LOCAL_REPOS;
142 : #endif
143 : /**
144 : * Creates a new repository, with initial files, where the first commit hash is the conversation id
145 : * @param account The related account
146 : * @param mode The wanted mode
147 : * @param otherMember The other uri
148 : * @return the conversation repository object
149 : */
150 : static LIBJAMI_TEST_EXPORT std::unique_ptr<ConversationRepository> createConversation(
151 : const std::shared_ptr<JamiAccount>& account,
152 : ConversationMode mode = ConversationMode::INVITES_ONLY,
153 : const std::string& otherMember = "");
154 :
155 : /**
156 : * Clones a conversation on a remote device
157 : * @note This will use the socket registered for the conversation with JamiAccount::addGitSocket()
158 : * @param account The account getting the conversation
159 : * @param deviceId Remote device
160 : * @param conversationId Conversation to clone
161 : */
162 : static LIBJAMI_TEST_EXPORT std::pair<std::unique_ptr<ConversationRepository>, std::vector<ConversationCommit>>
163 : cloneConversation(const std::shared_ptr<JamiAccount>& account,
164 : const std::string& deviceId,
165 : const std::string& conversationId);
166 :
167 : /**
168 : * Open a conversation repository for an account and an id
169 : * @param account The related account
170 : * @param id The conversation id
171 : */
172 : ConversationRepository(const std::shared_ptr<JamiAccount>& account, const std::string& id);
173 : ~ConversationRepository();
174 :
175 : /**
176 : * Write the certificate in /members and commit the change
177 : * @param uri Member to add
178 : * @return the commit id if successful
179 : */
180 : std::string addMember(const std::string& uri);
181 :
182 : /**
183 : * Fetch a remote repository via the given socket
184 : * @note This will use the socket registered for the conversation with JamiAccount::addGitSocket()
185 : * @note will create a remote identified by the deviceId
186 : * @param remoteDeviceId Remote device id to fetch
187 : * @return if the operation was successful
188 : */
189 : bool fetch(const std::string& remoteDeviceId);
190 :
191 : /**
192 : * Merge the history of the conversation with another peer
193 : * @param uri The peer uri
194 : * @param disconnectFromPeerCb Callback to disconnect from peer when banning
195 : * @return A vector of media maps representing the merged history
196 : */
197 : std::vector<std::map<std::string, std::string>> mergeHistory(
198 : const std::string& uri, std::function<void(const std::string&)>&& disconnectFromPeerCb = {});
199 :
200 : /**
201 : * Retrieve remote head. Can be useful after a fetch operation
202 : * @param remoteDeviceId The remote name
203 : * @param branch Remote branch to check (default: main)
204 : * @return the commit id pointed
205 : */
206 : std::string remoteHead(const std::string& remoteDeviceId, const std::string& branch = "main") const;
207 :
208 : /**
209 : * Return the conversation id
210 : */
211 : const std::string& id() const;
212 :
213 : /**
214 : * Add a new commit to the conversation
215 : * @param msg The commit message of the commit
216 : * @param verifyDevice If we need to validate that certificates are correct (used for testing)
217 : * @return <empty> on failure, else the message id
218 : */
219 : std::string commitMessage(const std::string& msg, bool verifyDevice = true);
220 :
221 : std::vector<std::string> commitMessages(const std::vector<std::string>& msgs);
222 :
223 : /**
224 : * Amend a commit message
225 : * @param id The commit to amend
226 : * @param msg The commit message of the commit
227 : * @return <empty> on failure, else the message id
228 : */
229 : std::string amend(const std::string& id, const std::string& msg);
230 :
231 : /**
232 : * Get commits depending on the options we pass
233 : * @return a list of commits
234 : */
235 : std::vector<ConversationCommit> log(const LogOptions& options = {}) const;
236 : void log(PreConditionCb&& preCondition,
237 : std::function<void(ConversationCommit&&)>&& emplaceCb,
238 : PostConditionCb&& postCondition,
239 : const std::string& from = "",
240 : bool logIfNotFound = true) const;
241 :
242 : /**
243 : * Check if a commit exists in the repository
244 : * @param commitId The commit id to check
245 : * @return true if the commit was found, false if not or if an error occurred
246 : */
247 : bool hasCommit(const std::string& commitId) const;
248 : std::optional<ConversationCommit> getCommit(const std::string& commitId) const;
249 :
250 : /**
251 : * Get parent via topological + date sort in branch main of a commit
252 : * @param commitId id to choice
253 : */
254 : std::optional<std::string> linearizedParent(const std::string& commitId) const;
255 :
256 : /**
257 : * Merge another branch into the main branch
258 : * @param merge_id The reference to merge
259 : * @param force Should be false, skip validateDevice() ; used for test purpose
260 : * @return a pair containing if the merge was successful and the merge commit id
261 : * generated if one (can be a fast forward merge without commit)
262 : */
263 : std::pair<bool, std::string> merge(const std::string& merge_id, bool force = false);
264 :
265 : /**
266 : * Get current diff stats between two commits
267 : * @param oldId Old commit
268 : * @param newId Recent commit (empty value will compare to the empty repository)
269 : * @note "HEAD" is also accepted as parameter for newId
270 : * @return diff stats
271 : */
272 : std::string diffStats(const std::string& newId, const std::string& oldId = "") const;
273 :
274 : /**
275 : * Get changed files from a git diff
276 : * @param diffStats The stats to analyze
277 : * @return get the changed files from a git diff
278 : */
279 : static std::vector<std::string> changedFiles(std::string_view diffStats);
280 :
281 : /**
282 : * Join a repository
283 : * @return commit Id
284 : */
285 : std::string join();
286 :
287 : /**
288 : * Erase self from repository
289 : * @return commit Id
290 : */
291 : std::string leave();
292 :
293 : /**
294 : * Erase repository
295 : */
296 : void erase();
297 :
298 : /**
299 : * Get conversation's mode
300 : * @return the mode
301 : */
302 : ConversationMode mode() const;
303 :
304 : /**
305 : * The voting system is divided in two parts. The voting phase where
306 : * admins can decide an action (such as kicking someone)
307 : * and the resolving phase, when > 50% of the admins voted, we can
308 : * considered the vote as finished
309 : */
310 : /**
311 : * Add a vote to kick a device or a user
312 : * @param uri identified of the user/device
313 : * @param type device, members, admins or invited
314 : * @return the commit id or empty if failed
315 : */
316 : std::string voteKick(const std::string& uri, const std::string& type);
317 : /**
318 : * Add a vote to re-add a user
319 : * @param uri identified of the user
320 : * @param type device, members, admins or invited
321 : * @return the commit id or empty if failed
322 : */
323 : std::string voteUnban(const std::string& uri, const std::string_view type);
324 : /**
325 : * Validate if a vote is finished
326 : * @param uri identified of the user/device
327 : * @param type device, members, admins or invited
328 : * @param voteType "ban" or "unban"
329 : * @return the commit id or empty if failed
330 : */
331 : std::string resolveVote(const std::string& uri, const std::string_view type, const std::string& voteType);
332 :
333 : /**
334 : * Validate a fetch with remote device
335 : * @param remotedevice
336 : * @return the validated commits and if an error occurs
337 : */
338 : std::pair<std::vector<ConversationCommit>, bool> validFetch(const std::string& remoteDevice) const;
339 :
340 : /**
341 : * Validate a clone
342 : * @return the validated commits and false if an error occurs
343 : */
344 : std::pair<std::vector<ConversationCommit>, bool> validClone() const;
345 :
346 : /**
347 : * Verify the signature against the given commit
348 : * @param userDevice the email of the sender (i.e. their device's public key)
349 : * @param commitId the id of the commit
350 : */
351 : bool isValidUserAtCommit(const std::string& userDevice,
352 : const std::string& commitId,
353 : const git_buf& sig,
354 : const git_buf& sig_data) const;
355 :
356 : /**
357 : * Validate that commits are not malformed
358 : * @param commitsToValidate the list of commits
359 : */
360 : bool validCommits(const std::vector<ConversationCommit>& commitsToValidate) const;
361 :
362 : /**
363 : * Delete branch with remote
364 : * @param remoteDevice
365 : */
366 : void removeBranchWith(const std::string& remoteDevice);
367 :
368 : /**
369 : * One to one util, get initial members
370 : * @return initial members
371 : */
372 : std::vector<std::string> getInitialMembers() const;
373 :
374 : /**
375 : * Get conversation's members
376 : * @return members
377 : */
378 : std::vector<ConversationMember> members() const;
379 :
380 : /**
381 : * Get conversation's devices
382 : * @param ignoreExpired If we want to ignore expired devices
383 : * @return members
384 : */
385 : std::map<std::string, std::vector<DeviceId>> devices(bool ignoreExpired = true) const;
386 :
387 : /**
388 : * @param filter If we want to remove one member
389 : * @param filteredRoles If we want to ignore some roles
390 : * @return members' uris
391 : */
392 : std::set<std::string> memberUris(std::string_view filter, const std::set<MemberRole>& filteredRoles) const;
393 :
394 : /**
395 : * To use after a merge with member's events, refresh members knowledge
396 : */
397 : void refreshMembers() const;
398 :
399 : void onMembersChanged(OnMembersChanged&& cb);
400 :
401 : /**
402 : * Because conversations can contains non contacts certificates, this methods
403 : * loads certificates in conversations into the cert store
404 : * @param blocking if we need to wait that certificates are pinned
405 : */
406 : void pinCertificates(bool blocking = false);
407 : /**
408 : * Retrieve the uri from a deviceId
409 : * @note used by swarm manager (peersToSyncWith)
410 : * @param deviceId
411 : * @return corresponding issuer
412 : */
413 : std::string uriFromDevice(const std::string& deviceId) const;
414 :
415 : /**
416 : * Change repository's infos
417 : * @param map New infos (supported keys: title, description, avatar)
418 : * @return the commit id
419 : */
420 : std::string updateInfos(const std::map<std::string, std::string>& map);
421 :
422 : /**
423 : * Retrieve current infos (title, description, avatar, mode)
424 : * @return infos
425 : */
426 : std::map<std::string, std::string> infos() const;
427 : static std::map<std::string, std::string> infosFromVCard(vCard::utils::VCardData&& details);
428 :
429 : /**
430 : * Convert ConversationCommit to MapStringString for the client
431 : */
432 : std::vector<std::map<std::string, std::string>> convCommitsToMap(
433 : const std::vector<ConversationCommit>& commits) const;
434 : std::optional<std::map<std::string, std::string>> convCommitToMap(const ConversationCommit& commit) const;
435 :
436 : /**
437 : * Get current HEAD hash
438 : */
439 : std::string getHead() const;
440 :
441 : private:
442 : ConversationRepository() = delete;
443 : class Impl;
444 : std::unique_ptr<Impl> pimpl_;
445 : };
446 :
447 : } // namespace jami
448 13992 : MSGPACK_ADD_ENUM(jami::MemberRole);
449 3258 : MSGPACK_ADD_ENUM(jami::ConversationMode);
|