LCOV - code coverage report
Current view: top level - src/jamidht - commit_message.h (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 86.6 % 119 103
Test Date: 2026-11-09 10:41:14 Functions: 100.0 % 16 16

            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              : constexpr const char* const MIME_TYPE {"mimeType"};
      47              : constexpr const char* const PARENT {"parent"};
      48              : } // namespace CommitKey
      49              : 
      50              : namespace CommitType {
      51              : constexpr const char* const TEXT {"text/plain"};
      52              : constexpr const char* const MEMBER {"member"};
      53              : constexpr const char* const CALL_HISTORY {"application/call-history+json"};
      54              : constexpr const char* const DATA_TRANSFER {"application/data-transfer+json"};
      55              : constexpr const char* const INITIAL {"initial"};
      56              : constexpr const char* const VOTE {"vote"};
      57              : constexpr const char* const UPDATE_PROFILE {"application/update-profile"};
      58              : constexpr const char* const MERGE {"merge"};
      59              : // Announces a collaborative document. The document's content does not live in
      60              : // the conversation: it has its own repository, synchronized separately. This
      61              : // commit only makes the document appear in the history and binds its id to this
      62              : // conversation.
      63              : constexpr const char* const COLLAB_DOC {"application/collab-doc+json"};
      64              : // Batch of CRDT updates in a collaborative document repository. The updates
      65              : // travel in the commit message itself (base64, one per line in the "body"
      66              : // field); the tree is carried over untouched, so a checkpoint costs one
      67              : // commit object and nothing else. The exception is a checkpoint adding an
      68              : // attachment: the file enters the tree and the "body" is empty. Only valid
      69              : // in document repositories.
      70              : constexpr const char* const CHECKPOINT {"application/checkpoint"};
      71              : // Jami no longer creates messages of type "application/edited-message", but we
      72              : // still need to be able to parse them for backward compatibility.
      73              : constexpr const char* const EDITED_MESSAGE {"application/edited-message"};
      74              : } // namespace CommitType
      75              : 
      76              : namespace CommitAction {
      77              : constexpr const char* const ADD {"add"};
      78              : constexpr const char* const JOIN {"join"};
      79              : constexpr const char* const REMOVE {"remove"};
      80              : constexpr const char* const BAN {"ban"};
      81              : constexpr const char* const UNBAN {"unban"};
      82              : } // namespace CommitAction
      83              : 
      84              : // Values are persisted in initial commits; keep them stable.
      85              : enum class ConversationMode : int {
      86              :     ONE_TO_ONE = 0,
      87              :     ADMIN_INVITES_ONLY = 1,
      88              :     INVITES_ONLY = 2,
      89              :     PUBLIC = 3,
      90              :     DOCUMENT = 4,
      91              :     FEED = 5
      92              : };
      93              : 
      94              : /*
      95              :  * Jami conversations are stored as git repositories. Most of the information is contained
      96              :  * in the commit messages. With the exception of merge commits, the commit messages are JSON
      97              :  * objects with a fixed set of possible fields defined in the CommitKey namespace above. The
      98              :  * "type" field is mandatory and determines which other fields can be present as well as their
      99              :  * meaning.
     100              :  */
     101              : struct CommitMessage
     102              : {
     103              :     std::string type {};
     104              :     std::string body {};
     105              :     std::string replyTo {};
     106              :     std::string reactTo {};
     107              :     std::string editedId {};
     108              :     std::string action {};
     109              :     std::string uri {};
     110              :     std::string confId {};
     111              :     std::string device {};
     112              :     std::string duration {};
     113              :     std::string reason {};
     114              :     std::string to {};
     115              :     std::string tid {};
     116              :     std::string displayName {};
     117              :     int64_t totalSize {-1};
     118              :     std::string sha3sum {};
     119              :     int mode {-1};
     120              :     std::string invited {};
     121              :     std::string mimeType {};
     122              :     std::string parent {};
     123              : 
     124              :     // User messages are stored as commits of type "text/plain". The message text is in the "body"
     125              :     // field. For example:
     126              :     //
     127              :     //     {"body":"Hello!","type":"text/plain"}
     128              :     //
     129              :     // When a user edits a message, the new message text is stored in the "body" field of a commit
     130              :     // of type "text/plain" (or "application/edited-message" in old versions of Jami), with an
     131              :     // additional "edit" field containing the ID of the commit being edited. For example:
     132              :     //
     133              :     //     {"body":"Hello, how are you?","edit":"7de8a42695da4de31f774df7040893d34de0829d","type":"text/plain"}
     134              :     //     {"body":"Hi!","edit":"81528f849e844b6b0ded23b92ed0fc8d06bc21a2","type":"application/edited-message"}
     135              :     //
     136              :     // If the same message is edited multiple times, the ID in the "edit" field always refers to
     137              :     // the original message, not the previous edit.
     138              :     //
     139              :     // A deleted message is represented by an edit with an empty "body", e.g.:
     140              :     //
     141              :     //     {"body":"","edit":"7de8a42695da4de31f774df7040893d34de0829d","type":"text/plain"}
     142              :     //
     143              :     // Text messages can optionally include a "reply-to" field with the ID of the message being
     144              :     // replied to, e.g.:
     145              :     //
     146              :     //     {"body":"You're right!","reply-to":"200779c99a3f6ed7efc2a83bdfddb6a9e45a4e55","type":"text/plain"}
     147              :     //
     148              :     // The "edit" and "reply-to" fields are mutually exclusive. When replying to an edited message,
     149              :     // the "reply-to" field contains the ID of the original message, not the edit.
     150              :     //
     151              :     // Reactions are also encoded as messages of type "text/plain" with a "react-to" field containing
     152              :     // the ID of the message being reacted to and the reaction itself in the "body" field, e.g.:
     153              :     //
     154              :     //     {"body":"\ud83d\udc4d","react-to":"d433e037b32314bc3de2fad2ca4a12d914ecad57","type":"text/plain"}
     155              :     //
     156              :     // Removing a reaction is done the same way as deleting a message, i.e. as an edit with an
     157              :     // empty "body":
     158              :     //
     159              :     //     {"body":"","edit":"9f5a429073f313d3edb149c61fbd682c6e0fc704","type":"text/plain"}
     160              :     //
     161              :     // The "react-to" field is mutually exclusive with the "edit" and "reply-to" fields.
     162          306 :     static CommitMessage text(const std::string& body, const std::string& replyToId = "")
     163              :     {
     164          306 :         CommitMessage msg;
     165          306 :         msg.type = CommitType::TEXT;
     166          306 :         msg.body = body;
     167          306 :         msg.replyTo = replyToId;
     168          306 :         return msg;
     169            0 :     }
     170          103 :     static CommitMessage reaction(const std::string& reaction, const std::string& reactToId)
     171              :     {
     172          103 :         CommitMessage msg;
     173          103 :         msg.type = CommitType::TEXT;
     174          103 :         msg.body = reaction;
     175          103 :         msg.reactTo = reactToId;
     176          103 :         return msg;
     177            0 :     }
     178          207 :     static CommitMessage edit(const std::string& newBody, const std::string& editedId)
     179              :     {
     180          207 :         CommitMessage msg;
     181          207 :         msg.type = CommitType::TEXT;
     182          207 :         msg.body = newBody;
     183          207 :         msg.editedId = editedId;
     184          207 :         return msg;
     185            0 :     }
     186              : 
     187              :     // Commits of type "member" always have an "action" field and a "uri" field. The "uri" field
     188              :     // contains the Jami ID of the user impacted by the action, which can be one of the following:
     189              :     // - "add": the user was invited to join the conversation
     190              :     // - "join": the user joined the conversation
     191              :     // - "remove": the user left the conversation
     192              :     // - "ban": the user was banned from the conversation
     193              :     // - "unban": the user was unbanned from the conversation
     194              :     // For example:
     195              :     //
     196              :     //     {"action":"join","type":"member","uri":"f32701058c69f8ad6a095c6d14650294a4ba39a3"}
     197         1123 :     static CommitMessage member(const std::string& action, const std::string& memberId)
     198              :     {
     199         1123 :         CommitMessage msg;
     200         1123 :         msg.type = CommitType::MEMBER;
     201         1123 :         msg.action = action;
     202         1123 :         msg.uri = memberId;
     203         1123 :         return msg;
     204            0 :     }
     205              : 
     206              :     // Commits of type "application/call-history+json" represent either the beginning or the end
     207              :     // of a call. Their format differs depending on whether the call was started in a one-to-one
     208              :     // conversation or in a group conversation.
     209              :     //
     210              :     // In a one-to-one conversation, the user who initiated a call creates a commit once it ends.
     211              :     // The "duration" field contains the duration of the call in milliseconds, and the "to" field
     212              :     // contains the Jami ID of the called peer. For example:
     213              :     //
     214              :     //      {"duration":"80805","to":"ff114e1934db7b79e4f7ac676cb943d97ffb6a32","type":"application/call-history+json"}
     215              :     //
     216              :     // If the call failed to start, the "reason" field may provide more information about the cause
     217              :     // of the failure. For example, the call may have been declined by the peer:
     218              :     //
     219              :     //     {"duration":"0","reason":"declined","to":"ff114e1934db7b79e4f7ac676cb943d97ffb6a32","type":"application/call-history+json"}
     220              :     //
     221              :     // In a group conversation, the host creates a commit when the call starts, and another one
     222              :     // when it ends. Both commits include the following fields:
     223              :     // - "confId": a 64-bit unsigned integer identifying the call
     224              :     // - "device": the host's device ID
     225              :     // - "uri": the host's Jami ID
     226              :     // The end call commit additionally includes a "duration" field with the call duration in
     227              :     // milliseconds. A pair of start/end commits for a group call may look like this:
     228              :     //
     229              :     //     {"confId":"6342183642926168","device":"c87dc5b688c0e6a7d1cd30fe5c2b4a24aa68d6387ebba9aa7cbb487419578ea1","type":"application/call-history+json","uri":"079ddd3b04f35f6381f2516315e6aa5b98d43ef4"}
     230              :     //     {"confId":"6342183642926168","device":"c87dc5b688c0e6a7d1cd30fe5c2b4a24aa68d6387ebba9aa7cbb487419578ea1","duration":"9142","type":"application/call-history+json","uri":"079ddd3b04f35f6381f2516315e6aa5b98d43ef4"}
     231          205 :     static CommitMessage outgoingCallEnd(const std::string& peer, uint64_t duration_ms, const std::string& reason = "")
     232              :     {
     233          205 :         CommitMessage msg;
     234          205 :         msg.type = CommitType::CALL_HISTORY;
     235          205 :         msg.to = peer;
     236          205 :         msg.duration = std::to_string(duration_ms);
     237          205 :         msg.reason = reason;
     238          205 :         return msg;
     239            0 :     }
     240          114 :     static CommitMessage conferenceHostingStart(const std::string& confId,
     241              :                                                 const std::string& device,
     242              :                                                 const std::string& hostId)
     243              :     {
     244          114 :         CommitMessage msg;
     245          114 :         msg.type = CommitType::CALL_HISTORY;
     246          114 :         msg.confId = confId;
     247          114 :         msg.device = device;
     248          114 :         msg.uri = hostId;
     249          114 :         return msg;
     250            0 :     }
     251          110 :     static CommitMessage conferenceHostingEnd(const std::string& confId,
     252              :                                               const std::string& device,
     253              :                                               const std::string& hostId,
     254              :                                               uint64_t duration_ms)
     255              :     {
     256          110 :         CommitMessage msg;
     257          110 :         msg.type = CommitType::CALL_HISTORY;
     258          110 :         msg.confId = confId;
     259          110 :         msg.device = device;
     260          110 :         msg.uri = hostId;
     261          110 :         msg.duration = std::to_string(duration_ms);
     262          110 :         return msg;
     263            0 :     }
     264              : 
     265              :     // When a user sends a file in a conversation, a commit of type "application/data-transfer+json"
     266              :     // is created with the following fields:
     267              :     // - "displayName": the file name
     268              :     // - "sha3sum": the SHA3-512 hash of the file content, encoded as a hexadecimal string
     269              :     // - "tid": an ID for the file transfer (currently consists of a nonzero 64-bit unsigned integer
     270              :     //   generated randomly by the sender, encoded as a decimal string)
     271              :     // - "totalSize": the file size in bytes
     272              :     // For example:
     273              :     //
     274              :     //     {"displayName":"some_image.png","sha3sum":"5ce2fb16eb16c9dc42f824218ec0b7be4927d9f9fef9860161159faee1c4236a758aeb4ed98b27bf439364ea3199fce23181be4720c79756cf714271b702efcd","tid":"6147910008623250","totalSize":"581","type":"application/data-transfer+json"}
     275              :     //
     276              :     // File transfers can optionally include a "reply-to" field with the ID of the message being
     277              :     // replied to, e.g.:
     278              :     //
     279              :     //     {"displayName":"aang.jpg","reply-to":"160e330e417401ecdd11094a8dad1355bd734583","sha3sum":"cae439cabde1dd86e15210f1f1486e7bbe0b901e9cb40d087b9673b7827ac5c9b2bf3d5fa3872666d418f205e986e8afa9977ddaba5e4141bdb157fd754656c2","tid":"693561489759880","totalSize":"89040","type":"application/data-transfer+json"}
     280              :     //
     281              :     // A deleted file is represented by a commit of type "application/data-transfer+json" with an "edit"
     282              :     // field containing the ID of the original commit and an empty "tid", e.g.:
     283              :     //
     284              :     //     {"edit":"7fc3b0cba7e0742b0753051a576a5d17a77a57d0","tid":"","type":"application/data-transfer+json"}
     285          219 :     static CommitMessage fileSent(const std::string& displayName,
     286              :                                   const std::string& sha3sum,
     287              :                                   uint64_t tid,
     288              :                                   int64_t totalSize,
     289              :                                   const std::string& replyToId = "")
     290              :     {
     291          219 :         CommitMessage msg;
     292          219 :         msg.type = CommitType::DATA_TRANSFER;
     293          219 :         msg.displayName = displayName;
     294          219 :         msg.sha3sum = sha3sum;
     295          219 :         msg.replyTo = replyToId;
     296          219 :         msg.tid = std::to_string(tid);
     297          219 :         msg.totalSize = totalSize;
     298          219 :         return msg;
     299            0 :     }
     300          104 :     static CommitMessage fileDeleted(const std::string& fileCommitId)
     301              :     {
     302          104 :         CommitMessage msg;
     303          104 :         msg.type = CommitType::DATA_TRANSFER;
     304          104 :         msg.editedId = fileCommitId;
     305          104 :         return msg;
     306            0 :     }
     307              : 
     308              :     // A collaborative document is announced with a commit of type "application/collab-doc+json"
     309              :     // carrying its initial name in "displayName" and the media type of what it holds in "mimeType".
     310              :     // The "uri" field contains the ID of the document's repository. For example:
     311              :     //
     312              :     //     {"displayName":"Notes","mimeType":"text/html","type":"application/collab-doc+json","uri":"332ba97d150dbad022c7d780af61d28ed25bf0a9"}
     313              :     //
     314              :     // A deleted document is represented by a commit of type "application/collab-doc+json" with an "edit"
     315              :     // field containing the ID of the original commit, e.g.:
     316              :     //
     317              :     //     {"edit":"8a3828b5d0450f69988d84baaf0ded8b4614cd14","type":"application/collab-doc+json"}
     318          125 :     static CommitMessage collabDocCreated(const std::string& documentId,
     319              :                                           const std::string& name,
     320              :                                           const std::string& mimeType)
     321              :     {
     322          125 :         CommitMessage msg;
     323          125 :         msg.type = CommitType::COLLAB_DOC;
     324          125 :         msg.uri = documentId;
     325          125 :         msg.displayName = name;
     326          125 :         msg.mimeType = mimeType;
     327          125 :         return msg;
     328            0 :     }
     329          102 :     static CommitMessage collabDocRemoved(const std::string& announcementCommitId)
     330              :     {
     331          102 :         CommitMessage msg;
     332          102 :         msg.type = CommitType::COLLAB_DOC;
     333          102 :         msg.editedId = announcementCommitId;
     334          102 :         return msg;
     335            0 :     }
     336              : 
     337              :     // Every Jami conversation starts with a commit of type "initial" containing a "mode" field indicating the
     338              :     // kind of conversation. In the case of one-to-one conversations (mode 0), there is an additional "invited"
     339              :     // field containing the Jami ID of the other participant. For example:
     340              :     //
     341              :     //     {"invited":"f32701048c59f9ad6a095c6d14650294b4cf30a4","mode":0,"type":"initial"}
     342              :     //     {"mode":2,"type":"initial"}
     343              :     //
     344              :     // Jami allows users to create one-to-one conversations with themselves. In that case, the "invited" field is
     345              :     // still present and contains the user's own Jami ID.
     346          564 :     static CommitMessage initial(ConversationMode mode, const std::string& invitedId = "")
     347              :     {
     348          564 :         CommitMessage msg;
     349          564 :         msg.type = CommitType::INITIAL;
     350          564 :         msg.mode = static_cast<int>(mode);
     351          564 :         if (mode == ConversationMode::ONE_TO_ONE) {
     352          377 :             msg.invited = invitedId;
     353              :         }
     354          564 :         return msg;
     355            0 :     }
     356              : 
     357              :     // A collaborative document repository is a swarm exactly like a conversation, distinguished
     358              :     // by its mode: 4 (ConversationMode::DOCUMENT). Its initial commit additionally records the
     359              :     // ID of the conversation the document was announced in ("parent") and the media type of
     360              :     // what the document holds ("mimeType"), e.g.:
     361              :     //
     362              :     //     {"mimeType":"text/html","mode":4,"parent":"f32701048c59f9ad6a095c6d14650294b4cf30a4","type":"initial"}
     363          133 :     static CommitMessage initialDocument(const std::string& parentConversationId, const std::string& mimeType)
     364              :     {
     365          133 :         CommitMessage msg;
     366          133 :         msg.type = CommitType::INITIAL;
     367          133 :         msg.mode = static_cast<int>(ConversationMode::DOCUMENT);
     368          133 :         msg.parent = parentConversationId;
     369          133 :         msg.mimeType = mimeType;
     370          133 :         return msg;
     371            0 :     }
     372              : 
     373              :     // A checkpoint persists a batch of CRDT updates in a document repository. The updates are
     374              :     // base64-encoded, one per line, in the "body" field:
     375              :     //
     376              :     //     {"body":"AQHZ...\nAQLa...","type":"application/checkpoint"}
     377              :     //
     378              :     // A checkpoint that adds an attachment (an embedded file, stored in the repository tree
     379              :     // rather than in the message) carries no update, so its "body" is empty:
     380              :     //
     381              :     //     {"body":"","type":"application/checkpoint"}
     382          122 :     static CommitMessage checkpoint(const std::vector<std::string>& base64Updates)
     383              :     {
     384          122 :         CommitMessage msg;
     385          122 :         msg.type = CommitType::CHECKPOINT;
     386          453 :         for (const auto& u : base64Updates) {
     387          331 :             if (!msg.body.empty())
     388          214 :                 msg.body += '\n';
     389          331 :             msg.body += u;
     390              :         }
     391          122 :         return msg;
     392            0 :     }
     393              : 
     394              :     // Commits of type "vote" are created by admins when voting to ban or unban a user from a
     395              :     // conversation. They have a "uri" field with the Jami ID of the user being voted on, e.g.:
     396              :     //
     397              :     //     {"type":"vote","uri":"f32701048c59f9ad6a095c6d14650294b4cf30a4"}
     398          117 :     static CommitMessage vote(const std::string& userId)
     399              :     {
     400          117 :         CommitMessage msg;
     401          117 :         msg.type = CommitType::VOTE;
     402          117 :         msg.uri = userId;
     403          117 :         return msg;
     404            0 :     }
     405              : 
     406              :     // Commits that modify a conversation's profile (which is stored in the 'profile.vcf' file at
     407              :     // the root of the conversation repository) are of type "application/update-profile". They
     408              :     // do not contain any additional fields:
     409              :     //
     410              :     //      {"type":"application/update-profile"}
     411           37 :     static CommitMessage updateProfile()
     412              :     {
     413           37 :         CommitMessage msg;
     414           37 :         msg.type = CommitType::UPDATE_PROFILE;
     415           37 :         return msg;
     416            0 :     }
     417              : 
     418              :     Json::Value toJson() const;
     419              :     std::string toString() const;
     420              :     static std::optional<CommitMessage> fromString(const std::string& str);
     421              : };
     422              : 
     423              : } // namespace jami
        

Generated by: LCOV version 2.0-1