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 :
18 : #pragma once
19 :
20 : #include "json_utils.h"
21 :
22 : #include <optional>
23 : #include <string>
24 :
25 : namespace jami {
26 :
27 : namespace CommitKey {
28 : constexpr const char* const TYPE {"type"};
29 : constexpr const char* const BODY {"body"};
30 : constexpr const char* const REPLY_TO {"reply-to"};
31 : constexpr const char* const REACT_TO {"react-to"};
32 : constexpr const char* const EDIT {"edit"};
33 : constexpr const char* const ACTION {"action"};
34 : constexpr const char* const URI {"uri"};
35 : constexpr const char* const DEVICE {"device"};
36 : constexpr const char* const CONF_ID {"confId"};
37 : constexpr const char* const TO {"to"};
38 : constexpr const char* const REASON {"reason"};
39 : constexpr const char* const DURATION {"duration"};
40 : constexpr const char* const TID {"tid"};
41 : constexpr const char* const DISPLAY_NAME {"displayName"};
42 : constexpr const char* const TOTAL_SIZE {"totalSize"};
43 : constexpr const char* const SHA3SUM {"sha3sum"};
44 : constexpr const char* const MODE {"mode"};
45 : constexpr const char* const INVITED {"invited"};
46 : } // namespace CommitKey
47 :
48 : namespace CommitType {
49 : constexpr const char* const TEXT {"text/plain"};
50 : constexpr const char* const MEMBER {"member"};
51 : constexpr const char* const CALL_HISTORY {"application/call-history+json"};
52 : constexpr const char* const DATA_TRANSFER {"application/data-transfer+json"};
53 : constexpr const char* const INITIAL {"initial"};
54 : constexpr const char* const VOTE {"vote"};
55 : constexpr const char* const UPDATE_PROFILE {"application/update-profile"};
56 : constexpr const char* const MERGE {"merge"};
57 : // Jami no longer creates messages of type "application/edited-message", but we
58 : // still need to be able to parse them for backward compatibility.
59 : constexpr const char* const EDITED_MESSAGE {"application/edited-message"};
60 : } // namespace CommitType
61 :
62 : namespace CommitAction {
63 : constexpr const char* const ADD {"add"};
64 : constexpr const char* const JOIN {"join"};
65 : constexpr const char* const REMOVE {"remove"};
66 : constexpr const char* const BAN {"ban"};
67 : constexpr const char* const UNBAN {"unban"};
68 : } // namespace CommitAction
69 :
70 : enum class ConversationMode : int { ONE_TO_ONE = 0, ADMIN_INVITES_ONLY, INVITES_ONLY, PUBLIC };
71 :
72 : /*
73 : * Jami conversations are stored as git repositories. Most of the information is contained
74 : * in the commit messages. With the exception of merge commits, the commit messages are JSON
75 : * objects with a fixed set of possible fields defined in the CommitKey namespace above. The
76 : * "type" field is mandatory and determines which other fields can be present as well as their
77 : * meaning.
78 : */
79 : struct CommitMessage
80 : {
81 : std::string type {};
82 : std::string body {};
83 : std::string replyTo {};
84 : std::string reactTo {};
85 : std::string editedId {};
86 : std::string action {};
87 : std::string uri {};
88 : std::string confId {};
89 : std::string device {};
90 : std::string duration {};
91 : std::string reason {};
92 : std::string to {};
93 : std::string tid {};
94 : std::string displayName {};
95 : int64_t totalSize {-1};
96 : std::string sha3sum {};
97 : int mode {-1};
98 : std::string invited {};
99 :
100 : // User messages are stored as commits of type "text/plain". The message text is in the "body"
101 : // field. For example:
102 : //
103 : // {"body":"Hello!","type":"text/plain"}
104 : //
105 : // When a user edits a message, the new message text is stored in the "body" field of a commit
106 : // of type "text/plain" (or "application/edited-message" in old versions of Jami), with an
107 : // additional "edit" field containing the ID of the commit being edited. For example:
108 : //
109 : // {"body":"Hello, how are you?","edit":"7de8a42695da4de31f774df7040893d34de0829d","type":"text/plain"}
110 : // {"body":"Hi!","edit":"81528f849e844b6b0ded23b92ed0fc8d06bc21a2","type":"application/edited-message"}
111 : //
112 : // If the same message is edited multiple times, the ID in the "edit" field always refers to
113 : // the original message, not the previous edit.
114 : //
115 : // A deleted message is represented by an edit with an empty "body", e.g.:
116 : //
117 : // {"body":"","edit":"7de8a42695da4de31f774df7040893d34de0829d","type":"text/plain"}
118 : //
119 : // Text messages can optionally include a "reply-to" field with the ID of the message being
120 : // replied to, e.g.:
121 : //
122 : // {"body":"You're right!","reply-to":"200779c99a3f6ed7efc2a83bdfddb6a9e45a4e55","type":"text/plain"}
123 : //
124 : // The "edit" and "reply-to" fields are mutually exclusive. When replying to an edited message,
125 : // the "reply-to" field contains the ID of the original message, not the edit.
126 : //
127 : // Reactions are also encoded as messages of type "text/plain" with a "react-to" field containing
128 : // the ID of the message being reacted to and the reaction itself in the "body" field, e.g.:
129 : //
130 : // {"body":"\ud83d\udc4d","react-to":"d433e037b32314bc3de2fad2ca4a12d914ecad57","type":"text/plain"}
131 : //
132 : // Removing a reaction is done the same way as deleting a message, i.e. as an edit with an
133 : // empty "body":
134 : //
135 : // {"body":"","edit":"9f5a429073f313d3edb149c61fbd682c6e0fc704","type":"text/plain"}
136 : //
137 : // The "react-to" field is mutually exclusive with the "edit" and "reply-to" fields.
138 93 : static CommitMessage text(const std::string& body, const std::string& replyToId = "")
139 : {
140 93 : CommitMessage msg;
141 93 : msg.type = CommitType::TEXT;
142 93 : msg.body = body;
143 93 : msg.replyTo = replyToId;
144 93 : return msg;
145 0 : }
146 3 : static CommitMessage reaction(const std::string& reaction, const std::string& reactToId)
147 : {
148 3 : CommitMessage msg;
149 3 : msg.type = CommitType::TEXT;
150 3 : msg.body = reaction;
151 3 : msg.reactTo = reactToId;
152 3 : return msg;
153 0 : }
154 5 : static CommitMessage edit(const std::string& newBody, const std::string& editedId)
155 : {
156 5 : CommitMessage msg;
157 5 : msg.type = CommitType::TEXT;
158 5 : msg.body = newBody;
159 5 : msg.editedId = editedId;
160 5 : return msg;
161 0 : }
162 :
163 : // Commits of type "member" always have an "action" field and a "uri" field. The "uri" field
164 : // contains the Jami ID of the user impacted by the action, which can be one of the following:
165 : // - "add": the user was invited to join the conversation
166 : // - "join": the user joined the conversation
167 : // - "remove": the user left the conversation
168 : // - "ban": the user was banned from the conversation
169 : // - "unban": the user was unbanned from the conversation
170 : // For example:
171 : //
172 : // {"action":"join","type":"member","uri":"f32701058c69f8ad6a095c6d14650294a4ba39a3"}
173 316 : static CommitMessage member(const std::string& action, const std::string& memberId)
174 : {
175 316 : CommitMessage msg;
176 316 : msg.type = CommitType::MEMBER;
177 316 : msg.action = action;
178 316 : msg.uri = memberId;
179 316 : return msg;
180 0 : }
181 :
182 : // Commits of type "application/call-history+json" represent either the beginning or the end
183 : // of a call. Their format differs depending on whether the call was started in a one-to-one
184 : // conversation or in a group conversation.
185 : //
186 : // In a one-to-one conversation, the user who initiated a call creates a commit once it ends.
187 : // The "duration" field contains the duration of the call in milliseconds, and the "to" field
188 : // contains the Jami ID of the called peer. For example:
189 : //
190 : // {"duration":"80805","to":"ff114e1934db7b79e4f7ac676cb943d97ffb6a32","type":"application/call-history+json"}
191 : //
192 : // If the call failed to start, the "reason" field may provide more information about the cause
193 : // of the failure. For example, the call may have been declined by the peer:
194 : //
195 : // {"duration":"0","reason":"declined","to":"ff114e1934db7b79e4f7ac676cb943d97ffb6a32","type":"application/call-history+json"}
196 : //
197 : // In a group conversation, the host creates a commit when the call starts, and another one
198 : // when it ends. Both commits include the following fields:
199 : // - "confId": a 64-bit unsigned integer identifying the call
200 : // - "device": the host's device ID
201 : // - "uri": the host's Jami ID
202 : // The end call commit additionally includes a "duration" field with the call duration in
203 : // milliseconds. A pair of start/end commits for a group call may look like this:
204 : //
205 : // {"confId":"6342183642926168","device":"c87dc5b688c0e6a7d1cd30fe5c2b4a24aa68d6387ebba9aa7cbb487419578ea1","type":"application/call-history+json","uri":"079ddd3b04f35f6381f2516315e6aa5b98d43ef4"}
206 : // {"confId":"6342183642926168","device":"c87dc5b688c0e6a7d1cd30fe5c2b4a24aa68d6387ebba9aa7cbb487419578ea1","duration":"9142","type":"application/call-history+json","uri":"079ddd3b04f35f6381f2516315e6aa5b98d43ef4"}
207 3 : static CommitMessage outgoingCallEnd(const std::string& peer, uint64_t duration_ms, const std::string& reason = "")
208 : {
209 3 : CommitMessage msg;
210 3 : msg.type = CommitType::CALL_HISTORY;
211 3 : msg.to = peer;
212 3 : msg.duration = std::to_string(duration_ms);
213 3 : msg.reason = reason;
214 3 : return msg;
215 0 : }
216 14 : static CommitMessage conferenceHostingStart(const std::string& confId,
217 : const std::string& device,
218 : const std::string& hostId)
219 : {
220 14 : CommitMessage msg;
221 14 : msg.type = CommitType::CALL_HISTORY;
222 14 : msg.confId = confId;
223 14 : msg.device = device;
224 14 : msg.uri = hostId;
225 14 : return msg;
226 0 : }
227 10 : static CommitMessage conferenceHostingEnd(const std::string& confId,
228 : const std::string& device,
229 : const std::string& hostId,
230 : uint64_t duration_ms)
231 : {
232 10 : CommitMessage msg;
233 10 : msg.type = CommitType::CALL_HISTORY;
234 10 : msg.confId = confId;
235 10 : msg.device = device;
236 10 : msg.uri = hostId;
237 10 : msg.duration = std::to_string(duration_ms);
238 10 : return msg;
239 0 : }
240 :
241 : // When a user sends a file in a conversation, a commit of type "application/data-transfer+json"
242 : // is created with the following fields:
243 : // - "displayName": the file name
244 : // - "sha3sum": the SHA3-512 hash of the file content, encoded as a hexadecimal string
245 : // - "tid": an ID for the file transfer (currently consists of a nonzero 64-bit unsigned integer
246 : // generated randomly by the sender, encoded as a decimal string)
247 : // - "totalSize": the file size in bytes
248 : // For example:
249 : //
250 : // {"displayName":"some_image.png","sha3sum":"5ce2fb16eb16c9dc42f824218ec0b7be4927d9f9fef9860161159faee1c4236a758aeb4ed98b27bf439364ea3199fce23181be4720c79756cf714271b702efcd","tid":"6147910008623250","totalSize":"581","type":"application/data-transfer+json"}
251 : //
252 : // File transfers can optionally include a "reply-to" field with the ID of the message being
253 : // replied to, e.g.:
254 : //
255 : // {"displayName":"aang.jpg","reply-to":"160e330e417401ecdd11094a8dad1355bd734583","sha3sum":"cae439cabde1dd86e15210f1f1486e7bbe0b901e9cb40d087b9673b7827ac5c9b2bf3d5fa3872666d418f205e986e8afa9977ddaba5e4141bdb157fd754656c2","tid":"693561489759880","totalSize":"89040","type":"application/data-transfer+json"}
256 : //
257 : // A deleted file is represented by a commit of type "application/data-transfer+json" with an "edit"
258 : // field containing the ID of the original commit and an empty "tid", e.g.:
259 : //
260 : // {"edit":"7fc3b0cba7e0742b0753051a576a5d17a77a57d0","tid":"","type":"application/data-transfer+json"}
261 14 : static CommitMessage fileSent(const std::string& displayName,
262 : const std::string& sha3sum,
263 : uint64_t tid,
264 : int64_t totalSize,
265 : const std::string& replyToId = "")
266 : {
267 14 : CommitMessage msg;
268 14 : msg.type = CommitType::DATA_TRANSFER;
269 14 : msg.displayName = displayName;
270 14 : msg.sha3sum = sha3sum;
271 14 : msg.replyTo = replyToId;
272 14 : msg.tid = std::to_string(tid);
273 14 : msg.totalSize = totalSize;
274 14 : return msg;
275 0 : }
276 2 : static CommitMessage fileDeleted(const std::string& fileCommitId)
277 : {
278 2 : CommitMessage msg;
279 2 : msg.type = CommitType::DATA_TRANSFER;
280 2 : msg.editedId = fileCommitId;
281 2 : return msg;
282 0 : }
283 :
284 : // Every Jami conversation starts with a commit of type "initial" containing a "mode" field indicating the
285 : // kind of conversation. There are currently two supported values for the mode: 0 (ConversationMode::ONE_TO_ONE)
286 : // and 2 (ConversationMode::INVITES_ONLY). In the case of one-to-one conversations (mode 0), there is an
287 : // additional "invited" field containing the Jami ID of the other participant. For example:
288 : //
289 : // {"invited":"f32701048c59f9ad6a095c6d14650294b4cf30a4","mode":0,"type":"initial"}
290 : // {"mode":2,"type":"initial"}
291 : //
292 : // Jami allows users to create one-to-one conversations with themselves. In that case, the "invited" field is
293 : // still present and contains the user's own Jami ID.
294 203 : static CommitMessage initial(ConversationMode mode, const std::string& invitedId = "")
295 : {
296 203 : CommitMessage msg;
297 203 : msg.type = CommitType::INITIAL;
298 203 : msg.mode = static_cast<int>(mode);
299 203 : if (mode == ConversationMode::ONE_TO_ONE) {
300 69 : msg.invited = invitedId;
301 : }
302 203 : return msg;
303 0 : }
304 :
305 : // Commits of type "vote" are created by admins when voting to ban or unban a user from a
306 : // conversation. They have a "uri" field with the Jami ID of the user being voted on, e.g.:
307 : //
308 : // {"type":"vote","uri":"f32701048c59f9ad6a095c6d14650294b4cf30a4"}
309 13 : static CommitMessage vote(const std::string& userId)
310 : {
311 13 : CommitMessage msg;
312 13 : msg.type = CommitType::VOTE;
313 13 : msg.uri = userId;
314 13 : return msg;
315 0 : }
316 :
317 : // Commits that modify a conversation's profile (which is stored in the 'profile.vcf' file at
318 : // the root of the conversation repository) are of type "application/update-profile". They
319 : // do not contain any additional fields:
320 : //
321 : // {"type":"application/update-profile"}
322 10 : static CommitMessage updateProfile()
323 : {
324 10 : CommitMessage msg;
325 10 : msg.type = CommitType::UPDATE_PROFILE;
326 10 : return msg;
327 0 : }
328 :
329 : Json::Value toJson() const;
330 : std::string toString() const;
331 : static std::optional<CommitMessage> fromString(const std::string& str);
332 : };
333 :
334 : } // namespace jami
|