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

Generated by: LCOV version 2.0-1