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