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 <optional>
20 : #include <git2.h>
21 : #include <memory>
22 : #include <opendht/default_types.h>
23 : #include <string>
24 : #include <vector>
25 :
26 : #include "def.h"
27 :
28 : using GitPackBuilder = std::unique_ptr<git_packbuilder, decltype(&git_packbuilder_free)>;
29 : using GitRepository = std::unique_ptr<git_repository, decltype(&git_repository_free)>;
30 : using GitRevWalker = std::unique_ptr<git_revwalk, decltype(&git_revwalk_free)>;
31 : using GitCommit = std::unique_ptr<git_commit, decltype(&git_commit_free)>;
32 : using GitAnnotatedCommit
33 : = 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 : struct ConversationMember
106 : {
107 : std::string uri;
108 : MemberRole role;
109 :
110 561 : std::map<std::string, std::string> map() const
111 : {
112 561 : std::string rolestr;
113 561 : if (role == MemberRole::ADMIN) {
114 422 : rolestr = "admin";
115 139 : } else if (role == MemberRole::MEMBER) {
116 86 : rolestr = "member";
117 53 : } else if (role == MemberRole::INVITED) {
118 46 : rolestr = "invited";
119 7 : } else if (role == MemberRole::BANNED) {
120 7 : rolestr = "banned";
121 0 : } else if (role == MemberRole::LEFT) {
122 0 : rolestr = "left"; // For one to one
123 : }
124 :
125 2244 : return {{"uri", uri}, {"role", rolestr}};
126 561 : }
127 13952 : MSGPACK_DEFINE(uri, role)
128 : };
129 :
130 : enum class CallbackResult { Skip, Break, Ok };
131 :
132 : using PreConditionCb
133 : = std::function<CallbackResult(const std::string&, const GitAuthor&, const GitCommit&)>;
134 : using PostConditionCb
135 : = std::function<bool(const std::string&, const GitAuthor&, ConversationCommit&)>;
136 : using OnMembersChanged = std::function<void(const std::set<std::string>&)>;
137 :
138 : /**
139 : * This class gives access to the git repository that represents the conversation
140 : */
141 : class LIBJAMI_TESTABLE ConversationRepository
142 : {
143 : public:
144 : #ifdef LIBJAMI_TESTABLE
145 : static bool DISABLE_RESET; // Some tests inject bad files so resetHard() will break the test
146 : #endif
147 : /**
148 : * Creates a new repository, with initial files, where the first commit hash is the conversation id
149 : * @param account The related account
150 : * @param mode The wanted mode
151 : * @param otherMember The other uri
152 : * @return the conversation repository object
153 : */
154 : static LIBJAMI_TESTABLE std::unique_ptr<ConversationRepository> createConversation(
155 : const std::shared_ptr<JamiAccount>& account,
156 : ConversationMode mode = ConversationMode::INVITES_ONLY,
157 : const std::string& otherMember = "");
158 :
159 : /**
160 : * Clones a conversation on a remote device
161 : * @note This will use the socket registered for the conversation with JamiAccount::addGitSocket()
162 : * @param account The account getting the conversation
163 : * @param deviceId Remote device
164 : * @param conversationId Conversation to clone
165 : * @param checkCommitCb Used if commits should be treated
166 : */
167 : static LIBJAMI_TESTABLE std::unique_ptr<ConversationRepository> cloneConversation(
168 : const std::shared_ptr<JamiAccount>& account,
169 : const std::string& deviceId,
170 : const std::string& conversationId,
171 : std::function<void(std::vector<ConversationCommit>)>&& checkCommitCb = {});
172 :
173 : /**
174 : * Open a conversation repository for an account and an id
175 : * @param account The related account
176 : * @param id The conversation id
177 : */
178 : ConversationRepository(const std::shared_ptr<JamiAccount>& account, const std::string& id);
179 : ~ConversationRepository();
180 :
181 : /**
182 : * Write the certificate in /members and commit the change
183 : * @param uri Member to add
184 : * @return the commit id if successful
185 : */
186 : std::string addMember(const std::string& uri);
187 :
188 : /**
189 : * Fetch a remote repository via the given socket
190 : * @note This will use the socket registered for the conversation with JamiAccount::addGitSocket()
191 : * @note will create a remote identified by the deviceId
192 : * @param remoteDeviceId Remote device id to fetch
193 : * @return if the operation was successful
194 : */
195 : bool fetch(const std::string& remoteDeviceId);
196 :
197 : /**
198 : * Retrieve remote head. Can be useful after a fetch operation
199 : * @param remoteDeviceId The remote name
200 : * @param branch Remote branch to check (default: main)
201 : * @return the commit id pointed
202 : */
203 : std::string remoteHead(const std::string& remoteDeviceId,
204 : const std::string& branch = "main") const;
205 :
206 : /**
207 : * Return the conversation id
208 : */
209 : const std::string& id() const;
210 :
211 : /**
212 : * Add a new commit to the conversation
213 : * @param msg The commit message of the commit
214 : * @param verifyDevice If we need to validate that certificates are correct (used for testing)
215 : * @return <empty> on failure, else the message id
216 : */
217 : std::string commitMessage(const std::string& msg, bool verifyDevice = true);
218 :
219 : std::vector<std::string> commitMessages(const std::vector<std::string>& msgs);
220 :
221 : /**
222 : * Amend a commit message
223 : * @param id The commit to amend
224 : * @param msg The commit message of the commit
225 : * @return <empty> on failure, else the message id
226 : */
227 : std::string amend(const std::string& id, const std::string& msg);
228 :
229 : /**
230 : * Get commits depending on the options we pass
231 : * @return a list of commits
232 : */
233 : std::vector<ConversationCommit> log(const LogOptions& options = {}) const;
234 : void log(PreConditionCb&& preCondition,
235 : std::function<void(ConversationCommit&&)>&& emplaceCb,
236 : PostConditionCb&& postCondition,
237 : const std::string& from = "",
238 : bool logIfNotFound = true) const;
239 : std::optional<ConversationCommit> getCommit(const std::string& commitId,
240 : bool logIfNotFound = true) const;
241 :
242 : /**
243 : * Get parent via topological + date sort in branch main of a commit
244 : * @param commitId id to choice
245 : */
246 : std::optional<std::string> linearizedParent(const std::string& commitId) const;
247 :
248 : /**
249 : * Merge another branch into the main branch
250 : * @param merge_id The reference to merge
251 : * @param force Should be false, skip validateDevice() ; used for test purpose
252 : * @return a pair containing if the merge was successful and the merge commit id
253 : * generated if one (can be a fast forward merge without commit)
254 : */
255 : std::pair<bool, std::string> merge(const std::string& merge_id, bool force = false);
256 :
257 : /**
258 : * Get the common parent between two branches
259 : * @param from The first branch
260 : * @param to The second branch
261 : * @return the common parent
262 : */
263 : std::string mergeBase(const std::string& from, const std::string& to) const;
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,
332 : const std::string_view type,
333 : const std::string& voteType);
334 :
335 : /**
336 : * Validate a fetch with remote device
337 : * @param remotedevice
338 : * @return the validated commits and if an error occurs
339 : */
340 : std::pair<std::vector<ConversationCommit>, bool> validFetch(
341 : const std::string& remoteDevice) const;
342 : bool validClone(std::function<void(std::vector<ConversationCommit>)>&& checkCommitCb) const;
343 :
344 : /**
345 : * Delete branch with remote
346 : * @param remoteDevice
347 : */
348 : void removeBranchWith(const std::string& remoteDevice);
349 :
350 : /**
351 : * One to one util, get initial members
352 : * @return initial members
353 : */
354 : std::vector<std::string> getInitialMembers() const;
355 :
356 : /**
357 : * Get conversation's members
358 : * @return members
359 : */
360 : std::vector<ConversationMember> members() const;
361 :
362 : /**
363 : * Get conversation's devices
364 : * @param ignoreExpired If we want to ignore expired devices
365 : * @return members
366 : */
367 : std::map<std::string, std::vector<DeviceId>> devices(bool ignoreExpired = true) const;
368 :
369 : /**
370 : * @param filter If we want to remove one member
371 : * @param filteredRoles If we want to ignore some roles
372 : * @return members' uris
373 : */
374 : std::set<std::string> memberUris(std::string_view filter,
375 : const std::set<MemberRole>& filteredRoles) const;
376 :
377 : /**
378 : * To use after a merge with member's events, refresh members knowledge
379 : */
380 : void refreshMembers() const;
381 :
382 : void onMembersChanged(OnMembersChanged&& cb);
383 :
384 : /**
385 : * Because conversations can contains non contacts certificates, this methods
386 : * loads certificates in conversations into the cert store
387 : * @param blocking if we need to wait that certificates are pinned
388 : */
389 : void pinCertificates(bool blocking = false);
390 : /**
391 : * Retrieve the uri from a deviceId
392 : * @note used by swarm manager (peersToSyncWith)
393 : * @param deviceId
394 : * @return corresponding issuer
395 : */
396 : std::string uriFromDevice(const std::string& deviceId) const;
397 :
398 : /**
399 : * Change repository's infos
400 : * @param map New infos (supported keys: title, description, avatar)
401 : * @return the commit id
402 : */
403 : std::string updateInfos(const std::map<std::string, std::string>& map);
404 :
405 : /**
406 : * Retrieve current infos (title, description, avatar, mode)
407 : * @return infos
408 : */
409 : std::map<std::string, std::string> infos() const;
410 : static std::map<std::string, std::string> infosFromVCard(
411 : std::map<std::string, std::string>&& details);
412 :
413 : /**
414 : * Convert ConversationCommit to MapStringString for the client
415 : */
416 : std::vector<std::map<std::string, std::string>> convCommitsToMap(
417 : const std::vector<ConversationCommit>& commits) const;
418 : std::optional<std::map<std::string, std::string>> convCommitToMap(
419 : const ConversationCommit& commit) const;
420 :
421 : /**
422 : * Get current HEAD hash
423 : */
424 : std::string getHead() const;
425 :
426 : private:
427 : ConversationRepository() = delete;
428 : class Impl;
429 : std::unique_ptr<Impl> pimpl_;
430 : };
431 :
432 : } // namespace jami
433 13952 : MSGPACK_ADD_ENUM(jami::MemberRole);
|