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 710 : std::map<std::string, std::string> map() const
105 : {
106 710 : std::string rolestr;
107 710 : if (role == MemberRole::ADMIN) {
108 427 : rolestr = "admin";
109 283 : } else if (role == MemberRole::MEMBER) {
110 234 : rolestr = "member";
111 49 : } else if (role == MemberRole::INVITED) {
112 42 : rolestr = "invited";
113 7 : } else if (role == MemberRole::BANNED) {
114 7 : rolestr = "banned";
115 0 : } else if (role == MemberRole::LEFT) {
116 0 : rolestr = "left"; // For one to one
117 : }
118 :
119 : return {
120 710 : {"uri", uri },
121 : {"role", rolestr}
122 2840 : };
123 710 : }
124 13993 : MSGPACK_DEFINE(uri, role)
125 : };
126 :
127 : enum class CallbackResult { Skip, Break, Ok };
128 :
129 : using PreConditionCb = std::function<CallbackResult(const std::string&, const GitAuthor&, const GitCommit&)>;
130 : using PostConditionCb = std::function<bool(const std::string&, const GitAuthor&, ConversationCommit&)>;
131 : using OnMembersChanged = std::function<void(const std::set<std::string>&)>;
132 :
133 : /**
134 : * This class gives access to the git repository that represents the conversation
135 : */
136 : class LIBJAMI_TEST_EXPORT ConversationRepository
137 : {
138 : public:
139 : #ifdef LIBJAMI_TEST
140 : static bool DISABLE_RESET; // Some tests inject bad files so resetHard() will break the test
141 : #endif
142 : /**
143 : * Creates a new repository, with initial files, where the first commit hash is the conversation id
144 : * @param account The related account
145 : * @param mode The wanted mode
146 : * @param otherMember The other uri
147 : * @return the conversation repository object
148 : */
149 : static LIBJAMI_TEST_EXPORT std::unique_ptr<ConversationRepository> createConversation(
150 : const std::shared_ptr<JamiAccount>& account,
151 : ConversationMode mode = ConversationMode::INVITES_ONLY,
152 : const std::string& otherMember = "");
153 :
154 : /**
155 : * Clones a conversation on a remote device
156 : * @note This will use the socket registered for the conversation with JamiAccount::addGitSocket()
157 : * @param account The account getting the conversation
158 : * @param deviceId Remote device
159 : * @param conversationId Conversation to clone
160 : */
161 : static LIBJAMI_TEST_EXPORT std::pair<std::unique_ptr<ConversationRepository>, std::vector<ConversationCommit>>
162 : cloneConversation(const std::shared_ptr<JamiAccount>& account,
163 : const std::string& deviceId,
164 : const std::string& conversationId);
165 :
166 : /**
167 : * Open a conversation repository for an account and an id
168 : * @param account The related account
169 : * @param id The conversation id
170 : */
171 : ConversationRepository(const std::shared_ptr<JamiAccount>& account, const std::string& id);
172 : ~ConversationRepository();
173 :
174 : /**
175 : * Write the certificate in /members and commit the change
176 : * @param uri Member to add
177 : * @return the commit id if successful
178 : */
179 : std::string addMember(const std::string& uri);
180 :
181 : /**
182 : * Fetch a remote repository via the given socket
183 : * @note This will use the socket registered for the conversation with JamiAccount::addGitSocket()
184 : * @note will create a remote identified by the deviceId
185 : * @param remoteDeviceId Remote device id to fetch
186 : * @return if the operation was successful
187 : */
188 : bool fetch(const std::string& remoteDeviceId);
189 :
190 : /**
191 : * Merge the history of the conversation with another peer
192 : * @param uri The peer uri
193 : * @param disconnectFromPeerCb Callback to disconnect from peer when banning
194 : * @return A vector of media maps representing the merged history
195 : */
196 : std::vector<std::map<std::string, std::string>> mergeHistory(
197 : const std::string& uri, std::function<void(const std::string&)>&& disconnectFromPeerCb = {});
198 :
199 : /**
200 : * Retrieve remote head. Can be useful after a fetch operation
201 : * @param remoteDeviceId The remote name
202 : * @param branch Remote branch to check (default: main)
203 : * @return the commit id pointed
204 : */
205 : std::string remoteHead(const std::string& remoteDeviceId, const std::string& branch = "main") const;
206 :
207 : /**
208 : * Return the conversation id
209 : */
210 : const std::string& id() const;
211 :
212 : /**
213 : * Add a new commit to the conversation
214 : * @param msg The commit message of the commit
215 : * @param verifyDevice If we need to validate that certificates are correct (used for testing)
216 : * @return <empty> on failure, else the message id
217 : */
218 : std::string commitMessage(const std::string& msg, bool verifyDevice = true);
219 :
220 : std::vector<std::string> commitMessages(const std::vector<std::string>& msgs);
221 :
222 : /**
223 : * Amend a commit message
224 : * @param id The commit to amend
225 : * @param msg The commit message of the commit
226 : * @return <empty> on failure, else the message id
227 : */
228 : std::string amend(const std::string& id, const std::string& msg);
229 :
230 : /**
231 : * Get commits depending on the options we pass
232 : * @return a list of commits
233 : */
234 : std::vector<ConversationCommit> log(const LogOptions& options = {}) const;
235 : void log(PreConditionCb&& preCondition,
236 : std::function<void(ConversationCommit&&)>&& emplaceCb,
237 : PostConditionCb&& postCondition,
238 : const std::string& from = "",
239 : bool logIfNotFound = true) const;
240 : std::optional<ConversationCommit> getCommit(const std::string& commitId, 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 current diff stats between two commits
259 : * @param oldId Old commit
260 : * @param newId Recent commit (empty value will compare to the empty repository)
261 : * @note "HEAD" is also accepted as parameter for newId
262 : * @return diff stats
263 : */
264 : std::string diffStats(const std::string& newId, const std::string& oldId = "") const;
265 :
266 : /**
267 : * Get changed files from a git diff
268 : * @param diffStats The stats to analyze
269 : * @return get the changed files from a git diff
270 : */
271 : static std::vector<std::string> changedFiles(std::string_view diffStats);
272 :
273 : /**
274 : * Join a repository
275 : * @return commit Id
276 : */
277 : std::string join();
278 :
279 : /**
280 : * Erase self from repository
281 : * @return commit Id
282 : */
283 : std::string leave();
284 :
285 : /**
286 : * Erase repository
287 : */
288 : void erase();
289 :
290 : /**
291 : * Get conversation's mode
292 : * @return the mode
293 : */
294 : ConversationMode mode() const;
295 :
296 : /**
297 : * The voting system is divided in two parts. The voting phase where
298 : * admins can decide an action (such as kicking someone)
299 : * and the resolving phase, when > 50% of the admins voted, we can
300 : * considered the vote as finished
301 : */
302 : /**
303 : * Add a vote to kick a device or a user
304 : * @param uri identified of the user/device
305 : * @param type device, members, admins or invited
306 : * @return the commit id or empty if failed
307 : */
308 : std::string voteKick(const std::string& uri, const std::string& type);
309 : /**
310 : * Add a vote to re-add a user
311 : * @param uri identified of the user
312 : * @param type device, members, admins or invited
313 : * @return the commit id or empty if failed
314 : */
315 : std::string voteUnban(const std::string& uri, const std::string_view type);
316 : /**
317 : * Validate if a vote is finished
318 : * @param uri identified of the user/device
319 : * @param type device, members, admins or invited
320 : * @param voteType "ban" or "unban"
321 : * @return the commit id or empty if failed
322 : */
323 : std::string resolveVote(const std::string& uri, const std::string_view type, const std::string& voteType);
324 :
325 : /**
326 : * Validate a fetch with remote device
327 : * @param remotedevice
328 : * @return the validated commits and if an error occurs
329 : */
330 : std::pair<std::vector<ConversationCommit>, bool> validFetch(const std::string& remoteDevice) const;
331 :
332 : /**
333 : * Validate a clone
334 : * @return the validated commits and false if an error occurs
335 : */
336 : std::pair<std::vector<ConversationCommit>, bool> validClone() const;
337 :
338 : /**
339 : * Verify the signature against the given commit
340 : * @param userDevice the email of the sender (i.e. their device's public key)
341 : * @param commitId the id of the commit
342 : */
343 : bool isValidUserAtCommit(const std::string& userDevice,
344 : const std::string& commitId,
345 : const git_buf& sig,
346 : const git_buf& sig_data) const;
347 :
348 : /**
349 : * Validate that commits are not malformed
350 : * @param commitsToValidate the list of commits
351 : */
352 : bool validCommits(const std::vector<ConversationCommit>& commitsToValidate) const;
353 :
354 : /**
355 : * Delete branch with remote
356 : * @param remoteDevice
357 : */
358 : void removeBranchWith(const std::string& remoteDevice);
359 :
360 : /**
361 : * One to one util, get initial members
362 : * @return initial members
363 : */
364 : std::vector<std::string> getInitialMembers() const;
365 :
366 : /**
367 : * Get conversation's members
368 : * @return members
369 : */
370 : std::vector<ConversationMember> members() const;
371 :
372 : /**
373 : * Get conversation's devices
374 : * @param ignoreExpired If we want to ignore expired devices
375 : * @return members
376 : */
377 : std::map<std::string, std::vector<DeviceId>> devices(bool ignoreExpired = true) const;
378 :
379 : /**
380 : * @param filter If we want to remove one member
381 : * @param filteredRoles If we want to ignore some roles
382 : * @return members' uris
383 : */
384 : std::set<std::string> memberUris(std::string_view filter, const std::set<MemberRole>& filteredRoles) const;
385 :
386 : /**
387 : * To use after a merge with member's events, refresh members knowledge
388 : */
389 : void refreshMembers() const;
390 :
391 : void onMembersChanged(OnMembersChanged&& cb);
392 :
393 : /**
394 : * Because conversations can contains non contacts certificates, this methods
395 : * loads certificates in conversations into the cert store
396 : * @param blocking if we need to wait that certificates are pinned
397 : */
398 : void pinCertificates(bool blocking = false);
399 : /**
400 : * Retrieve the uri from a deviceId
401 : * @note used by swarm manager (peersToSyncWith)
402 : * @param deviceId
403 : * @return corresponding issuer
404 : */
405 : std::string uriFromDevice(const std::string& deviceId) const;
406 :
407 : /**
408 : * Change repository's infos
409 : * @param map New infos (supported keys: title, description, avatar)
410 : * @return the commit id
411 : */
412 : std::string updateInfos(const std::map<std::string, std::string>& map);
413 :
414 : /**
415 : * Retrieve current infos (title, description, avatar, mode)
416 : * @return infos
417 : */
418 : std::map<std::string, std::string> infos() const;
419 : static std::map<std::string, std::string> infosFromVCard(vCard::utils::VCardData&& details);
420 :
421 : /**
422 : * Convert ConversationCommit to MapStringString for the client
423 : */
424 : std::vector<std::map<std::string, std::string>> convCommitsToMap(
425 : const std::vector<ConversationCommit>& commits) const;
426 : std::optional<std::map<std::string, std::string>> convCommitToMap(const ConversationCommit& commit) const;
427 :
428 : /**
429 : * Get current HEAD hash
430 : */
431 : std::string getHead() const;
432 :
433 : private:
434 : ConversationRepository() = delete;
435 : class Impl;
436 : std::unique_ptr<Impl> pimpl_;
437 : };
438 :
439 : } // namespace jami
440 13992 : MSGPACK_ADD_ENUM(jami::MemberRole);
441 3264 : MSGPACK_ADD_ENUM(jami::ConversationMode);
|