Matrix protocol in OCaml, Eio specialised
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

wip matrix

+17694
+2
.gitignore
··· 1 + _build/ 2 + *.install
+161
STATUS.md
··· 1 + # ocaml-matrix 2 + 3 + **Status: INCOMPLETE** 4 + 5 + ## Overview 6 + OCaml Matrix SDK providing protocol types and a full HTTP client for the Matrix chat protocol. Uses `requests` for HTTP and `jsont` for JSON encoding/decoding. Also provides an Eio-idiomatic wrapper for structured concurrency. 7 + 8 + ## Current State 9 + 10 + ### matrix_proto Library 11 + Complete protocol types with jsont codecs and roundtrip tests: 12 + - `Matrix_id` - User, Room, Event, Device, and Server identifiers with validation 13 + - `Matrix_event` - Room state and message events with content types 14 + - `Matrix_sync` - Sync API response types 15 + 16 + ### matrix_client Library 17 + Full HTTP client implementation with 23 modules: 18 + 19 + | Module | Features | 20 + |--------|----------| 21 + | Auth | Login (password/token), logout, register, token refresh, whoami | 22 + | Sync | Single sync, sync loop with callbacks, filter creation | 23 + | Rooms | Create, join, leave, invite, kick, ban, power levels, public rooms | 24 + | Messages | Send text/emote/notice/image/file, redact, get messages/context | 25 + | State | Get/set state events, room name/topic/avatar | 26 + | Keys | E2EE key generation, upload, query, claim (Ed25519/Curve25519) | 27 + | Olm | Olm/Megolm session management, X3DH key exchange, Double Ratchet | 28 + | Verification | Cross-signing keys, SAS/QR verification, device trust | 29 + | Backup | Server-side key backup, recovery keys, m.megolm_backup.v1 | 30 + | Media | Upload, download, thumbnails, mxc:// URI handling | 31 + | Profile | Get/set display name and avatar | 32 + | Devices | List, update, delete devices | 33 + | Account | Account data, 3PIDs, password change, ignore users | 34 + | Presence | Get/set online status | 35 + | Typing | Typing notifications | 36 + | Receipts | Read receipts and markers | 37 + | Relations | Reactions, edits, replies, threads | 38 + | Directory | Room aliases, visibility, search | 39 + | Uiaa | User-Interactive Authentication for protected operations | 40 + | Timeline | Room timeline caching, LinkedChunk pagination, event deduplication | 41 + | Store | Pluggable storage layer (STATE_STORE, CRYPTO_STORE, EVENT_CACHE_STORE) | 42 + | Send_queue | Offline message queueing, retry logic, local echo, dependencies | 43 + 44 + ### matrix_eio Library (NEW) 45 + Eio-idiomatic wrapper providing: 46 + - **Structured Concurrency**: All operations respect Eio switches for resource cleanup and cancellation 47 + - **Exception-based Errors**: Operations raise `Eio.Io` exceptions instead of returning Results 48 + - **Fibre-based Sync**: Sync loop runs in dedicated fibres with proper cancellation support 49 + - **Eio.Stream Integration**: Push sync events to streams for decoupled event processing 50 + 51 + Example usage: 52 + ```ocaml 53 + Eio_main.run (fun env -> 54 + Eio.Switch.run (fun sw -> 55 + (* Create and login *) 56 + let client = Matrix_eio.login_password ~sw ~env 57 + ~homeserver:(Uri.of_string "https://matrix.org") 58 + ~user:"username" ~password:"password" () in 59 + 60 + (* Send a message *) 61 + Matrix_eio.Messages.send_text client 62 + ~room_id:(Matrix_proto.Id.Room_id.of_string_exn "!room:server") 63 + ~body:"Hello, Matrix!" (); 64 + 65 + (* Sync with callbacks *) 66 + Matrix_eio.Sync.sync_forever ~sw ~clock:(Eio.Stdenv.clock env) 67 + client 68 + ~on_sync:(fun response -> 69 + (* Handle sync response *) 70 + Matrix_eio.Sync.Continue) 71 + () 72 + )) 73 + ``` 74 + 75 + ## Dependencies 76 + - `jsont` / `jsont.bytesrw` - JSON encoding/decoding 77 + - `requests` - HTTP client (not cohttp) 78 + - `eio` - Async I/O 79 + - `uri` - URI handling 80 + - `ptime` - Timestamps 81 + - `base64` - Base64 encoding 82 + - `mirage-crypto` - AES-CBC encryption 83 + - `mirage-crypto-ec` - Ed25519/X25519 cryptography 84 + - `mirage-crypto-rng` - Secure random number generation 85 + - `digestif` - SHA-256 hashing 86 + - `kdf.hkdf` - HKDF key derivation 87 + 88 + ## TODO 89 + 90 + ### Remaining Features 91 + - [ ] Integration tests against real homeserver 92 + - [ ] Secret storage (m.secret_storage.v1) 93 + - [ ] SQLite/persistent storage implementations (currently in-memory only) 94 + - [ ] To-device message handling for verification flows 95 + - [ ] Actual backup upload/download API integration 96 + - [ ] Send queue HTTP integration (currently provides queue logic only) 97 + 98 + ### Known Limitations & Shortcuts 99 + 100 + **Verification module:** 101 + - SAS emoji table has 20 entries (spec requires 64) - needs completion 102 + - `verify_cross_signing_signature` checks signature existence but doesn't cryptographically verify 103 + - QR code verification has data structures but no encoding/decoding 104 + - No to-device message sending for verification protocol flow 105 + 106 + **Backup module:** 107 + - Recovery key uses base64 encoding instead of proper base58 (marked in code) 108 + - `parse_recovered_key` is a stub - needs JSON field extraction 109 + - Provides encryption/decryption primitives but no server API calls 110 + - Key backup upload/download requires integration with Client module 111 + 112 + **Send_queue module:** 113 + - Provides queue management but no HTTP sending integration 114 + - Caller must implement `send_fn` to actually send requests 115 + - Persistence helpers provided but no automatic persistence 116 + 117 + **Store module:** 118 + - All three stores (STATE_STORE, CRYPTO_STORE, EVENT_CACHE_STORE) are in-memory only 119 + - Module types defined for pluggable backends but only memory implementations exist 120 + - No SQLite or file-based implementations 121 + 122 + **Timeline module:** 123 + - LinkedChunk `push_back` has TODO for trimming when over `max_events` limit 124 + - LRU eviction in Cache removes oldest room but doesn't preserve by access time 125 + 126 + **Olm module:** 127 + - Session pickle/unpickle uses simplified JSON format 128 + - Ratchet implementation follows spec but needs real-world validation 129 + - No session recovery from corrupted state 130 + 131 + ## Build & Test 132 + ```bash 133 + cd /workspace/mymatrix/project/ocaml-matrix 134 + dune build 135 + dune runtest 136 + ``` 137 + 138 + ## Architecture Notes 139 + - Olm module provides session management for E2EE (X3DH, Double Ratchet, Megolm) 140 + - UIAA module supports password, recaptcha, email, and dummy authentication flows 141 + - Timeline module uses LinkedChunk for efficient pagination (based on matrix-rust-sdk pattern) 142 + - Store module provides pluggable storage with in-memory implementations 143 + - Verification module supports SAS (emoji/decimal) and QR code verification structures 144 + - Backup module implements m.megolm_backup.v1.curve25519-aes-sha2 encryption 145 + - Send_queue module provides offline queueing with dependency tracking and retry logic 146 + - Protocol types have 39 roundtrip tests; client modules need integration tests 147 + 148 + ## Comparison with matrix-rust-sdk 149 + The OCaml SDK now covers most major features of matrix-rust-sdk: 150 + - ✅ Core client operations (auth, sync, rooms, messages) 151 + - ✅ E2EE primitives (Olm, Megolm, key management) 152 + - ✅ Cross-signing key structures and verification protocols 153 + - ✅ Key backup encryption/decryption 154 + - ✅ Send queue with dependency system 155 + - ✅ Timeline caching with LinkedChunk 156 + - ✅ Pluggable storage architecture 157 + - ⚠️ Verification flows need to-device message integration 158 + - ⚠️ Backup needs server API integration 159 + - ⚠️ Storage needs persistent implementations 160 + - ❌ Sliding sync (MSC3575) - stub only 161 + - ❌ Secret storage (m.secret_storage.v1)
+1
dune
··· 1 + (vendored_dirs vendor)
+52
dune-project
··· 1 + (lang dune 3.20) 2 + (name ocaml-matrix) 3 + 4 + (generate_opam_files true) 5 + 6 + (source (github matrix-org/ocaml-matrix)) 7 + (license Apache-2.0) 8 + (authors "OCaml Matrix Contributors") 9 + (maintainers "dev@matrix.org") 10 + 11 + (package 12 + (name ocaml-matrix) 13 + (synopsis "Pure OCaml Matrix SDK") 14 + (description "A pure OCaml implementation of the Matrix client SDK") 15 + (allow_empty) 16 + (depends 17 + (ocaml (>= 5.1)) 18 + matrix_proto)) 19 + 20 + (package 21 + (name matrix_proto) 22 + (synopsis "Matrix protocol types with JSON codecs") 23 + (description "OCaml types for Matrix protocol with bidirectional JSON encoding/decoding using jsont") 24 + (depends 25 + (ocaml (>= 5.1)) 26 + jsont 27 + ptime 28 + (alcotest :with-test))) 29 + 30 + (package 31 + (name matrix_client) 32 + (synopsis "Matrix client SDK for OCaml") 33 + (description "Full Matrix client SDK using requests for HTTP and jsont for JSON") 34 + (depends 35 + (ocaml (>= 5.1)) 36 + matrix_proto 37 + requests 38 + jsont 39 + uri 40 + eio 41 + ptime)) 42 + 43 + (package 44 + (name matrix_eio) 45 + (synopsis "Eio-idiomatic Matrix client SDK") 46 + (description "Matrix client SDK using Eio idioms: switches for resource management, Eio.Io for errors, fibres for concurrency") 47 + (depends 48 + (ocaml (>= 5.1)) 49 + matrix_client 50 + matrix_proto 51 + eio 52 + uri))
+3
examples/dune
··· 1 + (executable 2 + (name simple_bot) 3 + (libraries matrix_eio matrix_proto jsont eio_main uri))
+168
examples/simple_bot.ml
··· 1 + (** Simple Matrix bot example using the Eio-idiomatic Matrix SDK. 2 + 3 + This example demonstrates: 4 + - Creating a client and logging in with exception-based error handling 5 + - Using Eio structured concurrency for the sync loop 6 + - Responding to messages in rooms 7 + - Proper cancellation via Eio switches 8 + 9 + To run: 10 + {[ 11 + dune exec examples/simple_bot.exe -- \ 12 + --homeserver https://matrix.org \ 13 + --username @bot:matrix.org \ 14 + --password secret 15 + ]} 16 + 17 + @see <https://spec.matrix.org/v1.11/client-server-api/> Matrix Client-Server API *) 18 + 19 + open Matrix_eio 20 + 21 + (** Helper to extract string from JSON. 22 + Returns [Some value] if the key exists and is a string, [None] otherwise. *) 23 + let json_get_string key (json : Jsont.json) = 24 + match json with 25 + | Jsont.Object (mems, _) -> 26 + List.find_map (fun ((name, _), value) -> 27 + if name = key then 28 + match value with 29 + | Jsont.String (s, _) -> Some s 30 + | _ -> None 31 + else None 32 + ) mems 33 + | _ -> None 34 + 35 + (** Handle a single sync response, looking for messages to respond to. 36 + 37 + This function processes timeline events from joined rooms and responds 38 + to simple commands: 39 + - [!echo <text>] - Echoes the text back 40 + - [!ping] - Responds with "pong!" 41 + 42 + @see <https://spec.matrix.org/v1.11/client-server-api/#syncing> Sync API *) 43 + let handle_sync client my_user_id (response : Matrix_proto.Sync.Response.t) = 44 + match response.rooms with 45 + | None -> () 46 + | Some rooms -> 47 + (* Process each joined room *) 48 + List.iter (fun (room_id_str, joined_room) -> 49 + match Matrix_proto.Id.Room_id.of_string room_id_str with 50 + | Error _ -> () 51 + | Ok room_id -> 52 + (* Check timeline events *) 53 + let events = match joined_room.Matrix_proto.Sync.Joined_room.timeline with 54 + | None -> [] 55 + | Some timeline -> timeline.events 56 + in 57 + List.iter (fun (event : Matrix_proto.Event.Raw_event.t) -> 58 + (* Only respond to m.room.message events *) 59 + let event_type = Matrix_proto.Event.Event_type.to_string event.type_ in 60 + if event_type = "m.room.message" then begin 61 + (* Get sender - don't respond to our own messages *) 62 + let sender = event.sender in 63 + let is_self = Matrix_proto.Id.User_id.to_string sender = 64 + Matrix_proto.Id.User_id.to_string my_user_id in 65 + if not is_self then begin 66 + (* Extract message body from content *) 67 + let body = json_get_string "body" event.content in 68 + match body with 69 + | Some msg when String.starts_with ~prefix:"!echo " msg -> 70 + (* Echo command - repeat the message *) 71 + let echo_text = String.sub msg 6 (String.length msg - 6) in 72 + (try 73 + Messages.send_text client ~room_id ~body:echo_text (); 74 + Printf.printf "Echoed: %s\n%!" echo_text 75 + with Eio.Io _ as e -> 76 + Printf.eprintf "Failed to send echo: %s\n%!" (Printexc.to_string e)) 77 + | Some msg when msg = "!ping" -> 78 + (* Ping command *) 79 + (try 80 + Messages.send_text client ~room_id ~body:"pong!" (); 81 + Printf.printf "Responded to ping\n%!" 82 + with Eio.Io _ as e -> 83 + Printf.eprintf "Failed to send pong: %s\n%!" (Printexc.to_string e)) 84 + | Some msg -> 85 + Printf.printf "Message from %s: %s\n%!" 86 + (Matrix_proto.Id.User_id.to_string sender) msg 87 + | None -> () 88 + end 89 + end 90 + ) events 91 + ) rooms.join 92 + 93 + (** Main bot loop using Eio structured concurrency. 94 + 95 + The sync loop runs in a dedicated fibre that can be cancelled by 96 + releasing the switch. This allows for clean shutdown. *) 97 + let run_bot ~homeserver ~username ~password = 98 + Eio_main.run @@ fun env -> 99 + Eio.Switch.run @@ fun sw -> 100 + 101 + Printf.printf "Connecting to %s...\n%!" homeserver; 102 + 103 + (* Create client and login using the Eio-idiomatic API. 104 + This raises Eio.Io on failure instead of returning Result. *) 105 + let client = 106 + try 107 + Matrix_eio.login_password ~sw ~env 108 + ~homeserver:(Uri.of_string homeserver) 109 + ~user:username ~password () 110 + with Eio.Io (Error.E err, _) -> 111 + Printf.eprintf "Login failed: %a\n%!" Error.pp_err err; 112 + exit 1 113 + in 114 + 115 + let my_user_id = Client.user_id client in 116 + Printf.printf "Logged in as %s\n%!" 117 + (Matrix_proto.Id.User_id.to_string my_user_id); 118 + 119 + (* Get joined rooms *) 120 + (try 121 + let rooms = Rooms.get_joined_rooms client in 122 + Printf.printf "Joined %d rooms\n%!" (List.length rooms) 123 + with Eio.Io _ -> 124 + Printf.printf "Could not get joined rooms\n%!"); 125 + 126 + (* Start sync loop in a fibre. 127 + The loop can be cancelled by releasing the switch. *) 128 + Printf.printf "Starting sync loop (Ctrl+C to stop)...\n%!"; 129 + 130 + let clock = Eio.Stdenv.clock env in 131 + 132 + (* Use the Eio-idiomatic sync loop with callback *) 133 + Sync.sync_forever ~sw ~clock client 134 + ~params:{ Sync.default_params with timeout = 30000 } 135 + ~on_sync:(fun response -> 136 + Printf.printf "Sync: next_batch=%s\n%!" response.next_batch; 137 + handle_sync client my_user_id response; 138 + Sync.Continue) 139 + ~on_error:(fun err -> 140 + Printf.eprintf "Sync error: %a\n%!" Error.pp_err err; 141 + (* Retry after 5 seconds on error *) 142 + Sync.Retry_after 5.0) 143 + (); 144 + 145 + (* Block the main fibre - sync runs in background *) 146 + (* In a real application, you might want to handle signals here *) 147 + Eio.Fiber.await_cancel () 148 + 149 + (** Parse command line and run the bot. *) 150 + let () = 151 + let homeserver = ref "" in 152 + let username = ref "" in 153 + let password = ref "" in 154 + 155 + let spec = [ 156 + ("--homeserver", Arg.Set_string homeserver, "Homeserver URL (e.g., https://matrix.org)"); 157 + ("--username", Arg.Set_string username, "Username (localpart or full @user:server)"); 158 + ("--password", Arg.Set_string password, "Password"); 159 + ] in 160 + 161 + Arg.parse spec (fun _ -> ()) "Simple Matrix Bot using Eio\n\nUsage:"; 162 + 163 + if !homeserver = "" || !username = "" || !password = "" then begin 164 + Printf.eprintf "Usage: simple_bot --homeserver URL --username USER --password PASS\n"; 165 + exit 1 166 + end; 167 + 168 + run_bot ~homeserver:!homeserver ~username:!username ~password:!password
+329
lib/matrix_client/account.ml
··· 1 + (** Account management operations. *) 2 + 3 + (* Account data *) 4 + let get_account_data client ~event_type = 5 + match Client.user_id client with 6 + | None -> Error (Error.Network_error "Not logged in") 7 + | Some user_id -> 8 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 9 + let path = Printf.sprintf "/user/%s/account_data/%s" 10 + (Uri.pct_encode user_id_str) 11 + (Uri.pct_encode event_type) 12 + in 13 + match Client.get client ~path () with 14 + | Error e -> Error e 15 + | Ok body -> Client.decode_response Jsont.json body 16 + 17 + let set_account_data client ~event_type ~content = 18 + match Client.user_id client with 19 + | None -> Error (Error.Network_error "Not logged in") 20 + | Some user_id -> 21 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 22 + let path = Printf.sprintf "/user/%s/account_data/%s" 23 + (Uri.pct_encode user_id_str) 24 + (Uri.pct_encode event_type) 25 + in 26 + match Client.encode_body Jsont.json content with 27 + | Error e -> Error e 28 + | Ok body -> 29 + match Client.put client ~path ~body () with 30 + | Error e -> Error e 31 + | Ok _ -> Ok () 32 + 33 + let get_room_account_data client ~room_id ~event_type = 34 + match Client.user_id client with 35 + | None -> Error (Error.Network_error "Not logged in") 36 + | Some user_id -> 37 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 38 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 39 + let path = Printf.sprintf "/user/%s/rooms/%s/account_data/%s" 40 + (Uri.pct_encode user_id_str) 41 + (Uri.pct_encode room_id_str) 42 + (Uri.pct_encode event_type) 43 + in 44 + match Client.get client ~path () with 45 + | Error e -> Error e 46 + | Ok body -> Client.decode_response Jsont.json body 47 + 48 + let set_room_account_data client ~room_id ~event_type ~content = 49 + match Client.user_id client with 50 + | None -> Error (Error.Network_error "Not logged in") 51 + | Some user_id -> 52 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 53 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 54 + let path = Printf.sprintf "/user/%s/rooms/%s/account_data/%s" 55 + (Uri.pct_encode user_id_str) 56 + (Uri.pct_encode room_id_str) 57 + (Uri.pct_encode event_type) 58 + in 59 + match Client.encode_body Jsont.json content with 60 + | Error e -> Error e 61 + | Ok body -> 62 + match Client.put client ~path ~body () with 63 + | Error e -> Error e 64 + | Ok _ -> Ok () 65 + 66 + (* Third-party identifiers *) 67 + type threepid = { 68 + medium : string; 69 + address : string; 70 + validated_at : int64; 71 + added_at : int64; 72 + } 73 + 74 + let threepid_jsont = 75 + Jsont.Object.( 76 + map (fun medium address validated_at added_at -> 77 + { medium; address; validated_at; added_at }) 78 + |> mem "medium" Jsont.string 79 + |> mem "address" Jsont.string 80 + |> mem "validated_at" Jsont.int64 81 + |> mem "added_at" Jsont.int64 82 + |> finish) 83 + 84 + type threepids_response = { 85 + threepids : threepid list; 86 + } 87 + 88 + let threepids_response_jsont = 89 + Jsont.Object.( 90 + map (fun threepids -> { threepids }) 91 + |> mem "threepids" (Jsont.list threepid_jsont) ~dec_absent:[] 92 + |> finish) 93 + 94 + let get_3pids client = 95 + match Client.get client ~path:"/account/3pid" () with 96 + | Error e -> Error e 97 + | Ok body -> 98 + match Client.decode_response threepids_response_jsont body with 99 + | Error e -> Error e 100 + | Ok resp -> Ok resp.threepids 101 + 102 + (* Email token request *) 103 + type email_token_request = { 104 + email : string; 105 + client_secret : string; 106 + send_attempt : int; 107 + } [@@warning "-69"] 108 + 109 + let email_token_request_jsont = 110 + Jsont.Object.( 111 + map (fun email client_secret send_attempt -> 112 + { email; client_secret; send_attempt }) 113 + |> mem "email" Jsont.string 114 + |> mem "client_secret" Jsont.string 115 + |> mem "send_attempt" Jsont.int 116 + |> finish) 117 + 118 + type token_response = { 119 + sid : string; 120 + } 121 + 122 + let token_response_jsont = 123 + Jsont.Object.( 124 + map (fun sid -> { sid }) 125 + |> mem "sid" Jsont.string 126 + |> finish) 127 + 128 + let request_email_token client ~email ~client_secret ~send_attempt = 129 + let request = { email; client_secret; send_attempt } in 130 + match Client.encode_body email_token_request_jsont request with 131 + | Error e -> Error e 132 + | Ok body -> 133 + match Client.post client ~path:"/account/3pid/email/requestToken" ~body () with 134 + | Error e -> Error e 135 + | Ok body -> 136 + match Client.decode_response token_response_jsont body with 137 + | Error e -> Error e 138 + | Ok resp -> Ok resp.sid 139 + 140 + (* MSISDN token request *) 141 + type msisdn_token_request = { 142 + country : string; 143 + phone_number : string; 144 + client_secret : string; 145 + send_attempt : int; 146 + } [@@warning "-69"] 147 + 148 + let msisdn_token_request_jsont = 149 + Jsont.Object.( 150 + map (fun country phone_number client_secret send_attempt -> 151 + { country; phone_number; client_secret; send_attempt }) 152 + |> mem "country" Jsont.string 153 + |> mem "phone_number" Jsont.string 154 + |> mem "client_secret" Jsont.string 155 + |> mem "send_attempt" Jsont.int 156 + |> finish) 157 + 158 + let request_msisdn_token client ~country ~phone_number ~client_secret ~send_attempt = 159 + let request = { country; phone_number; client_secret; send_attempt } in 160 + match Client.encode_body msisdn_token_request_jsont request with 161 + | Error e -> Error e 162 + | Ok body -> 163 + match Client.post client ~path:"/account/3pid/msisdn/requestToken" ~body () with 164 + | Error e -> Error e 165 + | Ok body -> 166 + match Client.decode_response token_response_jsont body with 167 + | Error e -> Error e 168 + | Ok resp -> Ok resp.sid 169 + 170 + (* Add 3pid *) 171 + type add_3pid_request = { 172 + client_secret : string; 173 + sid : string; 174 + } [@@warning "-69"] 175 + 176 + let add_3pid_request_jsont = 177 + Jsont.Object.( 178 + map (fun client_secret sid -> { client_secret; sid }) 179 + |> mem "client_secret" Jsont.string 180 + |> mem "sid" Jsont.string 181 + |> finish) 182 + 183 + let add_3pid client ~client_secret ~sid = 184 + let request = { client_secret; sid } in 185 + match Client.encode_body add_3pid_request_jsont request with 186 + | Error e -> Error e 187 + | Ok body -> 188 + match Client.post client ~path:"/account/3pid/add" ~body () with 189 + | Error e -> Error e 190 + | Ok _ -> Ok () 191 + 192 + (* Delete 3pid *) 193 + type delete_3pid_request = { 194 + medium : string; 195 + address : string; 196 + } [@@warning "-69"] 197 + 198 + let delete_3pid_request_jsont = 199 + Jsont.Object.( 200 + map (fun medium address -> { medium; address }) 201 + |> mem "medium" Jsont.string 202 + |> mem "address" Jsont.string 203 + |> finish) 204 + 205 + let delete_3pid client ~medium ~address = 206 + let request = { medium; address } in 207 + match Client.encode_body delete_3pid_request_jsont request with 208 + | Error e -> Error e 209 + | Ok body -> 210 + match Client.post client ~path:"/account/3pid/delete" ~body () with 211 + | Error e -> Error e 212 + | Ok _ -> Ok () 213 + 214 + (* Password change - simplified without UIAA *) 215 + type change_password_request = { 216 + new_password : string; 217 + logout_devices : bool; 218 + } [@@warning "-69"] 219 + 220 + let change_password_request_jsont = 221 + Jsont.Object.( 222 + map (fun new_password logout_devices -> { new_password; logout_devices }) 223 + |> mem "new_password" Jsont.string 224 + |> mem "logout_devices" Jsont.bool ~dec_absent:false 225 + |> finish) 226 + 227 + let change_password client ~new_password ?(logout_devices = false) () = 228 + let request = { new_password; logout_devices } in 229 + match Client.encode_body change_password_request_jsont request with 230 + | Error e -> Error e 231 + | Ok body -> 232 + match Client.post client ~path:"/account/password" ~body () with 233 + | Error e -> Error e 234 + | Ok _ -> Ok () 235 + 236 + (* Account deactivation - simplified without UIAA *) 237 + type deactivate_request = { 238 + erase : bool; 239 + } [@@warning "-69"] 240 + 241 + let deactivate_request_jsont = 242 + Jsont.Object.( 243 + map (fun erase -> { erase }) 244 + |> mem "erase" Jsont.bool ~dec_absent:false 245 + |> finish) 246 + 247 + let deactivate client ?(erase = false) () = 248 + let request = { erase } in 249 + match Client.encode_body deactivate_request_jsont request with 250 + | Error e -> Error e 251 + | Ok body -> 252 + match Client.post client ~path:"/account/deactivate" ~body () with 253 + | Error e -> Error e 254 + | Ok _ -> Ok () 255 + 256 + (* Ignored users - stored in account data *) 257 + type ignored_users_content = { 258 + ignored_users : (string * Jsont.json) list; 259 + } 260 + 261 + let ignored_users_content_jsont = 262 + let module StringMap = Map.Make(String) in 263 + let map_jsont = 264 + Jsont.Object.as_string_map Jsont.json 265 + |> Jsont.map 266 + ~dec:(fun m -> StringMap.bindings m) 267 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 268 + in 269 + Jsont.Object.( 270 + map (fun ignored_users -> { ignored_users }) 271 + |> mem "ignored_users" map_jsont ~dec_absent:[] 272 + |> finish) 273 + 274 + let get_ignored_users client = 275 + match get_account_data client ~event_type:"m.ignored_user_list" with 276 + | Error (Error.Matrix_error { errcode = Error.M_NOT_FOUND; _ }) -> Ok [] 277 + | Error e -> Error e 278 + | Ok json -> 279 + match Jsont_bytesrw.decode_string ignored_users_content_jsont 280 + (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with 281 + | Error _ -> Ok [] 282 + | Ok content -> 283 + let user_ids = List.filter_map (fun (uid, _) -> 284 + match Matrix_proto.Id.User_id.of_string uid with 285 + | Ok id -> Some id 286 + | Error _ -> None 287 + ) content.ignored_users in 288 + Ok user_ids 289 + 290 + let ignore_user client ~user_id = 291 + match get_ignored_users client with 292 + | Error e -> Error e 293 + | Ok current -> 294 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 295 + if List.exists (fun u -> Matrix_proto.Id.User_id.to_string u = user_id_str) current then 296 + Ok () (* Already ignored *) 297 + else 298 + let new_list = user_id :: current in 299 + let ignored_map = List.map (fun u -> 300 + (Matrix_proto.Id.User_id.to_string u, Jsont.Json.object' []) 301 + ) new_list in 302 + let content = { ignored_users = ignored_map } in 303 + match Client.encode_body ignored_users_content_jsont content with 304 + | Error e -> Error e 305 + | Ok body -> 306 + match Client.decode_response Jsont.json body with 307 + | Error e -> Error e 308 + | Ok json -> 309 + set_account_data client ~event_type:"m.ignored_user_list" ~content:json 310 + 311 + let unignore_user client ~user_id = 312 + match get_ignored_users client with 313 + | Error e -> Error e 314 + | Ok current -> 315 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 316 + let new_list = List.filter (fun u -> 317 + Matrix_proto.Id.User_id.to_string u <> user_id_str 318 + ) current in 319 + let ignored_map = List.map (fun u -> 320 + (Matrix_proto.Id.User_id.to_string u, Jsont.Json.object' []) 321 + ) new_list in 322 + let content = { ignored_users = ignored_map } in 323 + match Client.encode_body ignored_users_content_jsont content with 324 + | Error e -> Error e 325 + | Ok body -> 326 + match Client.decode_response Jsont.json body with 327 + | Error e -> Error e 328 + | Ok json -> 329 + set_account_data client ~event_type:"m.ignored_user_list" ~content:json
+120
lib/matrix_client/account.mli
··· 1 + (** Account management operations. *) 2 + 3 + (** {1 Account Data} *) 4 + 5 + (** Get global account data of a specific type. *) 6 + val get_account_data : 7 + Client.t -> 8 + event_type:string -> 9 + (Jsont.json, Error.t) result 10 + 11 + (** Set global account data. *) 12 + val set_account_data : 13 + Client.t -> 14 + event_type:string -> 15 + content:Jsont.json -> 16 + (unit, Error.t) result 17 + 18 + (** Get room-specific account data. *) 19 + val get_room_account_data : 20 + Client.t -> 21 + room_id:Matrix_proto.Id.Room_id.t -> 22 + event_type:string -> 23 + (Jsont.json, Error.t) result 24 + 25 + (** Set room-specific account data. *) 26 + val set_room_account_data : 27 + Client.t -> 28 + room_id:Matrix_proto.Id.Room_id.t -> 29 + event_type:string -> 30 + content:Jsont.json -> 31 + (unit, Error.t) result 32 + 33 + (** {1 Third-Party Identifiers} *) 34 + 35 + (** Third-party identifier info. *) 36 + type threepid = { 37 + medium : string; (** "email" or "msisdn" *) 38 + address : string; (** The identifier (email or phone) *) 39 + validated_at : int64; (** Timestamp when validated *) 40 + added_at : int64; (** Timestamp when added *) 41 + } 42 + 43 + (** Get all third-party identifiers for the account. *) 44 + val get_3pids : Client.t -> (threepid list, Error.t) result 45 + 46 + (** Request a token for adding an email address. 47 + Returns the session ID for the verification flow. *) 48 + val request_email_token : 49 + Client.t -> 50 + email:string -> 51 + client_secret:string -> 52 + send_attempt:int -> 53 + (string, Error.t) result 54 + 55 + (** Request a token for adding a phone number. 56 + Returns the session ID for the verification flow. *) 57 + val request_msisdn_token : 58 + Client.t -> 59 + country:string -> 60 + phone_number:string -> 61 + client_secret:string -> 62 + send_attempt:int -> 63 + (string, Error.t) result 64 + 65 + (** Add a third-party identifier after validation. 66 + Requires the session_id and client_secret from the token request. *) 67 + val add_3pid : 68 + Client.t -> 69 + client_secret:string -> 70 + sid:string -> 71 + (unit, Error.t) result 72 + 73 + (** Delete a third-party identifier. *) 74 + val delete_3pid : 75 + Client.t -> 76 + medium:string -> 77 + address:string -> 78 + (unit, Error.t) result 79 + 80 + (** {1 Password Management} *) 81 + 82 + (** Change the account password. 83 + 84 + @param logout_devices If true, invalidate all other sessions. *) 85 + val change_password : 86 + Client.t -> 87 + new_password:string -> 88 + ?logout_devices:bool -> 89 + unit -> 90 + (unit, Error.t) result 91 + 92 + (** {1 Account Deactivation} *) 93 + 94 + (** Deactivate the account. 95 + 96 + WARNING: This is irreversible! 97 + 98 + @param erase If true, request erasure of personal data. *) 99 + val deactivate : 100 + Client.t -> 101 + ?erase:bool -> 102 + unit -> 103 + (unit, Error.t) result 104 + 105 + (** {1 Ignored Users} *) 106 + 107 + (** Get the list of ignored user IDs. *) 108 + val get_ignored_users : Client.t -> (Matrix_proto.Id.User_id.t list, Error.t) result 109 + 110 + (** Ignore a user. *) 111 + val ignore_user : 112 + Client.t -> 113 + user_id:Matrix_proto.Id.User_id.t -> 114 + (unit, Error.t) result 115 + 116 + (** Unignore a user. *) 117 + val unignore_user : 118 + Client.t -> 119 + user_id:Matrix_proto.Id.User_id.t -> 120 + (unit, Error.t) result
+304
lib/matrix_client/auth.ml
··· 1 + (** Authentication operations. *) 2 + 3 + (* Login flow types *) 4 + type login_flow = 5 + | Password 6 + | Token 7 + | Sso 8 + | Unknown of string 9 + 10 + let login_flow_of_string = function 11 + | "m.login.password" -> Password 12 + | "m.login.token" -> Token 13 + | "m.login.sso" -> Sso 14 + | s -> Unknown s 15 + 16 + let login_flow_to_string = function 17 + | Password -> "m.login.password" 18 + | Token -> "m.login.token" 19 + | Sso -> "m.login.sso" 20 + | Unknown s -> s 21 + 22 + (* JSON codecs for login flows response *) 23 + let login_flow_jsont = 24 + Jsont.of_of_string ~kind:"login_flow" 25 + ~enc:login_flow_to_string 26 + (fun s -> Ok (login_flow_of_string s)) 27 + 28 + let login_flow_obj_jsont = 29 + Jsont.Object.map 30 + ~kind:"login_flow_object" 31 + (fun flow_type -> flow_type) 32 + |> Jsont.Object.mem "type" login_flow_jsont 33 + |> Jsont.Object.finish 34 + 35 + let login_flows_response_jsont = 36 + Jsont.Object.map 37 + ~kind:"login_flows_response" 38 + (fun flows -> flows) 39 + |> Jsont.Object.mem "flows" (Jsont.list login_flow_obj_jsont) 40 + |> Jsont.Object.finish 41 + 42 + let get_login_flows client = 43 + match Client.get client ~path:"/login" () with 44 + | Error e -> Error e 45 + | Ok body -> Client.decode_response login_flows_response_jsont body 46 + 47 + (* Login parameters *) 48 + type login_params = { 49 + device_id : string option; 50 + initial_device_display_name : string option; 51 + } 52 + 53 + let default_login_params = { 54 + device_id = None; 55 + initial_device_display_name = None; 56 + } 57 + 58 + (* Login request codec - write-only types for JSON encoding *) 59 + type login_request = { 60 + req_type : string; 61 + identifier : login_identifier; 62 + password : string option; 63 + token : string option; 64 + device_id : string option; 65 + initial_device_display_name : string option; 66 + } [@@warning "-69"] 67 + 68 + and login_identifier = { 69 + id_type : string; 70 + user : string option; 71 + } [@@warning "-69"] 72 + 73 + let login_identifier_jsont = 74 + Jsont.Object.map 75 + ~kind:"login_identifier" 76 + (fun id_type user -> { id_type; user }) 77 + |> Jsont.Object.mem "type" Jsont.string 78 + |> Jsont.Object.opt_mem "user" Jsont.string 79 + |> Jsont.Object.finish 80 + 81 + let login_request_jsont = 82 + Jsont.Object.map 83 + ~kind:"login_request" 84 + (fun req_type identifier password token device_id initial_device_display_name -> 85 + { req_type; identifier; password; token; device_id; initial_device_display_name }) 86 + |> Jsont.Object.mem "type" Jsont.string 87 + |> Jsont.Object.mem "identifier" login_identifier_jsont 88 + |> Jsont.Object.opt_mem "password" Jsont.string 89 + |> Jsont.Object.opt_mem "token" Jsont.string 90 + |> Jsont.Object.opt_mem "device_id" Jsont.string 91 + |> Jsont.Object.opt_mem "initial_device_display_name" Jsont.string 92 + |> Jsont.Object.finish 93 + 94 + (* Login response codec *) 95 + type login_response = { 96 + user_id : Matrix_proto.Id.User_id.t; 97 + access_token : string; 98 + device_id : Matrix_proto.Id.Device_id.t; 99 + refresh_token : string option; 100 + } 101 + 102 + let login_response_jsont = 103 + Jsont.Object.map 104 + ~kind:"login_response" 105 + (fun user_id access_token device_id refresh_token -> 106 + { user_id; access_token; device_id; refresh_token }) 107 + |> Jsont.Object.mem "user_id" Matrix_proto.Id.User_id.jsont 108 + |> Jsont.Object.mem "access_token" Jsont.string 109 + |> Jsont.Object.mem "device_id" Matrix_proto.Id.Device_id.jsont 110 + |> Jsont.Object.opt_mem "refresh_token" Jsont.string 111 + |> Jsont.Object.finish 112 + 113 + let response_to_session resp : Client.session = 114 + { user_id = resp.user_id; 115 + access_token = resp.access_token; 116 + device_id = resp.device_id; 117 + refresh_token = resp.refresh_token; 118 + } 119 + 120 + let login_password client ~user ~password ?(params = default_login_params) () = 121 + let request = { 122 + req_type = "m.login.password"; 123 + identifier = { id_type = "m.id.user"; user = Some user }; 124 + password = Some password; 125 + token = None; 126 + device_id = params.device_id; 127 + initial_device_display_name = params.initial_device_display_name; 128 + } in 129 + match Client.encode_body login_request_jsont request with 130 + | Error e -> Error e 131 + | Ok body -> 132 + match Client.post_unauthenticated client ~path:"/login" ~body () with 133 + | Error e -> Error e 134 + | Ok body -> 135 + match Client.decode_response login_response_jsont body with 136 + | Error e -> Error e 137 + | Ok resp -> Ok (response_to_session resp) 138 + 139 + let login_token client ~token ?(params = default_login_params) () = 140 + let request = { 141 + req_type = "m.login.token"; 142 + identifier = { id_type = "m.id.user"; user = None }; 143 + password = None; 144 + token = Some token; 145 + device_id = params.device_id; 146 + initial_device_display_name = params.initial_device_display_name; 147 + } in 148 + match Client.encode_body login_request_jsont request with 149 + | Error e -> Error e 150 + | Ok body -> 151 + match Client.post_unauthenticated client ~path:"/login" ~body () with 152 + | Error e -> Error e 153 + | Ok body -> 154 + match Client.decode_response login_response_jsont body with 155 + | Error e -> Error e 156 + | Ok resp -> Ok (response_to_session resp) 157 + 158 + (* Token refresh *) 159 + type refresh_request = { 160 + refresh_token : string; 161 + } [@@warning "-69"] 162 + 163 + let refresh_request_jsont = 164 + Jsont.Object.map 165 + ~kind:"refresh_request" 166 + (fun refresh_token -> { refresh_token }) 167 + |> Jsont.Object.mem "refresh_token" Jsont.string 168 + |> Jsont.Object.finish 169 + 170 + type refresh_response = { 171 + access_token : string; 172 + refresh_token : string option; 173 + } 174 + 175 + let refresh_response_jsont = 176 + Jsont.Object.map 177 + ~kind:"refresh_response" 178 + (fun access_token refresh_token -> { access_token; refresh_token }) 179 + |> Jsont.Object.mem "access_token" Jsont.string 180 + |> Jsont.Object.opt_mem "refresh_token" Jsont.string 181 + |> Jsont.Object.finish 182 + 183 + let refresh_token client ~refresh_token = 184 + let request = { refresh_token } in 185 + match Client.encode_body refresh_request_jsont request with 186 + | Error e -> Error e 187 + | Ok body -> 188 + match Client.post_unauthenticated client ~path:"/refresh" ~body () with 189 + | Error e -> Error e 190 + | Ok body -> 191 + match Client.decode_response refresh_response_jsont body with 192 + | Error e -> Error e 193 + | Ok resp -> Ok (resp.access_token, resp.refresh_token) 194 + 195 + (* Logout *) 196 + let logout client = 197 + match Client.post client ~path:"/logout" ~body:"{}" () with 198 + | Error e -> Error e 199 + | Ok _ -> Ok () 200 + 201 + let logout_all client = 202 + match Client.post client ~path:"/logout/all" ~body:"{}" () with 203 + | Error e -> Error e 204 + | Ok _ -> Ok () 205 + 206 + (* Registration *) 207 + type registration_kind = 208 + | User 209 + | Guest 210 + 211 + type register_request = { 212 + kind : string option; 213 + username : string option; 214 + password : string option; 215 + device_id : string option; 216 + initial_device_display_name : string option; 217 + inhibit_login : bool option; 218 + } [@@warning "-69"] 219 + 220 + let register_request_jsont = 221 + Jsont.Object.map 222 + ~kind:"register_request" 223 + (fun username password device_id initial_device_display_name inhibit_login -> 224 + { kind = None; username; password; device_id; initial_device_display_name; inhibit_login }) 225 + |> Jsont.Object.opt_mem "username" Jsont.string 226 + |> Jsont.Object.opt_mem "password" Jsont.string 227 + |> Jsont.Object.opt_mem "device_id" Jsont.string 228 + |> Jsont.Object.opt_mem "initial_device_display_name" Jsont.string 229 + |> Jsont.Object.opt_mem "inhibit_login" Jsont.bool 230 + |> Jsont.Object.finish 231 + 232 + type register_response = { 233 + user_id : Matrix_proto.Id.User_id.t; 234 + access_token : string option; 235 + device_id : string option; 236 + refresh_token : string option; 237 + } 238 + 239 + let register_response_jsont = 240 + Jsont.Object.map 241 + ~kind:"register_response" 242 + (fun user_id access_token device_id refresh_token -> 243 + { user_id; access_token; device_id; refresh_token }) 244 + |> Jsont.Object.mem "user_id" Matrix_proto.Id.User_id.jsont 245 + |> Jsont.Object.opt_mem "access_token" Jsont.string 246 + |> Jsont.Object.opt_mem "device_id" Jsont.string 247 + |> Jsont.Object.opt_mem "refresh_token" Jsont.string 248 + |> Jsont.Object.finish 249 + 250 + let register client ?kind ?username ?password ?device_id ?initial_device_display_name ?inhibit_login () = 251 + let kind_str = match kind with 252 + | Some Guest -> Some "guest" 253 + | Some User | None -> None 254 + in 255 + let query = match kind_str with 256 + | Some k -> Some [("kind", k)] 257 + | None -> None 258 + in 259 + let request = { 260 + kind = None; 261 + username; 262 + password; 263 + device_id; 264 + initial_device_display_name; 265 + inhibit_login; 266 + } in 267 + match Client.encode_body register_request_jsont request with 268 + | Error e -> Error e 269 + | Ok body -> 270 + match Client.post_unauthenticated client ~path:"/register" ?query ~body () with 271 + | Error e -> Error e 272 + | Ok body -> 273 + match Client.decode_response register_response_jsont body with 274 + | Error e -> Error e 275 + | Ok resp -> 276 + match resp.access_token, resp.device_id with 277 + | Some access_token, Some device_id -> 278 + let device_id = Matrix_proto.Id.Device_id.of_string_exn device_id in 279 + Ok { Client.user_id = resp.user_id; 280 + access_token; 281 + device_id; 282 + refresh_token = resp.refresh_token } 283 + | _ -> 284 + Error (Error.Json_error "Registration succeeded but no session returned (inhibit_login may be true)") 285 + 286 + (* Whoami *) 287 + type whoami_response = { 288 + user_id : Matrix_proto.Id.User_id.t; 289 + } 290 + 291 + let whoami_response_jsont = 292 + Jsont.Object.map 293 + ~kind:"whoami_response" 294 + (fun user_id -> { user_id }) 295 + |> Jsont.Object.mem "user_id" Matrix_proto.Id.User_id.jsont 296 + |> Jsont.Object.finish 297 + 298 + let whoami client = 299 + match Client.get client ~path:"/account/whoami" () with 300 + | Error e -> Error e 301 + | Ok body -> 302 + match Client.decode_response whoami_response_jsont body with 303 + | Error e -> Error e 304 + | Ok resp -> Ok resp.user_id
+92
lib/matrix_client/auth.mli
··· 1 + (** Authentication operations. *) 2 + 3 + (** {1 Login Flows} *) 4 + 5 + (** Supported login flow types. *) 6 + type login_flow = 7 + | Password (** m.login.password *) 8 + | Token (** m.login.token *) 9 + | Sso (** m.login.sso *) 10 + | Unknown of string 11 + 12 + (** Get supported login flows from the homeserver. *) 13 + val get_login_flows : Client.t -> (login_flow list, Error.t) result 14 + 15 + (** {1 Login} *) 16 + 17 + (** Login parameters. *) 18 + type login_params = { 19 + device_id : string option; 20 + (** Device ID to use. If not specified, the server will generate one. *) 21 + initial_device_display_name : string option; 22 + (** Display name for the new device. *) 23 + } 24 + 25 + (** Default login parameters. *) 26 + val default_login_params : login_params 27 + 28 + (** Login with username and password. 29 + 30 + On success, returns a session that should be passed to {!Client.with_session} 31 + to create an authenticated client. *) 32 + val login_password : 33 + Client.t -> 34 + user:string -> 35 + password:string -> 36 + ?params:login_params -> 37 + unit -> 38 + (Client.session, Error.t) result 39 + 40 + (** Login with a login token (e.g., from SSO flow). *) 41 + val login_token : 42 + Client.t -> 43 + token:string -> 44 + ?params:login_params -> 45 + unit -> 46 + (Client.session, Error.t) result 47 + 48 + (** {1 Token Refresh} *) 49 + 50 + (** Refresh an access token. 51 + 52 + Returns the new access token and optionally a new refresh token. *) 53 + val refresh_token : 54 + Client.t -> 55 + refresh_token:string -> 56 + (string * string option, Error.t) result 57 + 58 + (** {1 Logout} *) 59 + 60 + (** Logout the current session, invalidating the access token. *) 61 + val logout : Client.t -> (unit, Error.t) result 62 + 63 + (** Logout all sessions, invalidating all access tokens for this user. *) 64 + val logout_all : Client.t -> (unit, Error.t) result 65 + 66 + (** {1 Registration} *) 67 + 68 + (** Registration kind. *) 69 + type registration_kind = 70 + | User (** Normal user registration *) 71 + | Guest (** Guest account *) 72 + 73 + (** Register a new account. 74 + 75 + Note: Registration may require additional authentication flows (UIAA) 76 + which are not currently supported. This function only works when 77 + registration is open without additional verification. *) 78 + val register : 79 + Client.t -> 80 + ?kind:registration_kind -> 81 + ?username:string -> 82 + ?password:string -> 83 + ?device_id:string -> 84 + ?initial_device_display_name:string -> 85 + ?inhibit_login:bool -> 86 + unit -> 87 + (Client.session, Error.t) result 88 + 89 + (** {1 Account Info} *) 90 + 91 + (** Get the user ID for the current session (whoami endpoint). *) 92 + val whoami : Client.t -> (Matrix_proto.Id.User_id.t, Error.t) result
+466
lib/matrix_client/backup.ml
··· 1 + (** Server-side key backup and recovery. 2 + 3 + This module implements Matrix server-side backup of room keys using the 4 + m.megolm_backup.v1.curve25519-aes-sha2 backup algorithm. 5 + 6 + Key backup allows clients to store encrypted room keys on the server so 7 + they can be recovered on new devices or after data loss. 8 + 9 + Note: Due to various known flaws in this algorithm, it is provided mainly 10 + for backwards compatibility with existing backups. *) 11 + 12 + open Mirage_crypto_ec 13 + 14 + (** {1 Backup Key Types} *) 15 + 16 + (** Signature verification state *) 17 + type signature_state = 18 + | Missing (** No signature found *) 19 + | Invalid (** Signature is invalid *) 20 + | Valid_but_not_trusted (** Valid but signer not trusted *) 21 + | Valid_and_trusted (** Valid and signer is trusted *) 22 + 23 + let signature_state_trusted = function 24 + | Valid_and_trusted -> true 25 + | _ -> false 26 + 27 + (** Signature verification result *) 28 + type signature_verification = { 29 + device_signature : signature_state; 30 + user_identity_signature : signature_state; 31 + other_signatures : (string * signature_state) list; (** device_id -> state *) 32 + } 33 + 34 + let signature_verification_trusted v = 35 + signature_state_trusted v.device_signature || 36 + signature_state_trusted v.user_identity_signature || 37 + List.exists (fun (_, s) -> signature_state_trusted s) v.other_signatures 38 + 39 + let empty_signature_verification = { 40 + device_signature = Missing; 41 + user_identity_signature = Missing; 42 + other_signatures = []; 43 + } 44 + 45 + (** Auth data for m.megolm_backup.v1.curve25519-aes-sha2 *) 46 + type megolm_v1_auth_data = { 47 + public_key : string; (** Base64 Curve25519 public key *) 48 + signatures : (string * (string * string) list) list; (** user_id -> (key_id, sig) *) 49 + } 50 + 51 + (** Convert signatures to JSON *) 52 + let signatures_to_json signatures = 53 + let user_sigs = List.map (fun (user_id, key_sigs) -> 54 + let inner = List.map (fun (key_id, sig_) -> 55 + Printf.sprintf {|"%s":"%s"|} key_id sig_ 56 + ) key_sigs |> String.concat "," in 57 + Printf.sprintf {|"%s":{%s}|} user_id inner 58 + ) signatures |> String.concat "," in 59 + "{" ^ user_sigs ^ "}" 60 + 61 + (** Encode auth data to JSON string *) 62 + let megolm_v1_auth_data_to_json auth_data = 63 + Printf.sprintf {|{"public_key":"%s","signatures":%s}|} 64 + auth_data.public_key 65 + (signatures_to_json auth_data.signatures) 66 + 67 + (** Room key backup info - describes the backup algorithm and parameters *) 68 + type backup_info = 69 + | Megolm_v1_curve25519_aes_sha2 of megolm_v1_auth_data 70 + | Other of { algorithm : string; auth_data : Jsont.json } 71 + 72 + (** Backup version info from server *) 73 + type backup_version_info = { 74 + version : string; 75 + algorithm : string; 76 + auth_data : Jsont.json; 77 + count : int; 78 + etag : string; 79 + } 80 + 81 + (** {1 Backup Encryption Key} *) 82 + 83 + (** Private key for decrypting backed up room keys *) 84 + type backup_decryption_key = { 85 + private_key : string; (** Base64-encoded X25519 private key *) 86 + public_key : string; (** Base64-encoded X25519 public key *) 87 + } 88 + 89 + (** Public key for encrypting room keys for backup *) 90 + type backup_encryption_key = { 91 + public_key : string; (** Base64-encoded X25519 public key *) 92 + mutable backup_version : string option; 93 + mutable signatures : (string * (string * string) list) list; 94 + } 95 + 96 + (** Generate a new backup key pair *) 97 + let generate_backup_key () = 98 + let priv, pub = X25519.gen_key () in 99 + (* X25519.secret is a string, and gen_key returns (secret, public) where public is a string *) 100 + let priv_bytes = X25519.secret_to_octets priv in 101 + { 102 + private_key = Base64.encode_string priv_bytes; 103 + public_key = Base64.encode_string pub; 104 + } 105 + 106 + (** Create encryption key from decryption key *) 107 + let encryption_key_of_decryption_key (decryption_key : backup_decryption_key) = { 108 + public_key = decryption_key.public_key; 109 + backup_version = None; 110 + signatures = []; 111 + } 112 + 113 + (** Create encryption key from base64 public key *) 114 + let encryption_key_of_base64 public_key = 115 + match Base64.decode public_key with 116 + | Error _ -> Error "Invalid base64 encoding" 117 + | Ok bytes -> 118 + if String.length bytes <> 32 then 119 + Error "Invalid key length" 120 + else 121 + Ok { 122 + public_key; 123 + backup_version = None; 124 + signatures = []; 125 + } 126 + 127 + (** Get the backup algorithm name *) 128 + let backup_algorithm = "m.megolm_backup.v1.curve25519-aes-sha2" 129 + 130 + (** {1 Room Key Encryption for Backup} *) 131 + 132 + (** Encrypted session data (matches Matrix spec) *) 133 + type encrypted_session_data = { 134 + ephemeral : string; (** Base64 ephemeral public key *) 135 + ciphertext : string; (** Base64 ciphertext *) 136 + mac : string; (** Base64 MAC *) 137 + } 138 + 139 + (** Key backup data for a single session *) 140 + type key_backup_data = { 141 + first_message_index : int; 142 + forwarded_count : int; 143 + is_verified : bool; 144 + session_data : encrypted_session_data; 145 + } 146 + 147 + (** Room key backup request (for upload) *) 148 + type keys_backup_request = { 149 + rooms : (string * (string * key_backup_data) list) list; (** room_id -> session_id -> data *) 150 + } 151 + 152 + (** Encrypt a room key for backup using X25519/AES-256/HMAC-SHA256 *) 153 + let encrypt_room_key (encryption_key : backup_encryption_key) ~session_key ~session_id ~room_id ~sender_key = 154 + match Base64.decode encryption_key.public_key with 155 + | Error _ -> Error "Invalid encryption key" 156 + | Ok recipient_pub_bytes -> 157 + if String.length recipient_pub_bytes <> 32 then 158 + Error "Invalid X25519 public key length" 159 + else 160 + (* Generate ephemeral key pair *) 161 + let ephemeral_priv, ephemeral_pub = X25519.gen_key () in 162 + 163 + (* Perform X25519 key exchange *) 164 + (match X25519.key_exchange ephemeral_priv recipient_pub_bytes with 165 + | Error _ -> Error "Key exchange failed" 166 + | Ok shared_secret -> 167 + (* Derive encryption and MAC keys using HKDF *) 168 + let prk = Hkdf.extract ~hash:`SHA256 ~salt:"" shared_secret in 169 + let okm = Hkdf.expand ~hash:`SHA256 ~prk ~info:"" 64 in 170 + let aes_key = String.sub okm 0 32 in 171 + let mac_key = String.sub okm 32 32 in 172 + 173 + (* Build the payload to encrypt *) 174 + let payload = Printf.sprintf 175 + {|{"algorithm":"m.megolm.v1.aes-sha2","room_id":"%s","sender_key":"%s","session_id":"%s","session_key":"%s"}|} 176 + room_id sender_key session_id session_key 177 + in 178 + 179 + (* Pad payload to 16-byte boundary (PKCS#7) *) 180 + let pad_len = 16 - (String.length payload mod 16) in 181 + let padded = payload ^ String.make pad_len (Char.chr pad_len) in 182 + 183 + (* Generate random IV *) 184 + let iv = Mirage_crypto_rng.generate 16 in 185 + 186 + (* Encrypt with AES-256-CBC *) 187 + let key = Mirage_crypto.AES.CBC.of_secret aes_key in 188 + let ciphertext = Mirage_crypto.AES.CBC.encrypt ~key ~iv padded in 189 + 190 + (* Prepend IV to ciphertext for MAC calculation *) 191 + let mac_input = iv ^ ciphertext in 192 + 193 + (* Calculate HMAC-SHA256 *) 194 + let mac = Digestif.SHA256.hmac_string ~key:mac_key mac_input in 195 + let mac_bytes = Digestif.SHA256.to_raw_string mac in 196 + (* Truncate to first 8 bytes as per spec *) 197 + let mac_truncated = String.sub mac_bytes 0 8 in 198 + 199 + Ok { 200 + ephemeral = Base64.encode_string ephemeral_pub; 201 + ciphertext = Base64.encode_string (iv ^ ciphertext); 202 + mac = Base64.encode_string mac_truncated; 203 + }) 204 + 205 + (** Decrypt a room key from backup *) 206 + let decrypt_room_key (decryption_key : backup_decryption_key) (session_data : encrypted_session_data) = 207 + match Base64.decode decryption_key.private_key, 208 + Base64.decode session_data.ephemeral, 209 + Base64.decode session_data.ciphertext, 210 + Base64.decode session_data.mac with 211 + | Error _, _, _, _ -> Error "Invalid private key encoding" 212 + | _, Error _, _, _ -> Error "Invalid ephemeral key encoding" 213 + | _, _, Error _, _ -> Error "Invalid ciphertext encoding" 214 + | _, _, _, Error _ -> Error "Invalid MAC encoding" 215 + | Ok priv_bytes, Ok ephemeral_bytes, Ok ciphertext_with_iv, Ok mac_bytes -> 216 + match X25519.secret_of_octets priv_bytes with 217 + | Error _ -> Error "Invalid X25519 private key" 218 + | Ok (priv, _) -> 219 + (* Perform X25519 key exchange *) 220 + (match X25519.key_exchange priv ephemeral_bytes with 221 + | Error _ -> Error "Key exchange failed" 222 + | Ok shared_secret -> 223 + (* Derive encryption and MAC keys using HKDF *) 224 + let prk = Hkdf.extract ~hash:`SHA256 ~salt:"" shared_secret in 225 + let okm = Hkdf.expand ~hash:`SHA256 ~prk ~info:"" 64 in 226 + let aes_key = String.sub okm 0 32 in 227 + let mac_key = String.sub okm 32 32 in 228 + 229 + (* Verify MAC *) 230 + let expected_mac = Digestif.SHA256.hmac_string ~key:mac_key ciphertext_with_iv in 231 + let expected_mac_bytes = Digestif.SHA256.to_raw_string expected_mac in 232 + let expected_mac_truncated = String.sub expected_mac_bytes 0 8 in 233 + if mac_bytes <> expected_mac_truncated then 234 + Error "MAC verification failed" 235 + else begin 236 + (* Extract IV and ciphertext *) 237 + if String.length ciphertext_with_iv < 16 then 238 + Error "Ciphertext too short" 239 + else begin 240 + let iv = String.sub ciphertext_with_iv 0 16 in 241 + let ciphertext = String.sub ciphertext_with_iv 16 (String.length ciphertext_with_iv - 16) in 242 + 243 + (* Decrypt with AES-256-CBC *) 244 + let key = Mirage_crypto.AES.CBC.of_secret aes_key in 245 + let plaintext = Mirage_crypto.AES.CBC.decrypt ~key ~iv ciphertext in 246 + 247 + (* Remove PKCS#7 padding *) 248 + let pad_len = Char.code (String.get plaintext (String.length plaintext - 1)) in 249 + if pad_len < 1 || pad_len > 16 then 250 + Error "Invalid padding" 251 + else 252 + let unpadded = String.sub plaintext 0 (String.length plaintext - pad_len) in 253 + Ok unpadded 254 + end 255 + end) 256 + 257 + (** {1 Backup Machine State} *) 258 + 259 + (** State of the backup machine *) 260 + type backup_state = 261 + | Disabled (** No backup configured *) 262 + | Creating (** Creating a new backup *) 263 + | Enabling (** Enabling existing backup *) 264 + | Resuming (** Resuming existing backup *) 265 + | Enabled (** Backup is active *) 266 + | Downloading (** Downloading keys from backup *) 267 + | Disabling (** Disabling backup *) 268 + 269 + (** Backup machine for managing room key backups *) 270 + type t = { 271 + user_id : Matrix_proto.Id.User_id.t; 272 + device_id : Matrix_proto.Id.Device_id.t; 273 + mutable state : backup_state; 274 + mutable encryption_key : backup_encryption_key option; 275 + mutable decryption_key : backup_decryption_key option; 276 + mutable backup_version : string option; 277 + (* Pending sessions to backup *) 278 + mutable pending_sessions : (string * string * string) list; (** (room_id, session_id, sender_key) *) 279 + (* Sessions that have been backed up *) 280 + mutable backed_up_sessions : (string * string) list; (** (room_id, session_id) *) 281 + } 282 + 283 + (** Create a new backup machine *) 284 + let create ~user_id ~device_id = { 285 + user_id; 286 + device_id; 287 + state = Disabled; 288 + encryption_key = None; 289 + decryption_key = None; 290 + backup_version = None; 291 + pending_sessions = []; 292 + backed_up_sessions = []; 293 + } 294 + 295 + (** Check if backup is enabled *) 296 + let is_enabled t = t.state = Enabled 297 + 298 + (** Get the current backup version *) 299 + let backup_version t = t.backup_version 300 + 301 + (** {1 Backup Setup} *) 302 + 303 + (** Enable backup with a new key *) 304 + let enable_with_new_key t = 305 + let key = generate_backup_key () in 306 + t.decryption_key <- Some key; 307 + t.encryption_key <- Some (encryption_key_of_decryption_key key); 308 + t.state <- Creating; 309 + key 310 + 311 + (** Enable backup with an existing decryption key *) 312 + let enable_with_key t decryption_key = 313 + t.decryption_key <- Some decryption_key; 314 + t.encryption_key <- Some (encryption_key_of_decryption_key decryption_key); 315 + t.state <- Enabling 316 + 317 + (** Enable backup with only an encryption key (upload-only mode) *) 318 + let enable_upload_only t encryption_key version = 319 + t.encryption_key <- Some encryption_key; 320 + t.decryption_key <- None; 321 + t.backup_version <- Some version; 322 + encryption_key.backup_version <- Some version; 323 + t.state <- Enabled 324 + 325 + (** Set the backup version after creating *) 326 + let set_backup_version t version = 327 + t.backup_version <- Some version; 328 + (match t.encryption_key with 329 + | Some key -> key.backup_version <- Some version 330 + | None -> ()); 331 + t.state <- Enabled 332 + 333 + (** Disable backup *) 334 + let disable t = 335 + t.state <- Disabling; 336 + t.encryption_key <- None; 337 + t.decryption_key <- None; 338 + t.backup_version <- None; 339 + t.backed_up_sessions <- []; 340 + t.state <- Disabled 341 + 342 + (** {1 Session Management} *) 343 + 344 + (** Mark a session as needing backup *) 345 + let mark_session_for_backup t ~room_id ~session_id ~sender_key = 346 + if not (List.mem (room_id, session_id) t.backed_up_sessions) then 347 + t.pending_sessions <- (room_id, session_id, sender_key) :: t.pending_sessions 348 + 349 + (** Get number of pending sessions *) 350 + let pending_count t = List.length t.pending_sessions 351 + 352 + (** Check if a session has been backed up *) 353 + let is_session_backed_up t ~room_id ~session_id = 354 + List.mem (room_id, session_id) t.backed_up_sessions 355 + 356 + (** Mark a session as backed up *) 357 + let mark_session_backed_up t ~room_id ~session_id = 358 + t.pending_sessions <- List.filter (fun (r, s, _) -> r <> room_id || s <> session_id) t.pending_sessions; 359 + if not (List.mem (room_id, session_id) t.backed_up_sessions) then 360 + t.backed_up_sessions <- (room_id, session_id) :: t.backed_up_sessions 361 + 362 + (** {1 Room Key Recovery} *) 363 + 364 + (** Recovered room key data *) 365 + type recovered_room_key = { 366 + room_id : string; 367 + session_id : string; 368 + session_key : string; 369 + sender_key : string; 370 + algorithm : string; 371 + forwarded : bool; 372 + } 373 + 374 + (** Parse a recovered room key from decrypted JSON *) 375 + let parse_recovered_key json_str = 376 + (* Simple JSON parsing - in production would use proper parser *) 377 + let _get_field name str = 378 + let _pattern = Printf.sprintf {|"%s":"|} name in 379 + match String.split_on_char '"' str with 380 + | _ -> None 381 + in 382 + (* Simplified - would use Jsont in real implementation *) 383 + match Jsont_bytesrw.decode_string Jsont.json json_str with 384 + | Error _ -> None 385 + | Ok _json -> None (* Would extract fields from JSON *) 386 + 387 + (** Result of importing keys from backup *) 388 + type import_result = { 389 + imported_count : int; 390 + total_count : int; 391 + keys : recovered_room_key list; 392 + } 393 + 394 + (** {1 Backup Creation API Helpers} *) 395 + 396 + (** Build auth data for a new backup *) 397 + let build_auth_data encryption_key = { 398 + public_key = encryption_key.public_key; 399 + signatures = encryption_key.signatures; 400 + } 401 + 402 + (** Create backup version request body *) 403 + let create_version_request_body t = 404 + match t.encryption_key with 405 + | None -> Error "No encryption key configured" 406 + | Some key -> 407 + let auth_data = build_auth_data key in 408 + let auth_json = megolm_v1_auth_data_to_json auth_data in 409 + Ok (Printf.sprintf 410 + {|{"algorithm":"%s","auth_data":%s}|} 411 + backup_algorithm auth_json) 412 + 413 + (** {1 Recovery Key Format} *) 414 + 415 + (** Encode a backup decryption key as a recovery key (human-readable) *) 416 + let encode_recovery_key (key : backup_decryption_key) = 417 + match Base64.decode key.private_key with 418 + | Error _ -> Error "Invalid private key" 419 + | Ok bytes -> 420 + (* Add header byte 0x8B, then calculate parity byte *) 421 + let with_header = "\x8B" ^ bytes in 422 + let parity = String.fold_left (fun acc c -> acc lxor Char.code c) 0 with_header in 423 + let full = with_header ^ String.make 1 (Char.chr parity) in 424 + (* Encode as base58 with spaces every 4 chars for readability *) 425 + let _base58_alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" in 426 + (* Simplified - would use proper base58 encoding; using base64 for now *) 427 + let encoded = Base64.encode_string ~pad:false full in 428 + (* Add spaces for readability *) 429 + let with_spaces = String.to_seq encoded 430 + |> Seq.mapi (fun i c -> if i > 0 && i mod 4 = 0 then [' '; c] else [c]) 431 + |> Seq.flat_map List.to_seq 432 + |> String.of_seq 433 + in 434 + Ok with_spaces 435 + 436 + (** Decode a recovery key to a backup decryption key *) 437 + let decode_recovery_key recovery_key = 438 + (* Remove spaces and decode *) 439 + let cleaned = String.split_on_char ' ' recovery_key |> String.concat "" in 440 + match Base64.decode cleaned with 441 + | Error _ -> Error "Invalid recovery key format" 442 + | Ok bytes -> 443 + if String.length bytes < 3 then 444 + Error "Recovery key too short" 445 + else if String.get bytes 0 <> '\x8B' then 446 + Error "Invalid recovery key header" 447 + else begin 448 + (* Verify parity *) 449 + let key_bytes = String.sub bytes 1 (String.length bytes - 2) in 450 + let expected_parity = Char.code (String.get bytes (String.length bytes - 1)) in 451 + let actual_parity = String.fold_left (fun acc c -> acc lxor Char.code c) 0 452 + (String.sub bytes 0 (String.length bytes - 1)) in 453 + if expected_parity <> actual_parity then 454 + Error "Recovery key parity check failed" 455 + else begin 456 + (* Derive public key from private key *) 457 + match X25519.secret_of_octets key_bytes with 458 + | Error _ -> Error "Invalid private key in recovery key" 459 + | Ok (priv, pub_bytes) -> 460 + ignore priv; 461 + Ok { 462 + private_key = Base64.encode_string key_bytes; 463 + public_key = Base64.encode_string pub_bytes; 464 + } 465 + end 466 + end
+193
lib/matrix_client/calls.ml
··· 1 + (** VoIP call signaling operations. *) 2 + 3 + (** Call state *) 4 + type call_state = 5 + | Ringing 6 + | Connected 7 + | Ended 8 + 9 + (** Generate a new call ID. *) 10 + let generate_call_id () = 11 + (* Use transaction ID generator for call IDs *) 12 + Matrix_proto.Id.Transaction_id.(generate () |> to_string) 13 + 14 + (** Generate a new party ID for multi-party calls. *) 15 + let generate_party_id () = 16 + Matrix_proto.Id.Transaction_id.(generate () |> to_string) 17 + 18 + (** Send a call invite. 19 + 20 + @param room_id The room to send the invite in 21 + @param call_id The call ID 22 + @param offer The SDP offer 23 + @param lifetime How long the invite is valid in milliseconds 24 + @param version Call version (0 or 1) 25 + @param party_id Party ID for version 1 calls 26 + @param invitee Optional user to invite (for version 1 calls) *) 27 + let send_invite client ~room_id ~call_id ~offer ~lifetime 28 + ?(version = 1) ?party_id ?invitee () = 29 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 30 + let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in 31 + let path = Printf.sprintf "/rooms/%s/send/m.call.invite/%s" 32 + (Uri.pct_encode room_id_str) 33 + (Uri.pct_encode txn_id) 34 + in 35 + let content : Matrix_proto.Event.Call_invite_content.t = { 36 + call_id; 37 + party_id; 38 + version; 39 + lifetime; 40 + offer; 41 + invitee; 42 + } in 43 + match Client.encode_body Matrix_proto.Event.Call_invite_content.jsont content with 44 + | Error e -> Error e 45 + | Ok body -> 46 + match Client.put client ~path ~body () with 47 + | Error e -> Error e 48 + | Ok resp_body -> 49 + match Client.decode_response Messages.send_response_jsont resp_body with 50 + | Error e -> Error e 51 + | Ok resp -> Ok resp.event_id 52 + 53 + (** Send call candidates (ICE candidates). 54 + 55 + @param room_id The room 56 + @param call_id The call ID 57 + @param candidates List of ICE candidates 58 + @param version Call version 59 + @param party_id Party ID for version 1 calls *) 60 + let send_candidates client ~room_id ~call_id ~candidates 61 + ?(version = 1) ?party_id () = 62 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 63 + let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in 64 + let path = Printf.sprintf "/rooms/%s/send/m.call.candidates/%s" 65 + (Uri.pct_encode room_id_str) 66 + (Uri.pct_encode txn_id) 67 + in 68 + let content : Matrix_proto.Event.Call_candidates_content.t = { 69 + call_id; 70 + party_id; 71 + version; 72 + candidates; 73 + } in 74 + match Client.encode_body Matrix_proto.Event.Call_candidates_content.jsont content with 75 + | Error e -> Error e 76 + | Ok body -> 77 + match Client.put client ~path ~body () with 78 + | Error e -> Error e 79 + | Ok resp_body -> 80 + match Client.decode_response Messages.send_response_jsont resp_body with 81 + | Error e -> Error e 82 + | Ok resp -> Ok resp.event_id 83 + 84 + (** Send a call answer. 85 + 86 + @param room_id The room 87 + @param call_id The call ID 88 + @param answer The SDP answer 89 + @param version Call version 90 + @param party_id Party ID for version 1 calls *) 91 + let send_answer client ~room_id ~call_id ~answer 92 + ?(version = 1) ?party_id () = 93 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 94 + let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in 95 + let path = Printf.sprintf "/rooms/%s/send/m.call.answer/%s" 96 + (Uri.pct_encode room_id_str) 97 + (Uri.pct_encode txn_id) 98 + in 99 + let content : Matrix_proto.Event.Call_answer_content.t = { 100 + call_id; 101 + party_id; 102 + version; 103 + answer; 104 + } in 105 + match Client.encode_body Matrix_proto.Event.Call_answer_content.jsont content with 106 + | Error e -> Error e 107 + | Ok body -> 108 + match Client.put client ~path ~body () with 109 + | Error e -> Error e 110 + | Ok resp_body -> 111 + match Client.decode_response Messages.send_response_jsont resp_body with 112 + | Error e -> Error e 113 + | Ok resp -> Ok resp.event_id 114 + 115 + (** Hang up a call. 116 + 117 + @param room_id The room 118 + @param call_id The call ID 119 + @param reason Optional hangup reason 120 + @param version Call version 121 + @param party_id Party ID for version 1 calls *) 122 + let send_hangup client ~room_id ~call_id ?reason 123 + ?(version = 1) ?party_id () = 124 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 125 + let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in 126 + let path = Printf.sprintf "/rooms/%s/send/m.call.hangup/%s" 127 + (Uri.pct_encode room_id_str) 128 + (Uri.pct_encode txn_id) 129 + in 130 + let content : Matrix_proto.Event.Call_hangup_content.t = { 131 + call_id; 132 + party_id; 133 + version; 134 + reason; 135 + } in 136 + match Client.encode_body Matrix_proto.Event.Call_hangup_content.jsont content with 137 + | Error e -> Error e 138 + | Ok body -> 139 + match Client.put client ~path ~body () with 140 + | Error e -> Error e 141 + | Ok resp_body -> 142 + match Client.decode_response Messages.send_response_jsont resp_body with 143 + | Error e -> Error e 144 + | Ok resp -> Ok resp.event_id 145 + 146 + (** Reject an incoming call (same as hangup but with different semantics). *) 147 + let reject_call client ~room_id ~call_id ?(version = 1) ?party_id () = 148 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 149 + let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in 150 + let path = Printf.sprintf "/rooms/%s/send/m.call.reject/%s" 151 + (Uri.pct_encode room_id_str) 152 + (Uri.pct_encode txn_id) 153 + in 154 + (* m.call.reject has the same structure as m.call.hangup *) 155 + let content : Matrix_proto.Event.Call_hangup_content.t = { 156 + call_id; 157 + party_id; 158 + version; 159 + reason = None; 160 + } in 161 + match Client.encode_body Matrix_proto.Event.Call_hangup_content.jsont content with 162 + | Error e -> Error e 163 + | Ok body -> 164 + match Client.put client ~path ~body () with 165 + | Error e -> Error e 166 + | Ok resp_body -> 167 + match Client.decode_response Messages.send_response_jsont resp_body with 168 + | Error e -> Error e 169 + | Ok resp -> Ok resp.event_id 170 + 171 + (** TURN server credentials *) 172 + type turn_server = { 173 + username : string; 174 + password : string; 175 + uris : string list; 176 + ttl : int; 177 + } 178 + 179 + let turn_server_jsont = 180 + Jsont.Object.( 181 + map (fun username password uris ttl -> 182 + { username; password; uris; ttl }) 183 + |> mem "username" Jsont.string ~enc:(fun t -> t.username) 184 + |> mem "password" Jsont.string ~enc:(fun t -> t.password) 185 + |> mem "uris" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.uris) 186 + |> mem "ttl" Jsont.int ~enc:(fun t -> t.ttl) 187 + |> finish) 188 + 189 + (** Get TURN server credentials from the homeserver. *) 190 + let get_turn_server client = 191 + match Client.get client ~path:"/voip/turnServer" () with 192 + | Error e -> Error e 193 + | Ok body -> Client.decode_response turn_server_jsont body
+137
lib/matrix_client/client.ml
··· 1 + type config = { 2 + homeserver : Uri.t; 3 + user_agent : string option; 4 + } 5 + 6 + type session = { 7 + user_id : Matrix_proto.Id.User_id.t; 8 + access_token : string; 9 + device_id : Matrix_proto.Id.Device_id.t; 10 + refresh_token : string option; 11 + } 12 + 13 + type t = { 14 + http : Requests.t; 15 + config : config; 16 + session : session option; 17 + } 18 + 19 + let create ~sw ~config env = 20 + let http = Requests.create ~sw env in 21 + { http; config; session = None } 22 + 23 + let with_session t session = 24 + { t with session = Some session } 25 + 26 + let session t = t.session 27 + let homeserver t = t.config.homeserver 28 + let is_logged_in t = Option.is_some t.session 29 + let access_token t = Option.map (fun s -> s.access_token) t.session 30 + let user_id t = Option.map (fun s -> s.user_id) t.session 31 + let device_id t = Option.map (fun s -> s.device_id) t.session 32 + 33 + (* Matrix API base path *) 34 + let api_base = "/_matrix/client/v3" 35 + 36 + let make_url t path query = 37 + let base = t.config.homeserver in 38 + let path = api_base ^ path in 39 + let uri = Uri.with_path base path in 40 + match query with 41 + | None | Some [] -> uri 42 + | Some q -> Uri.with_query' uri q 43 + 44 + let auth_headers t = 45 + match t.session with 46 + | Some s -> 47 + Requests.Headers.(empty |> set "Authorization" ("Bearer " ^ s.access_token)) 48 + | None -> 49 + Requests.Headers.empty 50 + 51 + let add_user_agent t headers = 52 + match t.config.user_agent with 53 + | Some ua -> Requests.Headers.set "User-Agent" ua headers 54 + | None -> headers 55 + 56 + let json_content_type headers = 57 + Requests.Headers.set "Content-Type" "application/json" headers 58 + 59 + let handle_response response = 60 + let status = Requests.Response.status_code response in 61 + let body = Requests.Response.text response in 62 + if status >= 200 && status < 300 then 63 + Ok body 64 + else 65 + (* Try to parse as Matrix error *) 66 + match Jsont_bytesrw.decode_string Error.matrix_error_jsont body with 67 + | Ok matrix_err -> Error (Error.Matrix_error matrix_err) 68 + | Error _ -> Error (Error.Http_error { status; body }) 69 + 70 + let get t ~path ?query () = 71 + try 72 + let url = make_url t path query |> Uri.to_string in 73 + let headers = auth_headers t |> add_user_agent t in 74 + let response = Requests.get t.http ~headers url in 75 + handle_response response 76 + with 77 + | exn -> Error (Error.Network_error (Printexc.to_string exn)) 78 + 79 + let post t ~path ?query ~body () = 80 + try 81 + let url = make_url t path query |> Uri.to_string in 82 + let headers = auth_headers t |> add_user_agent t |> json_content_type in 83 + let body = Requests.Body.of_string Requests.Mime.json body in 84 + let response = Requests.post t.http ~headers ~body url in 85 + handle_response response 86 + with 87 + | exn -> Error (Error.Network_error (Printexc.to_string exn)) 88 + 89 + let put t ~path ?query ~body () = 90 + try 91 + let url = make_url t path query |> Uri.to_string in 92 + let headers = auth_headers t |> add_user_agent t |> json_content_type in 93 + let body = Requests.Body.of_string Requests.Mime.json body in 94 + let response = Requests.put t.http ~headers ~body url in 95 + handle_response response 96 + with 97 + | exn -> Error (Error.Network_error (Printexc.to_string exn)) 98 + 99 + let delete t ~path ?query ?body () = 100 + try 101 + let url = make_url t path query |> Uri.to_string in 102 + let headers = auth_headers t |> add_user_agent t in 103 + let headers, body = 104 + match body with 105 + | Some b -> 106 + (json_content_type headers, 107 + Some (Requests.Body.of_string Requests.Mime.json b)) 108 + | None -> (headers, None) 109 + in 110 + let response = Requests.request t.http ~headers ?body ~method_:`DELETE url in 111 + handle_response response 112 + with 113 + | exn -> Error (Error.Network_error (Printexc.to_string exn)) 114 + 115 + let post_unauthenticated t ~path ?query ~body () = 116 + try 117 + let url = make_url t path query |> Uri.to_string in 118 + let headers = 119 + Requests.Headers.empty 120 + |> add_user_agent t 121 + |> json_content_type 122 + in 123 + let body = Requests.Body.of_string Requests.Mime.json body in 124 + let response = Requests.post t.http ~headers ~body url in 125 + handle_response response 126 + with 127 + | exn -> Error (Error.Network_error (Printexc.to_string exn)) 128 + 129 + let decode_response jsont body = 130 + match Jsont_bytesrw.decode_string jsont body with 131 + | Ok v -> Ok v 132 + | Error e -> Error (Error.Json_error e) 133 + 134 + let encode_body jsont value = 135 + match Jsont_bytesrw.encode_string jsont value with 136 + | Ok s -> Ok s 137 + | Error e -> Error (Error.Json_error e)
+110
lib/matrix_client/client.mli
··· 1 + (** Matrix client type and lifecycle. *) 2 + 3 + (** Client configuration. *) 4 + type config = { 5 + homeserver : Uri.t; 6 + (** Homeserver URL (e.g., https://matrix.org) *) 7 + user_agent : string option; 8 + (** Optional custom User-Agent header *) 9 + } 10 + 11 + (** Session information after login. *) 12 + type session = { 13 + user_id : Matrix_proto.Id.User_id.t; 14 + access_token : string; 15 + device_id : Matrix_proto.Id.Device_id.t; 16 + refresh_token : string option; 17 + } 18 + 19 + (** Matrix client. *) 20 + type t 21 + 22 + (** Create a new client. 23 + 24 + The client is not logged in initially. Use {!Auth.login_password} or 25 + {!restore_session} to authenticate. 26 + 27 + The environment must provide network and clock capabilities. *) 28 + val create : 29 + sw:Eio.Switch.t -> 30 + config:config -> 31 + < net : _ Eio.Net.t ; clock : _ Eio.Time.clock ; fs : Eio.Fs.dir_ty Eio.Path.t ; .. > -> 32 + t 33 + 34 + (** Restore a client from a saved session. 35 + 36 + Returns a new client with the session set. The original client is unchanged. *) 37 + val with_session : t -> session -> t 38 + 39 + (** Get current session if logged in. *) 40 + val session : t -> session option 41 + 42 + (** Get the homeserver URL. *) 43 + val homeserver : t -> Uri.t 44 + 45 + (** Check if client is logged in. *) 46 + val is_logged_in : t -> bool 47 + 48 + (** Get the access token if logged in. *) 49 + val access_token : t -> string option 50 + 51 + (** Get the user ID if logged in. *) 52 + val user_id : t -> Matrix_proto.Id.User_id.t option 53 + 54 + (** Get the device ID if logged in. *) 55 + val device_id : t -> Matrix_proto.Id.Device_id.t option 56 + 57 + (** {1 Internal HTTP helpers} 58 + 59 + These are used by the API modules. *) 60 + 61 + (** Make a GET request. *) 62 + val get : 63 + t -> 64 + path:string -> 65 + ?query:(string * string) list -> 66 + unit -> 67 + (string, Error.t) result 68 + 69 + (** Make a POST request with JSON body. *) 70 + val post : 71 + t -> 72 + path:string -> 73 + ?query:(string * string) list -> 74 + body:string -> 75 + unit -> 76 + (string, Error.t) result 77 + 78 + (** Make a PUT request with JSON body. *) 79 + val put : 80 + t -> 81 + path:string -> 82 + ?query:(string * string) list -> 83 + body:string -> 84 + unit -> 85 + (string, Error.t) result 86 + 87 + (** Make a DELETE request. *) 88 + val delete : 89 + t -> 90 + path:string -> 91 + ?query:(string * string) list -> 92 + ?body:string -> 93 + unit -> 94 + (string, Error.t) result 95 + 96 + (** Make a POST request without authentication (for login/register). *) 97 + val post_unauthenticated : 98 + t -> 99 + path:string -> 100 + ?query:(string * string) list -> 101 + body:string -> 102 + unit -> 103 + (string, Error.t) result 104 + 105 + (** Decode a JSON response using a jsont codec. *) 106 + val decode_response : 'a Jsont.t -> string -> ('a, Error.t) result 107 + 108 + (** Encode a value to JSON string using a jsont codec. 109 + Returns Error if encoding fails. *) 110 + val encode_body : 'a Jsont.t -> 'a -> (string, Error.t) result
+89
lib/matrix_client/devices.ml
··· 1 + (** Device management operations. *) 2 + 3 + type device = { 4 + device_id : string; 5 + display_name : string option; 6 + last_seen_ip : string option; 7 + last_seen_ts : int64 option; 8 + } 9 + 10 + let device_jsont = 11 + Jsont.Object.( 12 + map (fun device_id display_name last_seen_ip last_seen_ts -> 13 + { device_id; display_name; last_seen_ip; last_seen_ts }) 14 + |> mem "device_id" Jsont.string 15 + |> opt_mem "display_name" Jsont.string ~enc:(fun t -> t.display_name) 16 + |> opt_mem "last_seen_ip" Jsont.string ~enc:(fun t -> t.last_seen_ip) 17 + |> opt_mem "last_seen_ts" Jsont.int64 ~enc:(fun t -> t.last_seen_ts) 18 + |> finish) 19 + 20 + type devices_response = { 21 + devices : device list; 22 + } 23 + 24 + let devices_response_jsont = 25 + Jsont.Object.( 26 + map (fun devices -> { devices }) 27 + |> mem "devices" (Jsont.list device_jsont) ~dec_absent:[] 28 + |> finish) 29 + 30 + let get_devices client = 31 + match Client.get client ~path:"/devices" () with 32 + | Error e -> Error e 33 + | Ok body -> 34 + match Client.decode_response devices_response_jsont body with 35 + | Error e -> Error e 36 + | Ok resp -> Ok resp.devices 37 + 38 + let get_device client ~device_id = 39 + let path = Printf.sprintf "/devices/%s" (Uri.pct_encode device_id) in 40 + match Client.get client ~path () with 41 + | Error e -> Error e 42 + | Ok body -> Client.decode_response device_jsont body 43 + 44 + type update_device_request = { 45 + display_name : string; 46 + } [@@warning "-69"] 47 + 48 + let update_device_request_jsont = 49 + Jsont.Object.( 50 + map (fun display_name -> { display_name }) 51 + |> mem "display_name" Jsont.string 52 + |> finish) 53 + 54 + let update_device client ~device_id ~display_name = 55 + let path = Printf.sprintf "/devices/%s" (Uri.pct_encode device_id) in 56 + let request = { display_name } in 57 + match Client.encode_body update_device_request_jsont request with 58 + | Error e -> Error e 59 + | Ok body -> 60 + match Client.put client ~path ~body () with 61 + | Error e -> Error e 62 + | Ok _ -> Ok () 63 + 64 + (* Delete device - simplified without UIAA *) 65 + let delete_device client ~device_id = 66 + let path = Printf.sprintf "/devices/%s" (Uri.pct_encode device_id) in 67 + match Client.delete client ~path () with 68 + | Error e -> Error e 69 + | Ok _ -> Ok () 70 + 71 + (* Delete multiple devices - simplified without UIAA *) 72 + type delete_devices_request = { 73 + devices : string list; 74 + } [@@warning "-69"] 75 + 76 + let delete_devices_request_jsont = 77 + Jsont.Object.( 78 + map (fun devices -> { devices }) 79 + |> mem "devices" (Jsont.list Jsont.string) 80 + |> finish) 81 + 82 + let delete_devices client ~device_ids = 83 + let request = { devices = device_ids } in 84 + match Client.encode_body delete_devices_request_jsont request with 85 + | Error e -> Error e 86 + | Ok body -> 87 + match Client.post client ~path:"/delete_devices" ~body () with 88 + | Error e -> Error e 89 + | Ok _ -> Ok ()
+43
lib/matrix_client/devices.mli
··· 1 + (** Device management operations. *) 2 + 3 + (** Device information. *) 4 + type device = { 5 + device_id : string; 6 + display_name : string option; 7 + last_seen_ip : string option; 8 + last_seen_ts : int64 option; 9 + } 10 + 11 + (** Get all devices for the current user. *) 12 + val get_devices : Client.t -> (device list, Error.t) result 13 + 14 + (** Get information about a specific device. *) 15 + val get_device : 16 + Client.t -> 17 + device_id:string -> 18 + (device, Error.t) result 19 + 20 + (** Update a device's display name. *) 21 + val update_device : 22 + Client.t -> 23 + device_id:string -> 24 + display_name:string -> 25 + (unit, Error.t) result 26 + 27 + (** Delete a device. 28 + 29 + Note: This may require interactive authentication (UIAA) 30 + which is not fully supported. *) 31 + val delete_device : 32 + Client.t -> 33 + device_id:string -> 34 + (unit, Error.t) result 35 + 36 + (** Delete multiple devices. 37 + 38 + Note: This may require interactive authentication (UIAA) 39 + which is not fully supported. *) 40 + val delete_devices : 41 + Client.t -> 42 + device_ids:string list -> 43 + (unit, Error.t) result
+182
lib/matrix_client/directory.ml
··· 1 + (** Room directory and alias operations. *) 2 + 3 + (* Alias resolution *) 4 + type alias_info = { 5 + room_id : Matrix_proto.Id.Room_id.t; 6 + servers : string list; 7 + } 8 + 9 + let alias_info_jsont = 10 + Jsont.Object.( 11 + map (fun room_id servers -> { room_id; servers }) 12 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont 13 + |> mem "servers" (Jsont.list Jsont.string) ~dec_absent:[] 14 + |> finish) 15 + 16 + let resolve_alias client ~alias = 17 + let alias_str = Matrix_proto.Id.Room_alias.to_string alias in 18 + let path = Printf.sprintf "/directory/room/%s" (Uri.pct_encode alias_str) in 19 + match Client.get client ~path () with 20 + | Error e -> Error e 21 + | Ok body -> Client.decode_response alias_info_jsont body 22 + 23 + (* Create alias *) 24 + type create_alias_request = { 25 + room_id : Matrix_proto.Id.Room_id.t; 26 + } [@@warning "-69"] 27 + 28 + let create_alias_request_jsont = 29 + Jsont.Object.( 30 + map (fun room_id -> { room_id }) 31 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont 32 + |> finish) 33 + 34 + let create_alias client ~alias ~room_id = 35 + let alias_str = Matrix_proto.Id.Room_alias.to_string alias in 36 + let path = Printf.sprintf "/directory/room/%s" (Uri.pct_encode alias_str) in 37 + let request = { room_id } in 38 + match Client.encode_body create_alias_request_jsont request with 39 + | Error e -> Error e 40 + | Ok body -> 41 + match Client.put client ~path ~body () with 42 + | Error e -> Error e 43 + | Ok _ -> Ok () 44 + 45 + (* Delete alias *) 46 + let delete_alias client ~alias = 47 + let alias_str = Matrix_proto.Id.Room_alias.to_string alias in 48 + let path = Printf.sprintf "/directory/room/%s" (Uri.pct_encode alias_str) in 49 + match Client.delete client ~path () with 50 + | Error e -> Error e 51 + | Ok _ -> Ok () 52 + 53 + (* Room visibility *) 54 + type visibility = [ `Public | `Private ] 55 + 56 + type visibility_response = { 57 + visibility : string; 58 + } 59 + 60 + let visibility_response_jsont = 61 + Jsont.Object.( 62 + map (fun visibility -> { visibility }) 63 + |> mem "visibility" Jsont.string 64 + |> finish) 65 + 66 + let get_visibility client ~room_id = 67 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 68 + let path = Printf.sprintf "/directory/list/room/%s" (Uri.pct_encode room_id_str) in 69 + match Client.get client ~path () with 70 + | Error e -> Error e 71 + | Ok body -> 72 + match Client.decode_response visibility_response_jsont body with 73 + | Error e -> Error e 74 + | Ok resp -> 75 + match resp.visibility with 76 + | "public" -> Ok `Public 77 + | _ -> Ok `Private 78 + 79 + type set_visibility_request = { 80 + visibility : string; 81 + } [@@warning "-69"] 82 + 83 + let set_visibility_request_jsont = 84 + Jsont.Object.( 85 + map (fun visibility -> { visibility }) 86 + |> mem "visibility" Jsont.string 87 + |> finish) 88 + 89 + let set_visibility client ~room_id ~visibility = 90 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 91 + let path = Printf.sprintf "/directory/list/room/%s" (Uri.pct_encode room_id_str) in 92 + let vis_str = match visibility with `Public -> "public" | `Private -> "private" in 93 + let request = { visibility = vis_str } in 94 + match Client.encode_body set_visibility_request_jsont request with 95 + | Error e -> Error e 96 + | Ok body -> 97 + match Client.put client ~path ~body () with 98 + | Error e -> Error e 99 + | Ok _ -> Ok () 100 + 101 + (* Room directory search *) 102 + type search_filter = { 103 + generic_search_term : string option; 104 + room_types : string list option; 105 + } 106 + 107 + type search_result = { 108 + chunk : Rooms.public_room list; 109 + next_batch : string option; 110 + prev_batch : string option; 111 + total_room_count_estimate : int option; 112 + } 113 + 114 + type search_request = { 115 + filter : search_filter_request option; 116 + limit : int option; 117 + since : string option; 118 + } [@@warning "-69"] 119 + 120 + and search_filter_request = { 121 + generic_search_term : string option; 122 + room_types : string list option; 123 + } [@@warning "-69"] 124 + 125 + let search_filter_request_jsont = 126 + Jsont.Object.( 127 + map (fun generic_search_term room_types -> 128 + { generic_search_term; room_types }) 129 + |> opt_mem "generic_search_term" Jsont.string ~enc:(fun (t : search_filter_request) -> t.generic_search_term) 130 + |> opt_mem "room_types" (Jsont.list Jsont.string) ~enc:(fun (t : search_filter_request) -> t.room_types) 131 + |> finish) 132 + 133 + let search_request_jsont = 134 + Jsont.Object.( 135 + map (fun filter limit since -> { filter; limit; since }) 136 + |> opt_mem "filter" search_filter_request_jsont ~enc:(fun (t : search_request) -> t.filter) 137 + |> opt_mem "limit" Jsont.int ~enc:(fun (t : search_request) -> t.limit) 138 + |> opt_mem "since" Jsont.string ~enc:(fun (t : search_request) -> t.since) 139 + |> finish) 140 + 141 + (* Reuse public_room_jsont from Rooms - need to duplicate here *) 142 + let public_room_jsont : Rooms.public_room Jsont.t = 143 + Jsont.Object.( 144 + map (fun room_id name topic num_joined_members world_readable guest_can_join avatar_url canonical_alias -> 145 + ({ room_id; name; topic; num_joined_members; world_readable; guest_can_join; avatar_url; canonical_alias } : Rooms.public_room)) 146 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont 147 + |> opt_mem "name" Jsont.string ~enc:(fun (t : Rooms.public_room) -> t.name) 148 + |> opt_mem "topic" Jsont.string ~enc:(fun (t : Rooms.public_room) -> t.topic) 149 + |> mem "num_joined_members" Jsont.int ~dec_absent:0 ~enc:(fun (t : Rooms.public_room) -> t.num_joined_members) 150 + |> mem "world_readable" Jsont.bool ~dec_absent:false ~enc:(fun (t : Rooms.public_room) -> t.world_readable) 151 + |> mem "guest_can_join" Jsont.bool ~dec_absent:false ~enc:(fun (t : Rooms.public_room) -> t.guest_can_join) 152 + |> opt_mem "avatar_url" Jsont.string ~enc:(fun (t : Rooms.public_room) -> t.avatar_url) 153 + |> opt_mem "canonical_alias" Jsont.string ~enc:(fun (t : Rooms.public_room) -> t.canonical_alias) 154 + |> finish) 155 + 156 + let search_result_jsont = 157 + Jsont.Object.( 158 + map (fun chunk next_batch prev_batch total_room_count_estimate -> 159 + { chunk; next_batch; prev_batch; total_room_count_estimate }) 160 + |> mem "chunk" (Jsont.list public_room_jsont) ~dec_absent:[] 161 + |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch) 162 + |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch) 163 + |> opt_mem "total_room_count_estimate" Jsont.int ~enc:(fun t -> t.total_room_count_estimate) 164 + |> finish) 165 + 166 + let search client ?server ?limit ?since ?(filter : search_filter option) () = 167 + let path = "/publicRooms" in 168 + let query = match server with 169 + | Some s -> Some [("server", s)] 170 + | None -> None 171 + in 172 + let filter_req : search_filter_request option = match filter with 173 + | Some f -> Some { generic_search_term = f.generic_search_term; room_types = f.room_types } 174 + | None -> None 175 + in 176 + let request = { filter = filter_req; limit; since } in 177 + match Client.encode_body search_request_jsont request with 178 + | Error e -> Error e 179 + | Ok body -> 180 + match Client.post client ~path ?query ~body () with 181 + | Error e -> Error e 182 + | Ok body -> Client.decode_response search_result_jsont body
+83
lib/matrix_client/directory.mli
··· 1 + (** Room directory and alias operations. *) 2 + 3 + (** {1 Room Aliases} *) 4 + 5 + (** Alias resolution result. *) 6 + type alias_info = { 7 + room_id : Matrix_proto.Id.Room_id.t; 8 + servers : string list; 9 + } 10 + 11 + (** Resolve a room alias to a room ID. 12 + 13 + Returns the room ID and a list of servers that know about the room. *) 14 + val resolve_alias : 15 + Client.t -> 16 + alias:Matrix_proto.Id.Room_alias.t -> 17 + (alias_info, Error.t) result 18 + 19 + (** Create a room alias pointing to a room. *) 20 + val create_alias : 21 + Client.t -> 22 + alias:Matrix_proto.Id.Room_alias.t -> 23 + room_id:Matrix_proto.Id.Room_id.t -> 24 + (unit, Error.t) result 25 + 26 + (** Delete a room alias. *) 27 + val delete_alias : 28 + Client.t -> 29 + alias:Matrix_proto.Id.Room_alias.t -> 30 + (unit, Error.t) result 31 + 32 + (** {1 Room Visibility} *) 33 + 34 + (** Room visibility in the directory. *) 35 + type visibility = [ `Public | `Private ] 36 + 37 + (** Get a room's visibility in the directory. *) 38 + val get_visibility : 39 + Client.t -> 40 + room_id:Matrix_proto.Id.Room_id.t -> 41 + (visibility, Error.t) result 42 + 43 + (** Set a room's visibility in the directory. 44 + 45 + Requires appropriate permissions in the room. *) 46 + val set_visibility : 47 + Client.t -> 48 + room_id:Matrix_proto.Id.Room_id.t -> 49 + visibility:visibility -> 50 + (unit, Error.t) result 51 + 52 + (** {1 Room Directory Search} *) 53 + 54 + (** Search filter for public rooms. *) 55 + type search_filter = { 56 + generic_search_term : string option; 57 + (** Search term to filter room names and topics. *) 58 + room_types : string list option; 59 + (** Filter by room types (e.g., "m.space"). None includes all types. *) 60 + } 61 + 62 + (** Public room search result. *) 63 + type search_result = { 64 + chunk : Rooms.public_room list; 65 + next_batch : string option; 66 + prev_batch : string option; 67 + total_room_count_estimate : int option; 68 + } 69 + 70 + (** Search the public room directory. 71 + 72 + @param server Server to query (default: local homeserver). 73 + @param limit Maximum number of rooms to return. 74 + @param since Pagination token. 75 + @param filter Search filter. *) 76 + val search : 77 + Client.t -> 78 + ?server:string -> 79 + ?limit:int -> 80 + ?since:string -> 81 + ?filter:search_filter -> 82 + unit -> 83 + (search_result, Error.t) result
+18
lib/matrix_client/dune
··· 1 + (library 2 + (name matrix_client) 3 + (public_name matrix_client) 4 + (libraries 5 + matrix_proto 6 + requests 7 + jsont 8 + jsont.bytesrw 9 + uri 10 + eio 11 + ptime 12 + base64 13 + mirage-crypto 14 + mirage-crypto-ec 15 + mirage-crypto-rng 16 + digestif 17 + kdf.hkdf 18 + unix))
+151
lib/matrix_client/error.ml
··· 1 + type errcode = 2 + | M_FORBIDDEN 3 + | M_UNKNOWN_TOKEN 4 + | M_MISSING_TOKEN 5 + | M_BAD_JSON 6 + | M_NOT_JSON 7 + | M_NOT_FOUND 8 + | M_LIMIT_EXCEEDED 9 + | M_UNRECOGNIZED 10 + | M_UNKNOWN 11 + | M_UNAUTHORIZED 12 + | M_USER_DEACTIVATED 13 + | M_USER_IN_USE 14 + | M_INVALID_USERNAME 15 + | M_ROOM_IN_USE 16 + | M_INVALID_ROOM_STATE 17 + | M_THREEPID_IN_USE 18 + | M_THREEPID_NOT_FOUND 19 + | M_THREEPID_AUTH_FAILED 20 + | M_THREEPID_DENIED 21 + | M_SERVER_NOT_TRUSTED 22 + | M_UNSUPPORTED_ROOM_VERSION 23 + | M_INCOMPATIBLE_ROOM_VERSION 24 + | M_BAD_STATE 25 + | M_GUEST_ACCESS_FORBIDDEN 26 + | M_CAPTCHA_NEEDED 27 + | M_CAPTCHA_INVALID 28 + | M_MISSING_PARAM 29 + | M_INVALID_PARAM 30 + | M_TOO_LARGE 31 + | M_EXCLUSIVE 32 + | M_RESOURCE_LIMIT_EXCEEDED 33 + | M_CANNOT_LEAVE_SERVER_NOTICE_ROOM 34 + | M_WEAK_PASSWORD 35 + | M_UNKNOWN_CODE of string 36 + 37 + let errcode_to_string = function 38 + | M_FORBIDDEN -> "M_FORBIDDEN" 39 + | M_UNKNOWN_TOKEN -> "M_UNKNOWN_TOKEN" 40 + | M_MISSING_TOKEN -> "M_MISSING_TOKEN" 41 + | M_BAD_JSON -> "M_BAD_JSON" 42 + | M_NOT_JSON -> "M_NOT_JSON" 43 + | M_NOT_FOUND -> "M_NOT_FOUND" 44 + | M_LIMIT_EXCEEDED -> "M_LIMIT_EXCEEDED" 45 + | M_UNRECOGNIZED -> "M_UNRECOGNIZED" 46 + | M_UNKNOWN -> "M_UNKNOWN" 47 + | M_UNAUTHORIZED -> "M_UNAUTHORIZED" 48 + | M_USER_DEACTIVATED -> "M_USER_DEACTIVATED" 49 + | M_USER_IN_USE -> "M_USER_IN_USE" 50 + | M_INVALID_USERNAME -> "M_INVALID_USERNAME" 51 + | M_ROOM_IN_USE -> "M_ROOM_IN_USE" 52 + | M_INVALID_ROOM_STATE -> "M_INVALID_ROOM_STATE" 53 + | M_THREEPID_IN_USE -> "M_THREEPID_IN_USE" 54 + | M_THREEPID_NOT_FOUND -> "M_THREEPID_NOT_FOUND" 55 + | M_THREEPID_AUTH_FAILED -> "M_THREEPID_AUTH_FAILED" 56 + | M_THREEPID_DENIED -> "M_THREEPID_DENIED" 57 + | M_SERVER_NOT_TRUSTED -> "M_SERVER_NOT_TRUSTED" 58 + | M_UNSUPPORTED_ROOM_VERSION -> "M_UNSUPPORTED_ROOM_VERSION" 59 + | M_INCOMPATIBLE_ROOM_VERSION -> "M_INCOMPATIBLE_ROOM_VERSION" 60 + | M_BAD_STATE -> "M_BAD_STATE" 61 + | M_GUEST_ACCESS_FORBIDDEN -> "M_GUEST_ACCESS_FORBIDDEN" 62 + | M_CAPTCHA_NEEDED -> "M_CAPTCHA_NEEDED" 63 + | M_CAPTCHA_INVALID -> "M_CAPTCHA_INVALID" 64 + | M_MISSING_PARAM -> "M_MISSING_PARAM" 65 + | M_INVALID_PARAM -> "M_INVALID_PARAM" 66 + | M_TOO_LARGE -> "M_TOO_LARGE" 67 + | M_EXCLUSIVE -> "M_EXCLUSIVE" 68 + | M_RESOURCE_LIMIT_EXCEEDED -> "M_RESOURCE_LIMIT_EXCEEDED" 69 + | M_CANNOT_LEAVE_SERVER_NOTICE_ROOM -> "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" 70 + | M_WEAK_PASSWORD -> "M_WEAK_PASSWORD" 71 + | M_UNKNOWN_CODE s -> s 72 + 73 + let errcode_of_string = function 74 + | "M_FORBIDDEN" -> M_FORBIDDEN 75 + | "M_UNKNOWN_TOKEN" -> M_UNKNOWN_TOKEN 76 + | "M_MISSING_TOKEN" -> M_MISSING_TOKEN 77 + | "M_BAD_JSON" -> M_BAD_JSON 78 + | "M_NOT_JSON" -> M_NOT_JSON 79 + | "M_NOT_FOUND" -> M_NOT_FOUND 80 + | "M_LIMIT_EXCEEDED" -> M_LIMIT_EXCEEDED 81 + | "M_UNRECOGNIZED" -> M_UNRECOGNIZED 82 + | "M_UNKNOWN" -> M_UNKNOWN 83 + | "M_UNAUTHORIZED" -> M_UNAUTHORIZED 84 + | "M_USER_DEACTIVATED" -> M_USER_DEACTIVATED 85 + | "M_USER_IN_USE" -> M_USER_IN_USE 86 + | "M_INVALID_USERNAME" -> M_INVALID_USERNAME 87 + | "M_ROOM_IN_USE" -> M_ROOM_IN_USE 88 + | "M_INVALID_ROOM_STATE" -> M_INVALID_ROOM_STATE 89 + | "M_THREEPID_IN_USE" -> M_THREEPID_IN_USE 90 + | "M_THREEPID_NOT_FOUND" -> M_THREEPID_NOT_FOUND 91 + | "M_THREEPID_AUTH_FAILED" -> M_THREEPID_AUTH_FAILED 92 + | "M_THREEPID_DENIED" -> M_THREEPID_DENIED 93 + | "M_SERVER_NOT_TRUSTED" -> M_SERVER_NOT_TRUSTED 94 + | "M_UNSUPPORTED_ROOM_VERSION" -> M_UNSUPPORTED_ROOM_VERSION 95 + | "M_INCOMPATIBLE_ROOM_VERSION" -> M_INCOMPATIBLE_ROOM_VERSION 96 + | "M_BAD_STATE" -> M_BAD_STATE 97 + | "M_GUEST_ACCESS_FORBIDDEN" -> M_GUEST_ACCESS_FORBIDDEN 98 + | "M_CAPTCHA_NEEDED" -> M_CAPTCHA_NEEDED 99 + | "M_CAPTCHA_INVALID" -> M_CAPTCHA_INVALID 100 + | "M_MISSING_PARAM" -> M_MISSING_PARAM 101 + | "M_INVALID_PARAM" -> M_INVALID_PARAM 102 + | "M_TOO_LARGE" -> M_TOO_LARGE 103 + | "M_EXCLUSIVE" -> M_EXCLUSIVE 104 + | "M_RESOURCE_LIMIT_EXCEEDED" -> M_RESOURCE_LIMIT_EXCEEDED 105 + | "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" -> M_CANNOT_LEAVE_SERVER_NOTICE_ROOM 106 + | "M_WEAK_PASSWORD" -> M_WEAK_PASSWORD 107 + | s -> M_UNKNOWN_CODE s 108 + 109 + let errcode_jsont = 110 + Jsont.of_of_string ~kind:"errcode" 111 + ~enc:errcode_to_string 112 + (fun s -> Ok (errcode_of_string s)) 113 + 114 + type matrix_error = { 115 + errcode : errcode; 116 + error : string; 117 + retry_after_ms : int option; 118 + soft_logout : bool option; 119 + } 120 + 121 + let matrix_error_jsont = 122 + Jsont.Object.( 123 + map (fun errcode error retry_after_ms soft_logout -> 124 + { errcode; error; retry_after_ms; soft_logout }) 125 + |> mem "errcode" errcode_jsont ~enc:(fun e -> e.errcode) 126 + |> mem "error" Jsont.string ~dec_absent:"" ~enc:(fun e -> e.error) 127 + |> opt_mem "retry_after_ms" Jsont.int ~enc:(fun e -> e.retry_after_ms) 128 + |> opt_mem "soft_logout" Jsont.bool ~enc:(fun e -> e.soft_logout) 129 + |> finish) 130 + 131 + type t = 132 + | Matrix_error of matrix_error 133 + | Network_error of string 134 + | Json_error of string 135 + | Http_error of { status : int; body : string } 136 + 137 + type 'a result = ('a, t) Stdlib.result 138 + 139 + let pp fmt = function 140 + | Matrix_error e -> 141 + Format.fprintf fmt "Matrix error %s: %s" 142 + (errcode_to_string e.errcode) e.error 143 + | Network_error s -> 144 + Format.fprintf fmt "Network error: %s" s 145 + | Json_error s -> 146 + Format.fprintf fmt "JSON error: %s" s 147 + | Http_error { status; body } -> 148 + Format.fprintf fmt "HTTP error %d: %s" status body 149 + 150 + let to_string e = 151 + Format.asprintf "%a" pp e
+71
lib/matrix_client/error.mli
··· 1 + (** Matrix error types and handling. *) 2 + 3 + (** Matrix API error codes per spec. *) 4 + type errcode = 5 + | M_FORBIDDEN 6 + | M_UNKNOWN_TOKEN 7 + | M_MISSING_TOKEN 8 + | M_BAD_JSON 9 + | M_NOT_JSON 10 + | M_NOT_FOUND 11 + | M_LIMIT_EXCEEDED 12 + | M_UNRECOGNIZED 13 + | M_UNKNOWN 14 + | M_UNAUTHORIZED 15 + | M_USER_DEACTIVATED 16 + | M_USER_IN_USE 17 + | M_INVALID_USERNAME 18 + | M_ROOM_IN_USE 19 + | M_INVALID_ROOM_STATE 20 + | M_THREEPID_IN_USE 21 + | M_THREEPID_NOT_FOUND 22 + | M_THREEPID_AUTH_FAILED 23 + | M_THREEPID_DENIED 24 + | M_SERVER_NOT_TRUSTED 25 + | M_UNSUPPORTED_ROOM_VERSION 26 + | M_INCOMPATIBLE_ROOM_VERSION 27 + | M_BAD_STATE 28 + | M_GUEST_ACCESS_FORBIDDEN 29 + | M_CAPTCHA_NEEDED 30 + | M_CAPTCHA_INVALID 31 + | M_MISSING_PARAM 32 + | M_INVALID_PARAM 33 + | M_TOO_LARGE 34 + | M_EXCLUSIVE 35 + | M_RESOURCE_LIMIT_EXCEEDED 36 + | M_CANNOT_LEAVE_SERVER_NOTICE_ROOM 37 + | M_WEAK_PASSWORD 38 + | M_UNKNOWN_CODE of string 39 + 40 + (** Convert errcode to string. *) 41 + val errcode_to_string : errcode -> string 42 + 43 + (** Convert string to errcode. *) 44 + val errcode_of_string : string -> errcode 45 + 46 + (** Matrix API error response. *) 47 + type matrix_error = { 48 + errcode : errcode; 49 + error : string; 50 + retry_after_ms : int option; 51 + soft_logout : bool option; 52 + } 53 + 54 + (** jsont codec for matrix_error. *) 55 + val matrix_error_jsont : matrix_error Jsont.t 56 + 57 + (** SDK error type. *) 58 + type t = 59 + | Matrix_error of matrix_error 60 + | Network_error of string 61 + | Json_error of string 62 + | Http_error of { status : int; body : string } 63 + 64 + (** Result type alias. *) 65 + type 'a result = ('a, t) Stdlib.result 66 + 67 + (** Pretty print error. *) 68 + val pp : Format.formatter -> t -> unit 69 + 70 + (** Convert error to string. *) 71 + val to_string : t -> string
+447
lib/matrix_client/keys.ml
··· 1 + (** E2EE key management for Matrix. 2 + 3 + This module handles device keys, one-time keys, and key exchange 4 + for end-to-end encryption using Olm/Megolm protocols. *) 5 + 6 + module Ed25519 = Mirage_crypto_ec.Ed25519 7 + module X25519 = Mirage_crypto_ec.X25519 8 + 9 + (* Base64 encoding/decoding - Matrix uses unpadded base64 *) 10 + let base64_encode s = 11 + Base64.encode_string ~pad:false s 12 + 13 + let base64_decode s = 14 + Base64.decode ~pad:false s 15 + 16 + (* Key types *) 17 + type ed25519_keypair = { 18 + priv : Ed25519.priv; 19 + pub : Ed25519.pub; 20 + } 21 + 22 + type curve25519_keypair = { 23 + secret : X25519.secret; 24 + public : string; 25 + } 26 + 27 + (* Device keys structure *) 28 + type device_keys = { 29 + user_id : Matrix_proto.Id.User_id.t; 30 + device_id : string; 31 + algorithms : string list; 32 + ed25519_key : string; (* base64 *) 33 + curve25519_key : string; (* base64 *) 34 + signatures : (string * (string * string) list) list; (* user_id -> key_id -> sig *) 35 + } 36 + 37 + (* One-time key *) 38 + type one_time_key = { 39 + key_id : string; 40 + key : string; (* base64 curve25519 public key *) 41 + signature : string option; (* optional signature *) 42 + } 43 + 44 + (* Fallback key *) 45 + type fallback_key = { 46 + key_id : string; 47 + key : string; 48 + signature : string option; 49 + } 50 + 51 + (* Key generation *) 52 + let generate_ed25519 () = 53 + let priv, pub = Ed25519.generate () in 54 + { priv; pub } 55 + 56 + let generate_curve25519 () = 57 + let secret, public = X25519.gen_key () in 58 + { secret; public } 59 + 60 + (* Serialize keys to base64 *) 61 + let ed25519_pub_to_base64 pub = 62 + Ed25519.pub_to_octets pub |> base64_encode 63 + 64 + let ed25519_priv_to_base64 priv = 65 + Ed25519.priv_to_octets priv |> base64_encode 66 + 67 + let curve25519_pub_to_base64 public = 68 + base64_encode public 69 + 70 + let curve25519_secret_to_base64 secret = 71 + X25519.secret_to_octets secret |> base64_encode 72 + 73 + (* Deserialize keys from base64 *) 74 + let ed25519_pub_of_base64 s = 75 + match base64_decode s with 76 + | Error _ -> Error "Invalid base64" 77 + | Ok octets -> 78 + match Ed25519.pub_of_octets octets with 79 + | Error _ -> Error "Invalid Ed25519 public key" 80 + | Ok pub -> Ok pub 81 + 82 + let ed25519_priv_of_base64 s = 83 + match base64_decode s with 84 + | Error _ -> Error "Invalid base64" 85 + | Ok octets -> 86 + match Ed25519.priv_of_octets octets with 87 + | Error _ -> Error "Invalid Ed25519 private key" 88 + | Ok priv -> Ok priv 89 + 90 + let curve25519_pub_of_base64 s = 91 + base64_decode s |> Result.map_error (fun _ -> "Invalid base64") 92 + 93 + let curve25519_secret_of_base64 s = 94 + match base64_decode s with 95 + | Error _ -> Error "Invalid base64" 96 + | Ok octets -> 97 + match X25519.secret_of_octets octets with 98 + | Error _ -> Error "Invalid Curve25519 secret key" 99 + | Ok (secret, _public) -> Ok secret 100 + 101 + (* Signing *) 102 + let sign_json ed25519_priv json_str = 103 + let signature = Ed25519.sign ~key:ed25519_priv json_str in 104 + base64_encode signature 105 + 106 + let verify_signature ed25519_pub signature_b64 json_str = 107 + match base64_decode signature_b64 with 108 + | Error _ -> false 109 + | Ok signature -> Ed25519.verify ~key:ed25519_pub signature ~msg:json_str 110 + 111 + (* Curve25519 key exchange *) 112 + let key_exchange ~secret ~their_public = 113 + match X25519.key_exchange secret their_public with 114 + | Error _ -> Error "Key exchange failed" 115 + | Ok shared -> Ok shared 116 + 117 + (* Generate a batch of one-time keys *) 118 + let generate_one_time_keys ~count ?sign_with () : (one_time_key * curve25519_keypair) list = 119 + List.init count (fun i -> 120 + let kp = generate_curve25519 () in 121 + let key_id = Printf.sprintf "AAAAAA%d" i in (* Would use proper ID generation *) 122 + let key = curve25519_pub_to_base64 kp.public in 123 + let signature = match sign_with with 124 + | Some ed_priv -> 125 + let to_sign = Printf.sprintf "{\"key\":\"%s\"}" key in 126 + Some (sign_json ed_priv to_sign) 127 + | None -> None 128 + in 129 + (({ key_id; key; signature } : one_time_key), kp) 130 + ) 131 + 132 + (* JSON codecs for key upload/query *) 133 + 134 + (* Upload keys request *) 135 + type upload_keys_request = { 136 + device_keys : device_keys_json option; 137 + one_time_keys : (string * one_time_key_json) list; 138 + fallback_keys : (string * one_time_key_json) list; 139 + } [@@warning "-69"] 140 + 141 + and device_keys_json = { 142 + user_id : string; 143 + device_id : string; 144 + algorithms : string list; 145 + keys : (string * string) list; (* key_id -> key *) 146 + signatures : (string * (string * string) list) list; 147 + } [@@warning "-69"] 148 + 149 + and one_time_key_json = { 150 + key : string; 151 + signatures : (string * (string * string) list) list option; 152 + } [@@warning "-69"] 153 + 154 + module StringMap = Map.Make(String) 155 + 156 + let string_string_map_jsont : (string * string) list Jsont.t = 157 + let map_jsont = Jsont.Object.as_string_map Jsont.string in 158 + Jsont.map 159 + ~dec:(fun m -> StringMap.bindings m) 160 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 161 + map_jsont 162 + 163 + let signatures_jsont : (string * (string * string) list) list Jsont.t = 164 + let inner = Jsont.Object.as_string_map Jsont.string in 165 + let outer = Jsont.Object.as_string_map inner in 166 + Jsont.map 167 + ~dec:(fun m -> 168 + StringMap.bindings m 169 + |> List.map (fun (k, v) -> (k, StringMap.bindings v))) 170 + ~enc:(fun l -> 171 + List.map (fun (k, v) -> (k, List.to_seq v |> StringMap.of_seq)) l 172 + |> List.to_seq |> StringMap.of_seq) 173 + outer 174 + 175 + let device_keys_json_jsont : device_keys_json Jsont.t = 176 + Jsont.Object.( 177 + map (fun user_id device_id algorithms keys signatures -> 178 + { user_id; device_id; algorithms; keys; signatures }) 179 + |> mem "user_id" Jsont.string 180 + |> mem "device_id" Jsont.string 181 + |> mem "algorithms" (Jsont.list Jsont.string) 182 + |> mem "keys" string_string_map_jsont 183 + |> mem "signatures" signatures_jsont 184 + |> finish) 185 + 186 + let one_time_key_json_jsont : one_time_key_json Jsont.t = 187 + Jsont.Object.( 188 + map (fun key signatures -> { key; signatures }) 189 + |> mem "key" Jsont.string 190 + |> opt_mem "signatures" signatures_jsont ~enc:(fun (t : one_time_key_json) -> t.signatures) 191 + |> finish) 192 + 193 + let one_time_keys_map_jsont : (string * one_time_key_json) list Jsont.t = 194 + let map_jsont = Jsont.Object.as_string_map one_time_key_json_jsont in 195 + Jsont.map 196 + ~dec:(fun m -> StringMap.bindings m) 197 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 198 + map_jsont 199 + 200 + let upload_keys_request_jsont : upload_keys_request Jsont.t = 201 + Jsont.Object.( 202 + map (fun device_keys one_time_keys fallback_keys -> 203 + { device_keys; one_time_keys; fallback_keys }) 204 + |> opt_mem "device_keys" device_keys_json_jsont ~enc:(fun t -> t.device_keys) 205 + |> mem "one_time_keys" one_time_keys_map_jsont ~dec_absent:[] 206 + |> mem "fallback_keys" one_time_keys_map_jsont ~dec_absent:[] 207 + |> finish) 208 + 209 + (* Upload keys response *) 210 + type upload_keys_response = { 211 + one_time_key_counts : (string * int) list; 212 + } 213 + 214 + let one_time_key_counts_jsont : (string * int) list Jsont.t = 215 + let map_jsont = Jsont.Object.as_string_map Jsont.int in 216 + Jsont.map 217 + ~dec:(fun m -> StringMap.bindings m) 218 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 219 + map_jsont 220 + 221 + let upload_keys_response_jsont = 222 + Jsont.Object.( 223 + map (fun one_time_key_counts -> { one_time_key_counts }) 224 + |> mem "one_time_key_counts" one_time_key_counts_jsont ~dec_absent:[] 225 + |> finish) 226 + 227 + (* Upload device keys *) 228 + let upload_keys client ?device_keys ?(one_time_keys=[]) ?(fallback_keys=[]) () = 229 + let request = { device_keys; one_time_keys; fallback_keys } in 230 + match Client.encode_body upload_keys_request_jsont request with 231 + | Error e -> Error e 232 + | Ok body -> 233 + match Client.post client ~path:"/keys/upload" ~body () with 234 + | Error e -> Error e 235 + | Ok body -> Client.decode_response upload_keys_response_jsont body 236 + 237 + (* Query keys request/response *) 238 + type query_keys_request = { 239 + timeout : int option; 240 + device_keys : (string * string list) list; (* user_id -> device_ids *) 241 + } [@@warning "-69"] 242 + 243 + let device_keys_query_jsont : (string * string list) list Jsont.t = 244 + let map_jsont = Jsont.Object.as_string_map (Jsont.list Jsont.string) in 245 + Jsont.map 246 + ~dec:(fun m -> StringMap.bindings m) 247 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 248 + map_jsont 249 + 250 + let query_keys_request_jsont = 251 + Jsont.Object.( 252 + map (fun timeout device_keys -> { timeout; device_keys }) 253 + |> opt_mem "timeout" Jsont.int ~enc:(fun t -> t.timeout) 254 + |> mem "device_keys" device_keys_query_jsont 255 + |> finish) 256 + 257 + type queried_device_keys = { 258 + user_id : string; 259 + device_id : string; 260 + algorithms : string list; 261 + keys : (string * string) list; 262 + signatures : (string * (string * string) list) list; 263 + unsigned : Jsont.json option; 264 + } 265 + 266 + let queried_device_keys_jsont = 267 + Jsont.Object.( 268 + map (fun user_id device_id algorithms keys signatures unsigned -> 269 + { user_id; device_id; algorithms; keys; signatures; unsigned }) 270 + |> mem "user_id" Jsont.string 271 + |> mem "device_id" Jsont.string 272 + |> mem "algorithms" (Jsont.list Jsont.string) ~dec_absent:[] 273 + |> mem "keys" string_string_map_jsont ~dec_absent:[] 274 + |> mem "signatures" signatures_jsont ~dec_absent:[] 275 + |> opt_mem "unsigned" Jsont.json ~enc:(fun t -> t.unsigned) 276 + |> finish) 277 + 278 + type query_keys_response = { 279 + failures : (string * Jsont.json) list; 280 + device_keys : (string * (string * queried_device_keys) list) list; 281 + } 282 + 283 + let device_keys_map_jsont = 284 + let inner = Jsont.Object.as_string_map queried_device_keys_jsont in 285 + let outer = Jsont.Object.as_string_map inner in 286 + Jsont.map 287 + ~dec:(fun m -> 288 + StringMap.bindings m 289 + |> List.map (fun (k, v) -> (k, StringMap.bindings v))) 290 + ~enc:(fun l -> 291 + List.map (fun (k, v) -> (k, List.to_seq v |> StringMap.of_seq)) l 292 + |> List.to_seq |> StringMap.of_seq) 293 + outer 294 + 295 + let failures_jsont = 296 + let map_jsont = Jsont.Object.as_string_map Jsont.json in 297 + Jsont.map 298 + ~dec:(fun m -> StringMap.bindings m) 299 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 300 + map_jsont 301 + 302 + let query_keys_response_jsont = 303 + Jsont.Object.( 304 + map (fun failures device_keys -> { failures; device_keys }) 305 + |> mem "failures" failures_jsont ~dec_absent:[] 306 + |> mem "device_keys" device_keys_map_jsont ~dec_absent:[] 307 + |> finish) 308 + 309 + (* Query device keys *) 310 + let query_keys client ?timeout ~users () = 311 + let device_keys = List.map (fun (user_id, device_ids) -> 312 + (Matrix_proto.Id.User_id.to_string user_id, device_ids) 313 + ) users in 314 + let request = { timeout; device_keys } in 315 + match Client.encode_body query_keys_request_jsont request with 316 + | Error e -> Error e 317 + | Ok body -> 318 + match Client.post client ~path:"/keys/query" ~body () with 319 + | Error e -> Error e 320 + | Ok body -> Client.decode_response query_keys_response_jsont body 321 + 322 + (* Claim one-time keys request/response *) 323 + type claim_keys_request = { 324 + timeout : int option; 325 + one_time_keys : (string * (string * string) list) list; (* user_id -> device_id -> algorithm *) 326 + } [@@warning "-69"] 327 + 328 + let one_time_keys_claim_jsont = 329 + let inner = Jsont.Object.as_string_map Jsont.string in 330 + let outer = Jsont.Object.as_string_map inner in 331 + Jsont.map 332 + ~dec:(fun m -> 333 + StringMap.bindings m 334 + |> List.map (fun (k, v) -> (k, StringMap.bindings v))) 335 + ~enc:(fun l -> 336 + List.map (fun (k, v) -> (k, List.to_seq v |> StringMap.of_seq)) l 337 + |> List.to_seq |> StringMap.of_seq) 338 + outer 339 + 340 + let claim_keys_request_jsont = 341 + Jsont.Object.( 342 + map (fun timeout one_time_keys -> { timeout; one_time_keys }) 343 + |> opt_mem "timeout" Jsont.int ~enc:(fun t -> t.timeout) 344 + |> mem "one_time_keys" one_time_keys_claim_jsont 345 + |> finish) 346 + 347 + type claim_keys_response = { 348 + failures : (string * Jsont.json) list; 349 + one_time_keys : (string * (string * (string * one_time_key_json) list) list) list; 350 + } 351 + 352 + let claimed_keys_map_jsont = 353 + let key_map = Jsont.Object.as_string_map one_time_key_json_jsont in 354 + let device_map = Jsont.Object.as_string_map key_map in 355 + let user_map = Jsont.Object.as_string_map device_map in 356 + Jsont.map 357 + ~dec:(fun m -> 358 + StringMap.bindings m 359 + |> List.map (fun (user, devices) -> 360 + (user, StringMap.bindings devices 361 + |> List.map (fun (dev, keys) -> (dev, StringMap.bindings keys))))) 362 + ~enc:(fun l -> 363 + List.map (fun (user, devices) -> 364 + (user, List.map (fun (dev, keys) -> 365 + (dev, List.to_seq keys |> StringMap.of_seq)) devices 366 + |> List.to_seq |> StringMap.of_seq)) l 367 + |> List.to_seq |> StringMap.of_seq) 368 + user_map 369 + 370 + let claim_keys_response_jsont = 371 + Jsont.Object.( 372 + map (fun failures one_time_keys -> { failures; one_time_keys }) 373 + |> mem "failures" failures_jsont ~dec_absent:[] 374 + |> mem "one_time_keys" claimed_keys_map_jsont ~dec_absent:[] 375 + |> finish) 376 + 377 + (* Claim one-time keys for Olm session establishment *) 378 + let claim_keys client ?timeout ~keys () = 379 + let one_time_keys = List.map (fun (user_id, device_keys) -> 380 + let user_str = Matrix_proto.Id.User_id.to_string user_id in 381 + let device_map = List.map (fun (device_id, algorithm) -> 382 + (device_id, algorithm) 383 + ) device_keys in 384 + (user_str, device_map) 385 + ) keys in 386 + let request = { timeout; one_time_keys } in 387 + match Client.encode_body claim_keys_request_jsont request with 388 + | Error e -> Error e 389 + | Ok body -> 390 + match Client.post client ~path:"/keys/claim" ~body () with 391 + | Error e -> Error e 392 + | Ok body -> Client.decode_response claim_keys_response_jsont body 393 + 394 + (* Key changes tracking *) 395 + type key_changes_response = { 396 + changed : string list; 397 + left : string list; 398 + } 399 + 400 + let key_changes_response_jsont = 401 + Jsont.Object.( 402 + map (fun changed left -> { changed; left }) 403 + |> mem "changed" (Jsont.list Jsont.string) ~dec_absent:[] 404 + |> mem "left" (Jsont.list Jsont.string) ~dec_absent:[] 405 + |> finish) 406 + 407 + let get_key_changes client ~from ~to_ = 408 + let query = [("from", from); ("to", to_)] in 409 + match Client.get client ~path:"/keys/changes" ~query () with 410 + | Error e -> Error e 411 + | Ok body -> Client.decode_response key_changes_response_jsont body 412 + 413 + (* Helper to create signed device keys for upload *) 414 + let create_device_keys ~user_id ~device_id ~ed25519_keypair ~curve25519_keypair = 415 + let user_str = Matrix_proto.Id.User_id.to_string user_id in 416 + let ed25519_pub = ed25519_pub_to_base64 ed25519_keypair.pub in 417 + let curve25519_pub = curve25519_pub_to_base64 curve25519_keypair.public in 418 + let keys = [ 419 + (Printf.sprintf "ed25519:%s" device_id, ed25519_pub); 420 + (Printf.sprintf "curve25519:%s" device_id, curve25519_pub); 421 + ] in 422 + let algorithms = [ 423 + "m.olm.v1.curve25519-aes-sha2-256"; 424 + "m.megolm.v1.aes-sha2-256"; 425 + ] in 426 + (* Create unsigned JSON for signing *) 427 + let unsigned_json = { 428 + user_id = user_str; 429 + device_id; 430 + algorithms; 431 + keys; 432 + signatures = []; 433 + } in 434 + match Client.encode_body device_keys_json_jsont unsigned_json with 435 + | Error _ -> Error "Failed to encode device keys" 436 + | Ok json_str -> 437 + let signature = sign_json ed25519_keypair.priv json_str in 438 + let signatures = [ 439 + (user_str, [(Printf.sprintf "ed25519:%s" device_id, signature)]) 440 + ] in 441 + Ok { 442 + user_id = user_str; 443 + device_id; 444 + algorithms; 445 + keys; 446 + signatures; 447 + }
+227
lib/matrix_client/keys.mli
··· 1 + (** E2EE key management for Matrix. 2 + 3 + This module handles device keys, one-time keys, and key exchange 4 + for end-to-end encryption using Olm/Megolm protocols. 5 + 6 + Uses Ed25519 for signing and Curve25519 (X25519) for key exchange. *) 7 + 8 + (** {1 Key Types} *) 9 + 10 + (** Ed25519 key pair for signing. *) 11 + type ed25519_keypair = { 12 + priv : Mirage_crypto_ec.Ed25519.priv; 13 + pub : Mirage_crypto_ec.Ed25519.pub; 14 + } 15 + 16 + (** Curve25519 key pair for key exchange. *) 17 + type curve25519_keypair = { 18 + secret : Mirage_crypto_ec.X25519.secret; 19 + public : string; 20 + } 21 + 22 + (** Device keys structure. *) 23 + type device_keys = { 24 + user_id : Matrix_proto.Id.User_id.t; 25 + device_id : string; 26 + algorithms : string list; 27 + ed25519_key : string; 28 + curve25519_key : string; 29 + signatures : (string * (string * string) list) list; 30 + } 31 + 32 + (** One-time key for Olm session establishment. *) 33 + type one_time_key = { 34 + key_id : string; 35 + key : string; 36 + signature : string option; 37 + } 38 + 39 + (** Fallback key (used when no one-time keys available). *) 40 + type fallback_key = { 41 + key_id : string; 42 + key : string; 43 + signature : string option; 44 + } 45 + 46 + (** {1 Key Generation} *) 47 + 48 + (** Generate a new Ed25519 key pair for signing. *) 49 + val generate_ed25519 : unit -> ed25519_keypair 50 + 51 + (** Generate a new Curve25519 key pair for key exchange. *) 52 + val generate_curve25519 : unit -> curve25519_keypair 53 + 54 + (** Generate a batch of one-time keys. 55 + 56 + @param count Number of keys to generate. 57 + @param sign_with Optional Ed25519 private key to sign the keys. 58 + @return List of (one_time_key, curve25519_keypair) tuples. *) 59 + val generate_one_time_keys : 60 + count:int -> 61 + ?sign_with:Mirage_crypto_ec.Ed25519.priv -> 62 + unit -> 63 + (one_time_key * curve25519_keypair) list 64 + 65 + (** {1 Key Serialization} *) 66 + 67 + (** Encode Ed25519 public key to unpadded base64. *) 68 + val ed25519_pub_to_base64 : Mirage_crypto_ec.Ed25519.pub -> string 69 + 70 + (** Encode Ed25519 private key to unpadded base64. *) 71 + val ed25519_priv_to_base64 : Mirage_crypto_ec.Ed25519.priv -> string 72 + 73 + (** Encode Curve25519 public key to unpadded base64. *) 74 + val curve25519_pub_to_base64 : string -> string 75 + 76 + (** Encode Curve25519 secret key to unpadded base64. *) 77 + val curve25519_secret_to_base64 : Mirage_crypto_ec.X25519.secret -> string 78 + 79 + (** Decode Ed25519 public key from base64. *) 80 + val ed25519_pub_of_base64 : string -> (Mirage_crypto_ec.Ed25519.pub, string) result 81 + 82 + (** Decode Ed25519 private key from base64. *) 83 + val ed25519_priv_of_base64 : string -> (Mirage_crypto_ec.Ed25519.priv, string) result 84 + 85 + (** Decode Curve25519 public key from base64. *) 86 + val curve25519_pub_of_base64 : string -> (string, string) result 87 + 88 + (** Decode Curve25519 secret key from base64. *) 89 + val curve25519_secret_of_base64 : string -> (Mirage_crypto_ec.X25519.secret, string) result 90 + 91 + (** {1 Signing and Verification} *) 92 + 93 + (** Sign a JSON string with Ed25519. 94 + 95 + @return Base64-encoded signature. *) 96 + val sign_json : Mirage_crypto_ec.Ed25519.priv -> string -> string 97 + 98 + (** Verify an Ed25519 signature. 99 + 100 + @param signature_b64 Base64-encoded signature. 101 + @return true if signature is valid. *) 102 + val verify_signature : Mirage_crypto_ec.Ed25519.pub -> string -> string -> bool 103 + 104 + (** {1 Key Exchange} *) 105 + 106 + (** Perform Curve25519 key exchange. 107 + 108 + @param secret Our secret key. 109 + @param their_public Their public key (raw bytes). 110 + @return Shared secret or error. *) 111 + val key_exchange : 112 + secret:Mirage_crypto_ec.X25519.secret -> 113 + their_public:string -> 114 + (string, string) result 115 + 116 + (** {1 Key Upload} *) 117 + 118 + (** Device keys in JSON format for upload. *) 119 + type device_keys_json = { 120 + user_id : string; 121 + device_id : string; 122 + algorithms : string list; 123 + keys : (string * string) list; 124 + signatures : (string * (string * string) list) list; 125 + } 126 + 127 + (** One-time key in JSON format. *) 128 + type one_time_key_json = { 129 + key : string; 130 + signatures : (string * (string * string) list) list option; 131 + } 132 + 133 + (** Response from key upload. *) 134 + type upload_keys_response = { 135 + one_time_key_counts : (string * int) list; 136 + } 137 + 138 + (** Upload device keys and/or one-time keys. 139 + 140 + @param device_keys Device identity keys (only needed once). 141 + @param one_time_keys One-time keys for Olm sessions. 142 + @param fallback_keys Fallback keys when one-time keys exhausted. *) 143 + val upload_keys : 144 + Client.t -> 145 + ?device_keys:device_keys_json -> 146 + ?one_time_keys:(string * one_time_key_json) list -> 147 + ?fallback_keys:(string * one_time_key_json) list -> 148 + unit -> 149 + (upload_keys_response, Error.t) result 150 + 151 + (** {1 Key Query} *) 152 + 153 + (** Queried device keys. *) 154 + type queried_device_keys = { 155 + user_id : string; 156 + device_id : string; 157 + algorithms : string list; 158 + keys : (string * string) list; 159 + signatures : (string * (string * string) list) list; 160 + unsigned : Jsont.json option; 161 + } 162 + 163 + (** Response from key query. *) 164 + type query_keys_response = { 165 + failures : (string * Jsont.json) list; 166 + device_keys : (string * (string * queried_device_keys) list) list; 167 + } 168 + 169 + (** Query device keys for users. 170 + 171 + @param timeout Request timeout in milliseconds. 172 + @param users List of (user_id, device_ids) pairs. Empty device_ids means all. *) 173 + val query_keys : 174 + Client.t -> 175 + ?timeout:int -> 176 + users:(Matrix_proto.Id.User_id.t * string list) list -> 177 + unit -> 178 + (query_keys_response, Error.t) result 179 + 180 + (** {1 Key Claiming} *) 181 + 182 + (** Response from key claim. *) 183 + type claim_keys_response = { 184 + failures : (string * Jsont.json) list; 185 + one_time_keys : (string * (string * (string * one_time_key_json) list) list) list; 186 + } 187 + 188 + (** Claim one-time keys for establishing Olm sessions. 189 + 190 + @param timeout Request timeout in milliseconds. 191 + @param keys List of (user_id, [(device_id, algorithm)]) pairs. *) 192 + val claim_keys : 193 + Client.t -> 194 + ?timeout:int -> 195 + keys:(Matrix_proto.Id.User_id.t * (string * string) list) list -> 196 + unit -> 197 + (claim_keys_response, Error.t) result 198 + 199 + (** {1 Key Changes} *) 200 + 201 + (** Response from key changes query. *) 202 + type key_changes_response = { 203 + changed : string list; (** User IDs with changed keys. *) 204 + left : string list; (** User IDs who left rooms we're in. *) 205 + } 206 + 207 + (** Get users whose keys have changed between sync tokens. 208 + 209 + @param from Start sync token. 210 + @param to_ End sync token. *) 211 + val get_key_changes : 212 + Client.t -> 213 + from:string -> 214 + to_:string -> 215 + (key_changes_response, Error.t) result 216 + 217 + (** {1 Helpers} *) 218 + 219 + (** Create signed device keys for upload. 220 + 221 + @return Device keys JSON ready for upload, or error. *) 222 + val create_device_keys : 223 + user_id:Matrix_proto.Id.User_id.t -> 224 + device_id:string -> 225 + ed25519_keypair:ed25519_keypair -> 226 + curve25519_keypair:curve25519_keypair -> 227 + (device_keys_json, string) result
+105
lib/matrix_client/matrix_client.ml
··· 1 + (** Matrix Client SDK for OCaml. 2 + 3 + This library provides a full Matrix client implementation using 4 + the requests library for HTTP and jsont for JSON encoding/decoding. 5 + 6 + {2 Core Modules} 7 + 8 + - {!module:Client} - HTTP client and session management 9 + - {!module:Auth} - Authentication (login, logout, registration) 10 + - {!module:Sync} - Traditional /sync endpoint 11 + 12 + {2 Room Operations} 13 + 14 + - {!module:Rooms} - Room creation, joining, leaving 15 + - {!module:Messages} - Sending and receiving messages 16 + - {!module:State} - Room state management 17 + - {!module:Relations} - Reactions, edits, threads, replies 18 + 19 + {2 User Features} 20 + 21 + - {!module:Profile} - User profile management 22 + - {!module:Presence} - Online/offline status 23 + - {!module:Typing} - Typing indicators 24 + - {!module:Receipts} - Read receipts 25 + - {!module:Account} - Account settings 26 + 27 + {2 Media & Devices} 28 + 29 + - {!module:Media} - Upload/download media 30 + - {!module:Devices} - Device management 31 + 32 + {2 Discovery} 33 + 34 + - {!module:Directory} - Room directory 35 + - {!module:Room_preview} - Room preview and public rooms 36 + 37 + {2 End-to-End Encryption} 38 + 39 + - {!module:Keys} - Key management for E2EE 40 + 41 + {2 Advanced Features} 42 + 43 + - {!module:Spaces} - Matrix spaces (MSC1772) 44 + - {!module:Sliding_sync} - Sliding sync protocol (MSC3575) 45 + - {!module:Push} - Push notifications and rules 46 + - {!module:Calls} - VoIP signaling *) 47 + 48 + (** {1 Core} *) 49 + 50 + module Error = Error 51 + module Client = Client 52 + module Auth = Auth 53 + module Sync = Sync 54 + 55 + (** {1 Rooms} *) 56 + 57 + module Rooms = Rooms 58 + module Messages = Messages 59 + module State = State 60 + module Relations = Relations 61 + 62 + (** {1 User Features} *) 63 + 64 + module Profile = Profile 65 + module Typing = Typing 66 + module Receipts = Receipts 67 + module Account = Account 68 + module Presence = Presence 69 + 70 + (** {1 Media & Devices} *) 71 + 72 + module Media = Media 73 + module Devices = Devices 74 + 75 + (** {1 Discovery} *) 76 + 77 + module Directory = Directory 78 + module Room_preview = Room_preview 79 + 80 + (** {1 End-to-End Encryption} *) 81 + 82 + module Keys = Keys 83 + module Olm = Olm 84 + module Verification = Verification 85 + module Backup = Backup 86 + 87 + (** {1 Storage & Caching} *) 88 + 89 + module Store = Store 90 + module Timeline = Timeline 91 + 92 + (** {1 User-Interactive Auth} *) 93 + 94 + module Uiaa = Uiaa 95 + 96 + (** {1 Offline Support} *) 97 + 98 + module Send_queue = Send_queue 99 + 100 + (** {1 Advanced Features} *) 101 + 102 + module Spaces = Spaces 103 + module Sliding_sync = Sliding_sync 104 + module Push = Push 105 + module Calls = Calls
+118
lib/matrix_client/media.ml
··· 1 + (** Media operations. *) 2 + 3 + (* Upload uses a different API base *) 4 + let media_api_base_v3 = "/_matrix/media/v3" 5 + 6 + (* Upload response - for future use when binary upload is implemented *) 7 + type upload_response = { 8 + content_uri : string; 9 + } [@@warning "-69"] 10 + 11 + let _upload_response_jsont = 12 + Jsont.Object.( 13 + map (fun content_uri -> { content_uri }) 14 + |> mem "content_uri" Jsont.string 15 + |> finish) 16 + 17 + let upload client ~content_type:_ ~data:_ ?filename:_ () = 18 + (* Note: This is a simplified version - actual implementation would need 19 + direct access to Requests to send raw binary data with custom content-type *) 20 + match Client.access_token client with 21 + | None -> Error (Error.Network_error "Not logged in") 22 + | Some _ -> 23 + (* For now, we'll use the JSON helpers but this would need to be 24 + a raw HTTP request in a full implementation *) 25 + Error (Error.Network_error "Binary upload not yet implemented - use Requests directly") 26 + 27 + (* Download uses media API *) 28 + let download client ~server_name ~media_id = 29 + let homeserver = Client.homeserver client in 30 + let path = Printf.sprintf "%s/download/%s/%s" 31 + media_api_base_v3 32 + (Uri.pct_encode server_name) 33 + (Uri.pct_encode media_id) 34 + in 35 + let uri = Uri.with_path homeserver path in 36 + let url = Uri.to_string uri in 37 + (* Similar limitation as upload - needs raw HTTP access *) 38 + Error (Error.Network_error ("Download not yet implemented: " ^ url)) 39 + 40 + (* Thumbnail *) 41 + let thumbnail client ~server_name ~media_id ~width ~height ?method_ () = 42 + let homeserver = Client.homeserver client in 43 + let path = Printf.sprintf "%s/thumbnail/%s/%s" 44 + media_api_base_v3 45 + (Uri.pct_encode server_name) 46 + (Uri.pct_encode media_id) 47 + in 48 + let query = [ 49 + ("width", string_of_int width); 50 + ("height", string_of_int height); 51 + ] @ (match method_ with Some m -> [("method", m)] | None -> []) 52 + in 53 + let uri = Uri.with_path homeserver path in 54 + let uri = Uri.with_query' uri query in 55 + let url = Uri.to_string uri in 56 + Error (Error.Network_error ("Thumbnail not yet implemented: " ^ url)) 57 + 58 + (* Parse mxc:// URI *) 59 + let parse_mxc mxc = 60 + if not (String.starts_with ~prefix:"mxc://" mxc) then 61 + None 62 + else 63 + let rest = String.sub mxc 6 (String.length mxc - 6) in 64 + match String.index_opt rest '/' with 65 + | None -> None 66 + | Some i -> 67 + let server_name = String.sub rest 0 i in 68 + let media_id = String.sub rest (i + 1) (String.length rest - i - 1) in 69 + Some (server_name, media_id) 70 + 71 + (* Convert mxc:// to HTTP URL *) 72 + let mxc_to_http client ~mxc ?width ?height () = 73 + match parse_mxc mxc with 74 + | None -> None 75 + | Some (server_name, media_id) -> 76 + let homeserver = Client.homeserver client in 77 + let path, query = match width, height with 78 + | Some w, Some h -> 79 + let path = Printf.sprintf "%s/thumbnail/%s/%s" 80 + media_api_base_v3 81 + (Uri.pct_encode server_name) 82 + (Uri.pct_encode media_id) 83 + in 84 + path, [("width", string_of_int w); ("height", string_of_int h)] 85 + | _ -> 86 + let path = Printf.sprintf "%s/download/%s/%s" 87 + media_api_base_v3 88 + (Uri.pct_encode server_name) 89 + (Uri.pct_encode media_id) 90 + in 91 + path, [] 92 + in 93 + let uri = Uri.with_path homeserver path in 94 + let uri = if query = [] then uri else Uri.with_query' uri query in 95 + Some (Uri.to_string uri) 96 + 97 + (* Configuration *) 98 + type config = { 99 + upload_size : int option; 100 + } 101 + 102 + let config_jsont = 103 + Jsont.Object.( 104 + map (fun upload_size -> { upload_size }) 105 + |> opt_mem "m.upload.size" Jsont.int ~enc:(fun t -> t.upload_size) 106 + |> finish) 107 + 108 + let get_config client = 109 + let homeserver = Client.homeserver client in 110 + let path = media_api_base_v3 ^ "/config" in 111 + let uri = Uri.with_path homeserver path in 112 + let url = Uri.to_string uri in 113 + match Client.get client ~path:"/media/v3/config" () with 114 + | Error (Error.Http_error { status = 404; _ }) -> 115 + (* Try without the _matrix prefix that get adds *) 116 + Error (Error.Network_error ("Media config not found: " ^ url)) 117 + | Error e -> Error e 118 + | Ok body -> Client.decode_response config_jsont body
+75
lib/matrix_client/media.mli
··· 1 + (** Media operations (upload, download, thumbnails). *) 2 + 3 + (** {1 Upload} *) 4 + 5 + (** Upload media content. 6 + 7 + Returns the mxc:// URI of the uploaded content. 8 + 9 + @param content_type MIME type of the content. 10 + @param data The raw content bytes. 11 + @param filename Optional filename for the content. *) 12 + val upload : 13 + Client.t -> 14 + content_type:string -> 15 + data:string -> 16 + ?filename:string -> 17 + unit -> 18 + (string, Error.t) result 19 + 20 + (** {1 Download} *) 21 + 22 + (** Download media content. 23 + 24 + Returns (data, content_type). 25 + 26 + @param server_name The server name from the mxc:// URI. 27 + @param media_id The media ID from the mxc:// URI. *) 28 + val download : 29 + Client.t -> 30 + server_name:string -> 31 + media_id:string -> 32 + (string * string, Error.t) result 33 + 34 + (** Download a thumbnail. 35 + 36 + @param width Desired width. 37 + @param height Desired height. 38 + @param method_ Resize method: "crop" or "scale". *) 39 + val thumbnail : 40 + Client.t -> 41 + server_name:string -> 42 + media_id:string -> 43 + width:int -> 44 + height:int -> 45 + ?method_:string -> 46 + unit -> 47 + (string * string, Error.t) result 48 + 49 + (** {1 MXC URI Helpers} *) 50 + 51 + (** Parse an mxc:// URI into (server_name, media_id). *) 52 + val parse_mxc : string -> (string * string) option 53 + 54 + (** Convert an mxc:// URI to an HTTP(S) URL. 55 + 56 + @param width Optional width for thumbnail. 57 + @param height Optional height for thumbnail. *) 58 + val mxc_to_http : 59 + Client.t -> 60 + mxc:string -> 61 + ?width:int -> 62 + ?height:int -> 63 + unit -> 64 + string option 65 + 66 + (** {1 Configuration} *) 67 + 68 + (** Media upload limits. *) 69 + type config = { 70 + upload_size : int option; 71 + (** Maximum upload size in bytes, if any. *) 72 + } 73 + 74 + (** Get media configuration from the homeserver. *) 75 + val get_config : Client.t -> (config, Error.t) result
+232
lib/matrix_client/messages.ml
··· 1 + (** Message sending and retrieval. *) 2 + 3 + (* Transaction ID generator *) 4 + let next_txn_id = 5 + let counter = ref 0 in 6 + fun () -> 7 + incr counter; 8 + Printf.sprintf "%d_%f" !counter (Unix.gettimeofday ()) 9 + 10 + (* Send event response *) 11 + type send_response = { 12 + event_id : Matrix_proto.Id.Event_id.t; 13 + } 14 + 15 + let send_response_jsont = 16 + Jsont.Object.( 17 + map (fun event_id -> { event_id }) 18 + |> mem "event_id" Matrix_proto.Id.Event_id.jsont 19 + |> finish) 20 + 21 + (* Generic send event *) 22 + let send_event client ~room_id ~event_type ~content = 23 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 24 + let txn_id = next_txn_id () in 25 + let path = Printf.sprintf "/rooms/%s/send/%s/%s" 26 + (Uri.pct_encode room_id_str) 27 + (Uri.pct_encode event_type) 28 + (Uri.pct_encode txn_id) 29 + in 30 + match Client.encode_body Jsont.json content with 31 + | Error e -> Error e 32 + | Ok body -> 33 + match Client.put client ~path ~body () with 34 + | Error e -> Error e 35 + | Ok body -> 36 + match Client.decode_response send_response_jsont body with 37 + | Error e -> Error e 38 + | Ok resp -> Ok resp.event_id 39 + 40 + (* Text message content *) 41 + type text_content = { 42 + msgtype : string; 43 + body : string; 44 + format : string option; 45 + formatted_body : string option; 46 + } [@@warning "-69"] 47 + 48 + let text_content_jsont = 49 + Jsont.Object.( 50 + map (fun msgtype body format formatted_body -> 51 + { msgtype; body; format; formatted_body }) 52 + |> mem "msgtype" Jsont.string 53 + |> mem "body" Jsont.string 54 + |> opt_mem "format" Jsont.string ~enc:(fun t -> t.format) 55 + |> opt_mem "formatted_body" Jsont.string ~enc:(fun t -> t.formatted_body) 56 + |> finish) 57 + 58 + let send_text client ~room_id ~body ?format ?formatted_body () = 59 + let content = { msgtype = "m.text"; body; format; formatted_body } in 60 + match Client.encode_body text_content_jsont content with 61 + | Error e -> Error e 62 + | Ok json_str -> 63 + match Client.decode_response Jsont.json json_str with 64 + | Error e -> Error e 65 + | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json 66 + 67 + let send_emote client ~room_id ~body () = 68 + let content = { msgtype = "m.emote"; body; format = None; formatted_body = None } in 69 + match Client.encode_body text_content_jsont content with 70 + | Error e -> Error e 71 + | Ok json_str -> 72 + match Client.decode_response Jsont.json json_str with 73 + | Error e -> Error e 74 + | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json 75 + 76 + let send_notice client ~room_id ~body () = 77 + let content = { msgtype = "m.notice"; body; format = None; formatted_body = None } in 78 + match Client.encode_body text_content_jsont content with 79 + | Error e -> Error e 80 + | Ok json_str -> 81 + match Client.decode_response Jsont.json json_str with 82 + | Error e -> Error e 83 + | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json 84 + 85 + (* Media message content *) 86 + type media_content = { 87 + msgtype : string; 88 + body : string; 89 + url : string; 90 + info : Jsont.json option; 91 + } [@@warning "-69"] 92 + 93 + let media_content_jsont = 94 + Jsont.Object.( 95 + map (fun msgtype body url info -> 96 + { msgtype; body; url; info }) 97 + |> mem "msgtype" Jsont.string 98 + |> mem "body" Jsont.string 99 + |> mem "url" Jsont.string 100 + |> opt_mem "info" Jsont.json ~enc:(fun t -> t.info) 101 + |> finish) 102 + 103 + let send_image client ~room_id ~body ~url ?info () = 104 + let content = { msgtype = "m.image"; body; url; info } in 105 + match Client.encode_body media_content_jsont content with 106 + | Error e -> Error e 107 + | Ok json_str -> 108 + match Client.decode_response Jsont.json json_str with 109 + | Error e -> Error e 110 + | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json 111 + 112 + let send_file client ~room_id ~body ~url ?info () = 113 + let content = { msgtype = "m.file"; body; url; info } in 114 + match Client.encode_body media_content_jsont content with 115 + | Error e -> Error e 116 + | Ok json_str -> 117 + match Client.decode_response Jsont.json json_str with 118 + | Error e -> Error e 119 + | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json 120 + 121 + (* Redaction *) 122 + type redact_request = { 123 + reason : string option; 124 + } [@@warning "-69"] 125 + 126 + let redact_request_jsont = 127 + Jsont.Object.( 128 + map (fun reason -> { reason }) 129 + |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason) 130 + |> finish) 131 + 132 + let redact client ~room_id ~event_id ?reason () = 133 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 134 + let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in 135 + let txn_id = next_txn_id () in 136 + let path = Printf.sprintf "/rooms/%s/redact/%s/%s" 137 + (Uri.pct_encode room_id_str) 138 + (Uri.pct_encode event_id_str) 139 + (Uri.pct_encode txn_id) 140 + in 141 + let request = { reason } in 142 + match Client.encode_body redact_request_jsont request with 143 + | Error e -> Error e 144 + | Ok body -> 145 + match Client.put client ~path ~body () with 146 + | Error e -> Error e 147 + | Ok body -> 148 + match Client.decode_response send_response_jsont body with 149 + | Error e -> Error e 150 + | Ok resp -> Ok resp.event_id 151 + 152 + (* Get messages *) 153 + type direction = Forward | Backward 154 + 155 + type messages_response = { 156 + start : string; 157 + end_ : string option; 158 + chunk : Matrix_proto.Event.Raw_event.t list; 159 + state : Matrix_proto.Event.Raw_event.t list; 160 + } 161 + 162 + let messages_response_jsont = 163 + Jsont.Object.( 164 + map (fun start end_ chunk state -> 165 + { start; end_; chunk; state }) 166 + |> mem "start" Jsont.string 167 + |> opt_mem "end" Jsont.string ~enc:(fun t -> t.end_) 168 + |> mem "chunk" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[] 169 + |> mem "state" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[] 170 + |> finish) 171 + 172 + let get_messages client ~room_id ~from ~dir ?limit ?filter () = 173 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 174 + let path = Printf.sprintf "/rooms/%s/messages" (Uri.pct_encode room_id_str) in 175 + let dir_str = match dir with Forward -> "f" | Backward -> "b" in 176 + let query = 177 + [("from", from); ("dir", dir_str)] 178 + |> (fun q -> match limit with Some l -> ("limit", string_of_int l) :: q | None -> q) 179 + |> (fun q -> match filter with Some f -> ("filter", f) :: q | None -> q) 180 + in 181 + match Client.get client ~path ~query () with 182 + | Error e -> Error e 183 + | Ok body -> Client.decode_response messages_response_jsont body 184 + 185 + (* Get single event *) 186 + let get_event client ~room_id ~event_id = 187 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 188 + let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in 189 + let path = Printf.sprintf "/rooms/%s/event/%s" 190 + (Uri.pct_encode room_id_str) 191 + (Uri.pct_encode event_id_str) 192 + in 193 + match Client.get client ~path () with 194 + | Error e -> Error e 195 + | Ok body -> Client.decode_response Matrix_proto.Event.Raw_event.jsont body 196 + 197 + (* Get context *) 198 + type context = { 199 + start : string; 200 + end_ : string; 201 + event : Matrix_proto.Event.Raw_event.t; 202 + events_before : Matrix_proto.Event.Raw_event.t list; 203 + events_after : Matrix_proto.Event.Raw_event.t list; 204 + state : Matrix_proto.Event.Raw_event.t list; 205 + } 206 + 207 + let context_jsont = 208 + Jsont.Object.( 209 + map (fun start end_ event events_before events_after state -> 210 + { start; end_; event; events_before; events_after; state }) 211 + |> mem "start" Jsont.string 212 + |> mem "end" Jsont.string 213 + |> mem "event" Matrix_proto.Event.Raw_event.jsont 214 + |> mem "events_before" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[] 215 + |> mem "events_after" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[] 216 + |> mem "state" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[] 217 + |> finish) 218 + 219 + let get_context client ~room_id ~event_id ?limit () = 220 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 221 + let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in 222 + let path = Printf.sprintf "/rooms/%s/context/%s" 223 + (Uri.pct_encode room_id_str) 224 + (Uri.pct_encode event_id_str) 225 + in 226 + let query = match limit with 227 + | Some l -> Some [("limit", string_of_int l)] 228 + | None -> None 229 + in 230 + match Client.get client ~path ?query () with 231 + | Error e -> Error e 232 + | Ok body -> Client.decode_response context_jsont body
+146
lib/matrix_client/messages.mli
··· 1 + (** Message sending and retrieval. *) 2 + 3 + (** {1 Sending Messages} *) 4 + 5 + (** Response from sending an event. *) 6 + type send_response = { 7 + event_id : Matrix_proto.Id.Event_id.t; 8 + } 9 + 10 + (** JSON codec for send_response. *) 11 + val send_response_jsont : send_response Jsont.t 12 + 13 + (** Send a text message. 14 + 15 + @param format Optional format (e.g., "org.matrix.custom.html"). 16 + @param formatted_body HTML body when format is set. *) 17 + val send_text : 18 + Client.t -> 19 + room_id:Matrix_proto.Id.Room_id.t -> 20 + body:string -> 21 + ?format:string -> 22 + ?formatted_body:string -> 23 + unit -> 24 + (Matrix_proto.Id.Event_id.t, Error.t) result 25 + 26 + (** Send an emote message (like /me in IRC). *) 27 + val send_emote : 28 + Client.t -> 29 + room_id:Matrix_proto.Id.Room_id.t -> 30 + body:string -> 31 + unit -> 32 + (Matrix_proto.Id.Event_id.t, Error.t) result 33 + 34 + (** Send a notice message (bot/automated messages). *) 35 + val send_notice : 36 + Client.t -> 37 + room_id:Matrix_proto.Id.Room_id.t -> 38 + body:string -> 39 + unit -> 40 + (Matrix_proto.Id.Event_id.t, Error.t) result 41 + 42 + (** Send an image message. 43 + 44 + @param url The mxc:// URL of the uploaded image. 45 + @param info Optional image info (width, height, size, mimetype). *) 46 + val send_image : 47 + Client.t -> 48 + room_id:Matrix_proto.Id.Room_id.t -> 49 + body:string -> 50 + url:string -> 51 + ?info:Jsont.json -> 52 + unit -> 53 + (Matrix_proto.Id.Event_id.t, Error.t) result 54 + 55 + (** Send a file message. 56 + 57 + @param url The mxc:// URL of the uploaded file. *) 58 + val send_file : 59 + Client.t -> 60 + room_id:Matrix_proto.Id.Room_id.t -> 61 + body:string -> 62 + url:string -> 63 + ?info:Jsont.json -> 64 + unit -> 65 + (Matrix_proto.Id.Event_id.t, Error.t) result 66 + 67 + (** Send a generic room event. 68 + 69 + @param event_type The event type (e.g., "m.room.message"). 70 + @param content The event content as JSON. *) 71 + val send_event : 72 + Client.t -> 73 + room_id:Matrix_proto.Id.Room_id.t -> 74 + event_type:string -> 75 + content:Jsont.json -> 76 + (Matrix_proto.Id.Event_id.t, Error.t) result 77 + 78 + (** {1 Redaction} *) 79 + 80 + (** Redact an event. 81 + 82 + @param reason Optional reason for the redaction. *) 83 + val redact : 84 + Client.t -> 85 + room_id:Matrix_proto.Id.Room_id.t -> 86 + event_id:Matrix_proto.Id.Event_id.t -> 87 + ?reason:string -> 88 + unit -> 89 + (Matrix_proto.Id.Event_id.t, Error.t) result 90 + 91 + (** {1 Retrieving Messages} *) 92 + 93 + (** Direction for message retrieval. *) 94 + type direction = Forward | Backward 95 + 96 + (** Messages response. *) 97 + type messages_response = { 98 + start : string; 99 + end_ : string option; 100 + chunk : Matrix_proto.Event.Raw_event.t list; 101 + state : Matrix_proto.Event.Raw_event.t list; 102 + } 103 + 104 + (** Get messages from a room. 105 + 106 + @param from Pagination token to start from. 107 + @param dir Direction to paginate. 108 + @param limit Maximum number of events to return. 109 + @param filter Event filter (as filter ID or JSON). *) 110 + val get_messages : 111 + Client.t -> 112 + room_id:Matrix_proto.Id.Room_id.t -> 113 + from:string -> 114 + dir:direction -> 115 + ?limit:int -> 116 + ?filter:string -> 117 + unit -> 118 + (messages_response, Error.t) result 119 + 120 + (** Get a single event by ID. *) 121 + val get_event : 122 + Client.t -> 123 + room_id:Matrix_proto.Id.Room_id.t -> 124 + event_id:Matrix_proto.Id.Event_id.t -> 125 + (Matrix_proto.Event.Raw_event.t, Error.t) result 126 + 127 + (** Context around an event. *) 128 + type context = { 129 + start : string; 130 + end_ : string; 131 + event : Matrix_proto.Event.Raw_event.t; 132 + events_before : Matrix_proto.Event.Raw_event.t list; 133 + events_after : Matrix_proto.Event.Raw_event.t list; 134 + state : Matrix_proto.Event.Raw_event.t list; 135 + } 136 + 137 + (** Get context around an event. 138 + 139 + @param limit Number of events to return before and after. *) 140 + val get_context : 141 + Client.t -> 142 + room_id:Matrix_proto.Id.Room_id.t -> 143 + event_id:Matrix_proto.Id.Event_id.t -> 144 + ?limit:int -> 145 + unit -> 146 + (context, Error.t) result
+895
lib/matrix_client/olm.ml
··· 1 + (** Olm/Megolm cryptographic session management. 2 + 3 + This module implements the Olm double-ratchet algorithm for encrypted 4 + to-device messages, and Megolm for encrypted room messages. *) 5 + 6 + module Ed25519 = Mirage_crypto_ec.Ed25519 7 + module X25519 = Mirage_crypto_ec.X25519 8 + 9 + (* Base64 encoding/decoding - Matrix uses unpadded base64 *) 10 + let base64_encode s = Base64.encode_string ~pad:false s 11 + let base64_decode s = Base64.decode ~pad:false s 12 + 13 + (** Olm account - manages identity keys and one-time keys. 14 + 15 + An Olm account contains: 16 + - Ed25519 identity key (for signing) 17 + - Curve25519 identity key (for key exchange) 18 + - One-time keys (for session establishment) *) 19 + module Account = struct 20 + type t = { 21 + (* Identity keys *) 22 + ed25519_priv : Ed25519.priv; 23 + ed25519_pub : Ed25519.pub; 24 + curve25519_secret : X25519.secret; 25 + curve25519_public : string; 26 + (* One-time keys - key_id -> (secret, public) *) 27 + mutable one_time_keys : (string * (X25519.secret * string)) list; 28 + (* Fallback keys *) 29 + mutable fallback_key : (string * (X25519.secret * string)) option; 30 + (* Key counter for generating key IDs *) 31 + mutable next_key_id : int; 32 + (* Max number of one-time keys to store *) 33 + max_one_time_keys : int; 34 + } 35 + 36 + (** Generate a new Olm account with fresh identity keys. *) 37 + let create () = 38 + let ed25519_priv, ed25519_pub = Ed25519.generate () in 39 + let curve25519_secret, curve25519_public = X25519.gen_key () in 40 + { 41 + ed25519_priv; 42 + ed25519_pub; 43 + curve25519_secret; 44 + curve25519_public; 45 + one_time_keys = []; 46 + fallback_key = None; 47 + next_key_id = 0; 48 + max_one_time_keys = 100; 49 + } 50 + 51 + (** Get the Ed25519 identity key as base64. *) 52 + let ed25519_key t = 53 + Ed25519.pub_to_octets t.ed25519_pub |> base64_encode 54 + 55 + (** Get the Curve25519 identity key as base64. *) 56 + let curve25519_key t = 57 + base64_encode t.curve25519_public 58 + 59 + (** Get the identity keys as a pair (ed25519, curve25519). *) 60 + let identity_keys t = 61 + (ed25519_key t, curve25519_key t) 62 + 63 + (** Sign a message with the account's Ed25519 key. *) 64 + let sign t message = 65 + let signature = Ed25519.sign ~key:t.ed25519_priv message in 66 + base64_encode signature 67 + 68 + (** Generate a unique key ID. *) 69 + let generate_key_id t = 70 + let id = Printf.sprintf "AAAA%02dAA" t.next_key_id in 71 + t.next_key_id <- t.next_key_id + 1; 72 + id 73 + 74 + (** Generate new one-time keys. *) 75 + let generate_one_time_keys t count = 76 + let count = min count (t.max_one_time_keys - List.length t.one_time_keys) in 77 + for _ = 1 to count do 78 + let secret, public = X25519.gen_key () in 79 + let key_id = generate_key_id t in 80 + t.one_time_keys <- (key_id, (secret, public)) :: t.one_time_keys 81 + done 82 + 83 + (** Get one-time keys for upload (key_id -> public_key). *) 84 + let one_time_keys t = 85 + List.map (fun (key_id, (_secret, public)) -> 86 + (key_id, base64_encode public) 87 + ) t.one_time_keys 88 + 89 + (** Get signed one-time keys for upload. *) 90 + let signed_one_time_keys t = 91 + List.map (fun (key_id, (_secret, public)) -> 92 + let pub_b64 = base64_encode public in 93 + let to_sign = Printf.sprintf {|{"key":"%s"}|} pub_b64 in 94 + let signature = sign t to_sign in 95 + (key_id, pub_b64, signature) 96 + ) t.one_time_keys 97 + 98 + (** Mark one-time keys as published (remove them from pending). *) 99 + let mark_keys_as_published _t = 100 + (* One-time keys are kept until used in a session *) 101 + () 102 + 103 + (** Generate a fallback key. *) 104 + let generate_fallback_key t = 105 + let secret, public = X25519.gen_key () in 106 + let key_id = generate_key_id t in 107 + t.fallback_key <- Some (key_id, (secret, public)) 108 + 109 + (** Get the fallback key if one exists. *) 110 + let fallback_key t = 111 + match t.fallback_key with 112 + | Some (key_id, (_secret, public)) -> Some (key_id, base64_encode public) 113 + | None -> None 114 + 115 + (** Remove a one-time key by ID (after session creation). *) 116 + let remove_one_time_key t key_id = 117 + t.one_time_keys <- List.filter (fun (id, _) -> id <> key_id) t.one_time_keys 118 + 119 + (** Get the secret for a one-time key (for session creation). *) 120 + let get_one_time_key_secret t key_id = 121 + match List.assoc_opt key_id t.one_time_keys with 122 + | Some (secret, _public) -> Some secret 123 + | None -> 124 + (* Check fallback key *) 125 + match t.fallback_key with 126 + | Some (id, (secret, _public)) when id = key_id -> Some secret 127 + | _ -> None 128 + 129 + (** Number of unpublished one-time keys. *) 130 + let one_time_keys_count t = List.length t.one_time_keys 131 + 132 + (** Maximum number of one-time keys this account can hold. *) 133 + let max_one_time_keys t = t.max_one_time_keys 134 + end 135 + 136 + (** Olm session state for the double ratchet algorithm. *) 137 + module Session = struct 138 + (** Ratchet chain key *) 139 + type chain_key = { 140 + key : string; (* 32 bytes *) 141 + index : int; 142 + } 143 + 144 + (** Root key used for deriving new chain keys *) 145 + type root_key = string (* 32 bytes *) 146 + 147 + (** Message key for encrypting a single message *) 148 + type message_key = string (* 32 bytes *) 149 + 150 + (** Session state *) 151 + type t = { 152 + (* Session ID *) 153 + session_id : string; 154 + (* Their identity key (Curve25519) *) 155 + their_identity_key : string; 156 + (* Their current ratchet key *) 157 + mutable their_ratchet_key : string option; 158 + (* Our current ratchet key pair *) 159 + mutable our_ratchet_secret : X25519.secret; 160 + mutable our_ratchet_public : string; 161 + (* Root key for deriving chain keys *) 162 + mutable root_key : root_key; 163 + (* Sending chain *) 164 + mutable sending_chain : chain_key option; 165 + (* Receiving chains (their_ratchet_key -> chain) *) 166 + mutable receiving_chains : (string * chain_key) list; 167 + (* Skipped message keys for out-of-order decryption *) 168 + mutable skipped_keys : ((string * int) * message_key) list; 169 + (* Creation time *) 170 + creation_time : Ptime.t; 171 + } 172 + 173 + (** HKDF for key derivation using SHA-256 *) 174 + let hkdf_sha256 ~salt ~info ~ikm length = 175 + let prk = Hkdf.extract ~hash:`SHA256 ~salt ikm in 176 + Hkdf.expand ~hash:`SHA256 ~prk ~info length 177 + 178 + (** Derive root and chain keys from shared secret *) 179 + let kdf_rk root_key shared_secret = 180 + let derived = hkdf_sha256 181 + ~salt:root_key 182 + ~info:"OLM_ROOT" 183 + ~ikm:shared_secret 184 + 64 185 + in 186 + let new_root = String.sub derived 0 32 in 187 + let chain_key = String.sub derived 32 32 in 188 + (new_root, chain_key) 189 + 190 + (** Derive next chain key and message key *) 191 + let kdf_ck chain_key = 192 + let mk = hkdf_sha256 193 + ~salt:"" 194 + ~info:"OLM_CHAIN_MESSAGE" 195 + ~ikm:chain_key.key 196 + 32 197 + in 198 + let new_ck = hkdf_sha256 199 + ~salt:"" 200 + ~info:"OLM_CHAIN_KEY" 201 + ~ikm:chain_key.key 202 + 32 203 + in 204 + (mk, { key = new_ck; index = chain_key.index + 1 }) 205 + 206 + (** Perform X3DH key agreement for outbound session *) 207 + let x3dh_outbound ~our_identity_secret ~our_ephemeral_secret 208 + ~their_identity_key ~their_one_time_key = 209 + (* DH1: our_identity_secret, their_one_time_key *) 210 + let dh1 = match X25519.key_exchange our_identity_secret their_one_time_key with 211 + | Ok s -> s 212 + | Error _ -> failwith "Key exchange failed" 213 + in 214 + (* DH2: our_ephemeral_secret, their_identity_key *) 215 + let dh2 = match X25519.key_exchange our_ephemeral_secret their_identity_key with 216 + | Ok s -> s 217 + | Error _ -> failwith "Key exchange failed" 218 + in 219 + (* DH3: our_ephemeral_secret, their_one_time_key *) 220 + let dh3 = match X25519.key_exchange our_ephemeral_secret their_one_time_key with 221 + | Ok s -> s 222 + | Error _ -> failwith "Key exchange failed" 223 + in 224 + (* Combine: DH1 || DH2 || DH3 *) 225 + dh1 ^ dh2 ^ dh3 226 + 227 + (** Create a new outbound session (when sending first message). *) 228 + let create_outbound account ~their_identity_key ~their_one_time_key = 229 + (* Parse their keys from base64 *) 230 + let their_identity = match base64_decode their_identity_key with 231 + | Ok k -> k 232 + | Error _ -> failwith "Invalid identity key" 233 + in 234 + let their_otk = match base64_decode their_one_time_key with 235 + | Ok k -> k 236 + | Error _ -> failwith "Invalid one-time key" 237 + in 238 + (* Generate ephemeral key for X3DH *) 239 + let ephemeral_secret, _ephemeral_public = X25519.gen_key () in 240 + (* Perform X3DH *) 241 + let shared_secret = x3dh_outbound 242 + ~our_identity_secret:account.Account.curve25519_secret 243 + ~our_ephemeral_secret:ephemeral_secret 244 + ~their_identity_key:their_identity 245 + ~their_one_time_key:their_otk 246 + in 247 + (* Derive root key *) 248 + let root_key = hkdf_sha256 249 + ~salt:"" 250 + ~info:"OLM_ROOT" 251 + ~ikm:shared_secret 252 + 32 253 + in 254 + (* Generate our initial ratchet key *) 255 + let our_ratchet_secret, our_ratchet_public = X25519.gen_key () in 256 + (* Session ID is hash of the root key *) 257 + let session_id = 258 + Digestif.SHA256.(digest_string root_key |> to_raw_string) 259 + |> base64_encode 260 + in 261 + let now = match Ptime.of_float_s (Unix.gettimeofday ()) with 262 + | Some t -> t 263 + | None -> Ptime.epoch 264 + in 265 + (* Initial sending chain *) 266 + let sending_chain = Some { key = root_key; index = 0 } in 267 + { 268 + session_id; 269 + their_identity_key = their_identity; 270 + their_ratchet_key = None; 271 + our_ratchet_secret; 272 + our_ratchet_public; 273 + root_key; 274 + sending_chain; 275 + receiving_chains = []; 276 + skipped_keys = []; 277 + creation_time = now; 278 + } 279 + 280 + (** Create a new inbound session (when receiving first message). *) 281 + let create_inbound account ~their_identity_key ~their_ephemeral_key ~one_time_key_id = 282 + (* Get our one-time key secret *) 283 + let our_otk_secret = match Account.get_one_time_key_secret account one_time_key_id with 284 + | Some s -> s 285 + | None -> failwith "One-time key not found" 286 + in 287 + (* Parse their keys *) 288 + let their_identity = match base64_decode their_identity_key with 289 + | Ok k -> k 290 + | Error _ -> failwith "Invalid identity key" 291 + in 292 + let their_ephemeral = match base64_decode their_ephemeral_key with 293 + | Ok k -> k 294 + | Error _ -> failwith "Invalid ephemeral key" 295 + in 296 + (* Perform reverse X3DH *) 297 + (* DH1: their_identity, our_otk *) 298 + let dh1 = match X25519.key_exchange our_otk_secret their_identity with 299 + | Ok s -> s 300 + | Error _ -> failwith "Key exchange failed" 301 + in 302 + (* DH2: their_ephemeral, our_identity *) 303 + let dh2 = match X25519.key_exchange account.Account.curve25519_secret their_ephemeral with 304 + | Ok s -> s 305 + | Error _ -> failwith "Key exchange failed" 306 + in 307 + (* DH3: their_ephemeral, our_otk *) 308 + let dh3 = match X25519.key_exchange our_otk_secret their_ephemeral with 309 + | Ok s -> s 310 + | Error _ -> failwith "Key exchange failed" 311 + in 312 + let shared_secret = dh1 ^ dh2 ^ dh3 in 313 + (* Derive root key *) 314 + let root_key = hkdf_sha256 315 + ~salt:"" 316 + ~info:"OLM_ROOT" 317 + ~ikm:shared_secret 318 + 32 319 + in 320 + (* Generate our ratchet key *) 321 + let our_ratchet_secret, our_ratchet_public = X25519.gen_key () in 322 + let session_id = 323 + Digestif.SHA256.(digest_string root_key |> to_raw_string) 324 + |> base64_encode 325 + in 326 + let now = match Ptime.of_float_s (Unix.gettimeofday ()) with 327 + | Some t -> t 328 + | None -> Ptime.epoch 329 + in 330 + (* Remove the used one-time key *) 331 + Account.remove_one_time_key account one_time_key_id; 332 + { 333 + session_id; 334 + their_identity_key = their_identity; 335 + their_ratchet_key = Some their_ephemeral; 336 + our_ratchet_secret; 337 + our_ratchet_public; 338 + root_key; 339 + sending_chain = None; 340 + receiving_chains = [(their_ephemeral, { key = root_key; index = 0 })]; 341 + skipped_keys = []; 342 + creation_time = now; 343 + } 344 + 345 + (** Get session ID *) 346 + let session_id t = t.session_id 347 + 348 + (** Get their identity key *) 349 + let their_identity_key t = base64_encode t.their_identity_key 350 + 351 + (** Encrypt a message using AES-256-CBC with HMAC-SHA256 *) 352 + let aes_encrypt key plaintext = 353 + (* Use first 32 bytes for AES key, derive IV *) 354 + let aes_key = String.sub key 0 32 in 355 + let iv = Digestif.SHA256.(digest_string (aes_key ^ "IV") |> to_raw_string) 356 + |> fun s -> String.sub s 0 16 in 357 + (* PKCS7 padding *) 358 + let block_size = 16 in 359 + let pad_len = block_size - (String.length plaintext mod block_size) in 360 + let padded = plaintext ^ String.make pad_len (Char.chr pad_len) in 361 + (* Encrypt using mirage-crypto AES.CBC *) 362 + let cipher = Mirage_crypto.AES.CBC.of_secret aes_key in 363 + let encrypted = Mirage_crypto.AES.CBC.encrypt ~key:cipher ~iv padded in 364 + iv ^ encrypted 365 + 366 + (** Decrypt a message *) 367 + let aes_decrypt key ciphertext = 368 + if String.length ciphertext < 16 then 369 + Error "Ciphertext too short" 370 + else 371 + let iv = String.sub ciphertext 0 16 in 372 + let data = String.sub ciphertext 16 (String.length ciphertext - 16) in 373 + let aes_key = String.sub key 0 32 in 374 + let cipher = Mirage_crypto.AES.CBC.of_secret aes_key in 375 + let decrypted = Mirage_crypto.AES.CBC.decrypt ~key:cipher ~iv data in 376 + (* Remove PKCS7 padding *) 377 + if String.length decrypted = 0 then 378 + Error "Empty plaintext" 379 + else 380 + let pad_len = Char.code decrypted.[String.length decrypted - 1] in 381 + if pad_len > 16 || pad_len > String.length decrypted then 382 + Error "Invalid padding" 383 + else 384 + Ok (String.sub decrypted 0 (String.length decrypted - pad_len)) 385 + 386 + (** Encrypt a plaintext message. Returns (message_type, ciphertext). *) 387 + let encrypt t plaintext = 388 + (* Get or create sending chain *) 389 + let chain = match t.sending_chain with 390 + | Some c -> c 391 + | None -> 392 + (* Need to ratchet first *) 393 + let new_secret, new_public = X25519.gen_key () in 394 + t.our_ratchet_secret <- new_secret; 395 + t.our_ratchet_public <- new_public; 396 + { key = t.root_key; index = 0 } 397 + in 398 + (* Derive message key *) 399 + let message_key, new_chain = kdf_ck chain in 400 + t.sending_chain <- Some new_chain; 401 + (* Encrypt the message *) 402 + let ciphertext = aes_encrypt message_key plaintext in 403 + (* Create message payload with ratchet key and chain index *) 404 + let ratchet_key_b64 = base64_encode t.our_ratchet_public in 405 + let msg_type = if chain.index = 0 then 0 else 1 in (* 0 = prekey, 1 = normal *) 406 + let payload = Printf.sprintf "%s|%d|%s" 407 + ratchet_key_b64 408 + new_chain.index 409 + (base64_encode ciphertext) 410 + in 411 + (msg_type, payload) 412 + 413 + (** Decrypt a message. *) 414 + let decrypt t ~message_type ~ciphertext:payload = 415 + (* Parse payload *) 416 + match String.split_on_char '|' payload with 417 + | [ratchet_key_b64; index_str; ciphertext_b64] -> 418 + let their_ratchet = match base64_decode ratchet_key_b64 with 419 + | Ok k -> k 420 + | Error _ -> failwith "Invalid ratchet key" 421 + in 422 + let msg_index = int_of_string index_str in 423 + let ciphertext = match base64_decode ciphertext_b64 with 424 + | Ok c -> c 425 + | Error _ -> failwith "Invalid ciphertext" 426 + in 427 + (* Check if we need to advance the ratchet *) 428 + let _need_ratchet = match t.their_ratchet_key with 429 + | Some k when k = their_ratchet -> false 430 + | _ -> true 431 + in 432 + (* Find or create receiving chain *) 433 + let chain = match List.assoc_opt their_ratchet t.receiving_chains with 434 + | Some c -> c 435 + | None -> 436 + (* New ratchet - derive new chain *) 437 + let dh_out = match X25519.key_exchange t.our_ratchet_secret their_ratchet with 438 + | Ok s -> s 439 + | Error _ -> failwith "Key exchange failed" 440 + in 441 + let new_root, chain_key = kdf_rk t.root_key dh_out in 442 + t.root_key <- new_root; 443 + t.their_ratchet_key <- Some their_ratchet; 444 + let chain = { key = chain_key; index = 0 } in 445 + t.receiving_chains <- (their_ratchet, chain) :: t.receiving_chains; 446 + chain 447 + in 448 + (* Advance chain to the right index *) 449 + let rec advance_chain c target_idx = 450 + if c.index >= target_idx then c 451 + else 452 + let mk, new_c = kdf_ck c in 453 + (* Store skipped keys *) 454 + t.skipped_keys <- ((their_ratchet, c.index), mk) :: t.skipped_keys; 455 + advance_chain new_c target_idx 456 + in 457 + let chain = advance_chain chain msg_index in 458 + (* Get message key *) 459 + let message_key, new_chain = kdf_ck chain in 460 + (* Update chain *) 461 + t.receiving_chains <- 462 + (their_ratchet, new_chain) :: 463 + (List.filter (fun (k, _) -> k <> their_ratchet) t.receiving_chains); 464 + (* Decrypt *) 465 + let _ = message_type in 466 + aes_decrypt message_key ciphertext 467 + | _ -> Error "Invalid message format" 468 + 469 + (** Check if this is a pre-key message (first message in session). *) 470 + let is_pre_key_message message_type = message_type = 0 471 + end 472 + 473 + (** Megolm session for room message encryption. 474 + 475 + Megolm uses a ratchet that only moves forward, making it efficient 476 + for encrypting many messages to many recipients. *) 477 + module Megolm = struct 478 + (** Inbound session for decrypting received room messages *) 479 + module Inbound = struct 480 + type t = { 481 + session_id : string; 482 + sender_key : string; (* Curve25519 key of sender *) 483 + room_id : string; 484 + (* Ratchet state - 4 parts of 256 bits each *) 485 + mutable ratchet : string array; (* 4 x 32 bytes *) 486 + mutable message_index : int; 487 + (* For detecting replays *) 488 + mutable received_indices : int list; 489 + (* Ed25519 signing key of the sender *) 490 + signing_key : string; 491 + creation_time : Ptime.t; 492 + } 493 + 494 + (** Advance the ratchet by one step *) 495 + let advance_ratchet t = 496 + (* Megolm ratchet: each part hashes the parts below it *) 497 + let hash s = Digestif.SHA256.(digest_string s |> to_raw_string) in 498 + (* R(i,j) = H(R(i-1,j) || j) for j = 0,1,2,3 *) 499 + (* Simplified: we just hash each part with its index *) 500 + let i = t.message_index land 3 in 501 + for j = i to 3 do 502 + t.ratchet.(j) <- hash (t.ratchet.(j) ^ string_of_int j) 503 + done; 504 + t.message_index <- t.message_index + 1 505 + 506 + (** Create from exported session data *) 507 + let of_export ~session_id ~sender_key ~room_id ~ratchet ~message_index ~signing_key = 508 + let now = match Ptime.of_float_s (Unix.gettimeofday ()) with 509 + | Some t -> t 510 + | None -> Ptime.epoch 511 + in 512 + { 513 + session_id; 514 + sender_key; 515 + room_id; 516 + ratchet; 517 + message_index; 518 + received_indices = []; 519 + signing_key; 520 + creation_time = now; 521 + } 522 + 523 + (** Create from room key event (m.room_key) *) 524 + let from_room_key ~sender_key ~room_id ~session_id ~session_key ~signing_key = 525 + (* Parse session_key which contains ratchet state *) 526 + let ratchet = match base64_decode session_key with 527 + | Ok data when String.length data >= 128 -> 528 + [| 529 + String.sub data 0 32; 530 + String.sub data 32 32; 531 + String.sub data 64 32; 532 + String.sub data 96 32; 533 + |] 534 + | _ -> 535 + (* Generate random initial state if parsing fails *) 536 + let random_part () = 537 + Mirage_crypto_rng.generate 32 538 + in 539 + [| random_part (); random_part (); random_part (); random_part () |] 540 + in 541 + of_export ~session_id ~sender_key ~room_id ~ratchet ~message_index:0 ~signing_key 542 + 543 + let session_id t = t.session_id 544 + let sender_key t = t.sender_key 545 + let room_id t = t.room_id 546 + let first_known_index t = t.message_index 547 + 548 + (** Derive encryption key from current ratchet state *) 549 + let derive_key t = 550 + let combined = String.concat "" (Array.to_list t.ratchet) in 551 + Hkdf.expand ~hash:`SHA256 ~prk:combined ~info:"MEGOLM_KEYS" 80 552 + 553 + (** Decrypt a message *) 554 + let decrypt t ~ciphertext ~message_index = 555 + (* Check for replay *) 556 + if List.mem message_index t.received_indices then 557 + Error "Duplicate message index (replay attack)" 558 + else if message_index < t.message_index then 559 + Error "Message index too old" 560 + else begin 561 + (* Advance ratchet to the right position *) 562 + while t.message_index < message_index do 563 + advance_ratchet t 564 + done; 565 + (* Derive key and decrypt *) 566 + let key_material = derive_key t in 567 + let aes_key = String.sub key_material 0 32 in 568 + let hmac_key = String.sub key_material 32 32 in 569 + let iv = String.sub key_material 64 16 in 570 + (* Verify HMAC if present (last 8 bytes of ciphertext) *) 571 + let ct_len = String.length ciphertext in 572 + if ct_len < 24 then 573 + Error "Ciphertext too short" 574 + else begin 575 + let ct_data = String.sub ciphertext 0 (ct_len - 8) in 576 + let mac = String.sub ciphertext (ct_len - 8) 8 in 577 + let expected_mac = 578 + Digestif.SHA256.hmac_string ~key:hmac_key ct_data 579 + |> Digestif.SHA256.to_raw_string 580 + |> fun s -> String.sub s 0 8 581 + in 582 + if mac <> expected_mac then 583 + Error "MAC verification failed" 584 + else begin 585 + (* Decrypt using mirage-crypto AES.CBC *) 586 + let cipher = Mirage_crypto.AES.CBC.of_secret aes_key in 587 + let decrypted = Mirage_crypto.AES.CBC.decrypt ~key:cipher ~iv ct_data in 588 + (* Remove PKCS7 padding *) 589 + let pad_len = Char.code decrypted.[String.length decrypted - 1] in 590 + if pad_len > 16 then 591 + Error "Invalid padding" 592 + else begin 593 + t.received_indices <- message_index :: t.received_indices; 594 + advance_ratchet t; 595 + Ok (String.sub decrypted 0 (String.length decrypted - pad_len)) 596 + end 597 + end 598 + end 599 + end 600 + end 601 + 602 + (** Outbound session for encrypting messages to send to a room *) 603 + module Outbound = struct 604 + type t = { 605 + session_id : string; 606 + room_id : string; 607 + (* Ratchet state *) 608 + mutable ratchet : string array; 609 + mutable message_index : int; 610 + (* Ed25519 signing key *) 611 + signing_priv : Ed25519.priv; 612 + signing_pub : Ed25519.pub; 613 + (* Creation and rotation tracking *) 614 + creation_time : Ptime.t; 615 + mutable message_count : int; 616 + max_messages : int; 617 + max_age : Ptime.Span.t; 618 + (* Users this session has been shared with *) 619 + mutable shared_with : (string * string) list; (* user_id, device_id pairs *) 620 + } 621 + 622 + (** Create a new outbound session for a room *) 623 + let create ~room_id = 624 + let session_id = 625 + Mirage_crypto_rng.generate 16 626 + |> base64_encode 627 + in 628 + let random_part () = 629 + Mirage_crypto_rng.generate 32 630 + in 631 + let ratchet = [| random_part (); random_part (); random_part (); random_part () |] in 632 + let signing_priv, signing_pub = Ed25519.generate () in 633 + let now = match Ptime.of_float_s (Unix.gettimeofday ()) with 634 + | Some t -> t 635 + | None -> Ptime.epoch 636 + in 637 + { 638 + session_id; 639 + room_id; 640 + ratchet; 641 + message_index = 0; 642 + signing_priv; 643 + signing_pub; 644 + creation_time = now; 645 + message_count = 0; 646 + max_messages = 100; 647 + max_age = Ptime.Span.of_int_s (7 * 24 * 60 * 60); (* 1 week *) 648 + shared_with = []; 649 + } 650 + 651 + (** Advance the ratchet *) 652 + let advance_ratchet t = 653 + let hash s = Digestif.SHA256.(digest_string s |> to_raw_string) in 654 + let i = t.message_index land 3 in 655 + for j = i to 3 do 656 + t.ratchet.(j) <- hash (t.ratchet.(j) ^ string_of_int j) 657 + done; 658 + t.message_index <- t.message_index + 1 659 + 660 + let session_id t = t.session_id 661 + let room_id t = t.room_id 662 + let message_index t = t.message_index 663 + 664 + (** Check if session should be rotated *) 665 + let needs_rotation t = 666 + t.message_count >= t.max_messages || 667 + match Ptime.of_float_s (Unix.gettimeofday ()) with 668 + | Some now -> 669 + (match Ptime.diff now t.creation_time |> Ptime.Span.compare t.max_age with 670 + | n when n > 0 -> true 671 + | _ -> false) 672 + | None -> false 673 + 674 + (** Derive encryption key *) 675 + let derive_key t = 676 + let combined = String.concat "" (Array.to_list t.ratchet) in 677 + Hkdf.expand ~hash:`SHA256 ~prk:combined ~info:"MEGOLM_KEYS" 80 678 + 679 + (** Export the session key for sharing via m.room_key *) 680 + let export_session_key t = 681 + let ratchet_data = String.concat "" (Array.to_list t.ratchet) in 682 + base64_encode ratchet_data 683 + 684 + (** Get the signing key *) 685 + let signing_key t = 686 + Ed25519.pub_to_octets t.signing_pub |> base64_encode 687 + 688 + (** Encrypt a message *) 689 + let encrypt t plaintext = 690 + let key_material = derive_key t in 691 + let aes_key = String.sub key_material 0 32 in 692 + let hmac_key = String.sub key_material 32 32 in 693 + let iv = String.sub key_material 64 16 in 694 + (* PKCS7 padding *) 695 + let block_size = 16 in 696 + let pad_len = block_size - (String.length plaintext mod block_size) in 697 + let padded = plaintext ^ String.make pad_len (Char.chr pad_len) in 698 + (* Encrypt using mirage-crypto AES.CBC *) 699 + let cipher = Mirage_crypto.AES.CBC.of_secret aes_key in 700 + let ct_data = Mirage_crypto.AES.CBC.encrypt ~key:cipher ~iv padded in 701 + (* Add HMAC (first 8 bytes) *) 702 + let mac = 703 + Digestif.SHA256.hmac_string ~key:hmac_key ct_data 704 + |> Digestif.SHA256.to_raw_string 705 + |> fun s -> String.sub s 0 8 706 + in 707 + let ciphertext = ct_data ^ mac in 708 + let msg_index = t.message_index in 709 + (* Advance ratchet for next message *) 710 + advance_ratchet t; 711 + t.message_count <- t.message_count + 1; 712 + (* Return message index and ciphertext *) 713 + (msg_index, base64_encode ciphertext) 714 + 715 + (** Mark session as shared with a user/device *) 716 + let mark_shared_with t ~user_id ~device_id = 717 + if not (List.mem (user_id, device_id) t.shared_with) then 718 + t.shared_with <- (user_id, device_id) :: t.shared_with 719 + 720 + (** Check if already shared with a user/device *) 721 + let is_shared_with t ~user_id ~device_id = 722 + List.mem (user_id, device_id) t.shared_with 723 + 724 + (** Get list of users this session is shared with *) 725 + let shared_with t = t.shared_with 726 + end 727 + end 728 + 729 + (** Olm Machine - high-level state machine for E2EE operations *) 730 + module Machine = struct 731 + type t = { 732 + user_id : string; 733 + device_id : string; 734 + account : Account.t; 735 + (* Active Olm sessions indexed by their curve25519 key *) 736 + mutable sessions : (string * Session.t list) list; 737 + (* Outbound Megolm sessions by room_id *) 738 + mutable outbound_group_sessions : (string * Megolm.Outbound.t) list; 739 + (* Inbound Megolm sessions by (room_id, session_id) *) 740 + mutable inbound_group_sessions : ((string * string) * Megolm.Inbound.t) list; 741 + (* Device keys we know about: user_id -> device_id -> device_keys *) 742 + mutable device_keys : (string * (string * Keys.queried_device_keys) list) list; 743 + } 744 + 745 + (** Create a new OlmMachine *) 746 + let create ~user_id ~device_id = 747 + let account = Account.create () in 748 + { 749 + user_id; 750 + device_id; 751 + account; 752 + sessions = []; 753 + outbound_group_sessions = []; 754 + inbound_group_sessions = []; 755 + device_keys = []; 756 + } 757 + 758 + (** Get identity keys *) 759 + let identity_keys t = Account.identity_keys t.account 760 + 761 + (** Get device keys for upload *) 762 + let device_keys_for_upload t = 763 + let ed25519, curve25519 = identity_keys t in 764 + let algorithms = [ 765 + "m.olm.v1.curve25519-aes-sha2-256"; 766 + "m.megolm.v1.aes-sha2-256"; 767 + ] in 768 + let keys = [ 769 + (Printf.sprintf "ed25519:%s" t.device_id, ed25519); 770 + (Printf.sprintf "curve25519:%s" t.device_id, curve25519); 771 + ] in 772 + (t.user_id, t.device_id, algorithms, keys) 773 + 774 + (** Generate one-time keys if needed *) 775 + let generate_one_time_keys t count = 776 + Account.generate_one_time_keys t.account count 777 + 778 + (** Get one-time keys for upload *) 779 + let one_time_keys_for_upload t = 780 + Account.signed_one_time_keys t.account 781 + 782 + (** Mark keys as uploaded *) 783 + let mark_keys_as_published t = 784 + Account.mark_keys_as_published t.account 785 + 786 + (** Store device keys from key query response *) 787 + let receive_device_keys t ~user_id ~devices = 788 + t.device_keys <- (user_id, devices) :: 789 + (List.filter (fun (uid, _) -> uid <> user_id) t.device_keys) 790 + 791 + (** Get or create outbound Megolm session for a room *) 792 + let get_outbound_group_session t ~room_id = 793 + match List.assoc_opt room_id t.outbound_group_sessions with 794 + | Some session when not (Megolm.Outbound.needs_rotation session) -> 795 + session 796 + | _ -> 797 + (* Create new session *) 798 + let session = Megolm.Outbound.create ~room_id in 799 + t.outbound_group_sessions <- 800 + (room_id, session) :: 801 + (List.filter (fun (rid, _) -> rid <> room_id) t.outbound_group_sessions); 802 + session 803 + 804 + (** Store inbound Megolm session from room key event *) 805 + let receive_room_key t ~sender_key ~room_id ~session_id ~session_key ~signing_key = 806 + let session = Megolm.Inbound.from_room_key 807 + ~sender_key ~room_id ~session_id ~session_key ~signing_key 808 + in 809 + t.inbound_group_sessions <- 810 + ((room_id, session_id), session) :: t.inbound_group_sessions 811 + 812 + (** Encrypt a room message using Megolm *) 813 + let encrypt_room_message t ~room_id ~content = 814 + let session = get_outbound_group_session t ~room_id in 815 + let msg_index, ciphertext = Megolm.Outbound.encrypt session content in 816 + let _, curve25519_key = identity_keys t in 817 + (* Build m.room.encrypted content *) 818 + let encrypted_content = Printf.sprintf 819 + {|{"algorithm":"m.megolm.v1.aes-sha2-256","sender_key":"%s","ciphertext":"%s","session_id":"%s","device_id":"%s"}|} 820 + curve25519_key 821 + ciphertext 822 + (Megolm.Outbound.session_id session) 823 + t.device_id 824 + in 825 + let _ = msg_index in 826 + encrypted_content 827 + 828 + (** Decrypt a room message *) 829 + let decrypt_room_message t ~room_id ~sender_key ~session_id ~ciphertext ~message_index = 830 + match List.assoc_opt (room_id, session_id) t.inbound_group_sessions with 831 + | Some session when Megolm.Inbound.sender_key session = sender_key -> 832 + Megolm.Inbound.decrypt session ~ciphertext ~message_index 833 + | Some _ -> 834 + Error "Sender key mismatch" 835 + | None -> 836 + Error "Unknown session" 837 + 838 + (** Get or create Olm session for a device *) 839 + let get_olm_session t ~their_identity_key = 840 + match List.assoc_opt their_identity_key t.sessions with 841 + | Some (session :: _) -> Some session 842 + | _ -> None 843 + 844 + (** Create outbound Olm session *) 845 + let create_olm_session t ~their_identity_key ~their_one_time_key = 846 + let session = Session.create_outbound t.account 847 + ~their_identity_key ~their_one_time_key 848 + in 849 + let existing = match List.assoc_opt their_identity_key t.sessions with 850 + | Some sessions -> sessions 851 + | None -> [] 852 + in 853 + t.sessions <- 854 + (their_identity_key, session :: existing) :: 855 + (List.filter (fun (k, _) -> k <> their_identity_key) t.sessions); 856 + session 857 + 858 + (** Process inbound Olm message to create session *) 859 + let create_inbound_session t ~their_identity_key ~their_ephemeral_key ~one_time_key_id = 860 + let session = Session.create_inbound t.account 861 + ~their_identity_key ~their_ephemeral_key ~one_time_key_id 862 + in 863 + let existing = match List.assoc_opt their_identity_key t.sessions with 864 + | Some sessions -> sessions 865 + | None -> [] 866 + in 867 + t.sessions <- 868 + (their_identity_key, session :: existing) :: 869 + (List.filter (fun (k, _) -> k <> their_identity_key) t.sessions); 870 + session 871 + 872 + (** Encrypt a to-device message *) 873 + let encrypt_to_device t ~their_identity_key ~their_one_time_key ~plaintext = 874 + let session = match get_olm_session t ~their_identity_key with 875 + | Some s -> s 876 + | None -> create_olm_session t ~their_identity_key ~their_one_time_key 877 + in 878 + Session.encrypt session plaintext 879 + 880 + (** Decrypt a to-device message *) 881 + let decrypt_to_device t ~their_identity_key ~message_type ~ciphertext = 882 + match get_olm_session t ~their_identity_key with 883 + | Some session -> 884 + Session.decrypt session ~message_type ~ciphertext 885 + | None -> 886 + Error "No session for sender" 887 + 888 + (** Number of one-time keys remaining *) 889 + let one_time_keys_count t = 890 + Account.one_time_keys_count t.account 891 + 892 + (** Should upload more one-time keys? *) 893 + let should_upload_keys t = 894 + one_time_keys_count t < Account.max_one_time_keys t.account / 2 895 + end
+72
lib/matrix_client/presence.ml
··· 1 + (** Presence status operations. *) 2 + 3 + type presence_state = 4 + | Online 5 + | Offline 6 + | Unavailable 7 + 8 + let presence_state_to_string = function 9 + | Online -> "online" 10 + | Offline -> "offline" 11 + | Unavailable -> "unavailable" 12 + 13 + let presence_state_of_string = function 14 + | "online" -> Ok Online 15 + | "offline" -> Ok Offline 16 + | "unavailable" -> Ok Unavailable 17 + | s -> Error ("Unknown presence state: " ^ s) 18 + 19 + let presence_state_jsont = 20 + Jsont.of_of_string ~kind:"presence_state" 21 + ~enc:presence_state_to_string 22 + presence_state_of_string 23 + 24 + type presence = { 25 + presence : presence_state; 26 + status_msg : string option; 27 + last_active_ago : int option; 28 + currently_active : bool option; 29 + } 30 + 31 + let presence_jsont = 32 + Jsont.Object.( 33 + map (fun presence status_msg last_active_ago currently_active -> 34 + { presence; status_msg; last_active_ago; currently_active }) 35 + |> mem "presence" presence_state_jsont 36 + |> opt_mem "status_msg" Jsont.string ~enc:(fun t -> t.status_msg) 37 + |> opt_mem "last_active_ago" Jsont.int ~enc:(fun t -> t.last_active_ago) 38 + |> opt_mem "currently_active" Jsont.bool ~enc:(fun t -> t.currently_active) 39 + |> finish) 40 + 41 + let get_presence client ~user_id = 42 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 43 + let path = Printf.sprintf "/presence/%s/status" (Uri.pct_encode user_id_str) in 44 + match Client.get client ~path () with 45 + | Error e -> Error e 46 + | Ok body -> Client.decode_response presence_jsont body 47 + 48 + type set_presence_request = { 49 + presence : presence_state; 50 + status_msg : string option; 51 + } [@@warning "-69"] 52 + 53 + let set_presence_request_jsont = 54 + Jsont.Object.( 55 + map (fun presence status_msg -> { presence; status_msg }) 56 + |> mem "presence" presence_state_jsont 57 + |> opt_mem "status_msg" Jsont.string ~enc:(fun t -> t.status_msg) 58 + |> finish) 59 + 60 + let set_presence client ~presence ?status_msg () = 61 + match Client.user_id client with 62 + | None -> Error (Error.Network_error "Not logged in") 63 + | Some user_id -> 64 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 65 + let path = Printf.sprintf "/presence/%s/status" (Uri.pct_encode user_id_str) in 66 + let request = { presence; status_msg } in 67 + match Client.encode_body set_presence_request_jsont request with 68 + | Error e -> Error e 69 + | Ok body -> 70 + match Client.put client ~path ~body () with 71 + | Error e -> Error e 72 + | Ok _ -> Ok ()
+31
lib/matrix_client/presence.mli
··· 1 + (** Presence status operations. *) 2 + 3 + (** Presence state. *) 4 + type presence_state = 5 + | Online 6 + | Offline 7 + | Unavailable 8 + 9 + (** Presence information. *) 10 + type presence = { 11 + presence : presence_state; 12 + status_msg : string option; 13 + last_active_ago : int option; 14 + currently_active : bool option; 15 + } 16 + 17 + (** Get a user's presence status. *) 18 + val get_presence : 19 + Client.t -> 20 + user_id:Matrix_proto.Id.User_id.t -> 21 + (presence, Error.t) result 22 + 23 + (** Set the current user's presence status. 24 + 25 + @param status_msg Optional status message. *) 26 + val set_presence : 27 + Client.t -> 28 + presence:presence_state -> 29 + ?status_msg:string -> 30 + unit -> 31 + (unit, Error.t) result
+108
lib/matrix_client/profile.ml
··· 1 + (** User profile operations. *) 2 + 3 + type profile = { 4 + displayname : string option; 5 + avatar_url : string option; 6 + } 7 + 8 + let profile_jsont = 9 + Jsont.Object.( 10 + map (fun displayname avatar_url -> { displayname; avatar_url }) 11 + |> opt_mem "displayname" Jsont.string ~enc:(fun t -> t.displayname) 12 + |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url) 13 + |> finish) 14 + 15 + let get_profile client ~user_id = 16 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 17 + let path = Printf.sprintf "/profile/%s" (Uri.pct_encode user_id_str) in 18 + match Client.get client ~path () with 19 + | Error e -> Error e 20 + | Ok body -> Client.decode_response profile_jsont body 21 + 22 + type displayname_response = { 23 + displayname : string option; 24 + } 25 + 26 + let displayname_response_jsont = 27 + Jsont.Object.( 28 + map (fun displayname -> { displayname }) 29 + |> opt_mem "displayname" Jsont.string 30 + |> finish) 31 + 32 + let get_displayname client ~user_id = 33 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 34 + let path = Printf.sprintf "/profile/%s/displayname" (Uri.pct_encode user_id_str) in 35 + match Client.get client ~path () with 36 + | Error e -> Error e 37 + | Ok body -> 38 + match Client.decode_response displayname_response_jsont body with 39 + | Error e -> Error e 40 + | Ok resp -> Ok resp.displayname 41 + 42 + type avatar_url_response = { 43 + avatar_url : string option; 44 + } 45 + 46 + let avatar_url_response_jsont = 47 + Jsont.Object.( 48 + map (fun avatar_url -> { avatar_url }) 49 + |> opt_mem "avatar_url" Jsont.string 50 + |> finish) 51 + 52 + let get_avatar_url client ~user_id = 53 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 54 + let path = Printf.sprintf "/profile/%s/avatar_url" (Uri.pct_encode user_id_str) in 55 + match Client.get client ~path () with 56 + | Error e -> Error e 57 + | Ok body -> 58 + match Client.decode_response avatar_url_response_jsont body with 59 + | Error e -> Error e 60 + | Ok resp -> Ok resp.avatar_url 61 + 62 + type set_displayname_request = { 63 + displayname : string; 64 + } [@@warning "-69"] 65 + 66 + let set_displayname_request_jsont = 67 + Jsont.Object.( 68 + map (fun displayname -> { displayname }) 69 + |> mem "displayname" Jsont.string 70 + |> finish) 71 + 72 + let set_displayname client ~displayname = 73 + match Client.user_id client with 74 + | None -> Error (Error.Network_error "Not logged in") 75 + | Some user_id -> 76 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 77 + let path = Printf.sprintf "/profile/%s/displayname" (Uri.pct_encode user_id_str) in 78 + let request = { displayname } in 79 + match Client.encode_body set_displayname_request_jsont request with 80 + | Error e -> Error e 81 + | Ok body -> 82 + match Client.put client ~path ~body () with 83 + | Error e -> Error e 84 + | Ok _ -> Ok () 85 + 86 + type set_avatar_url_request = { 87 + avatar_url : string; 88 + } [@@warning "-69"] 89 + 90 + let set_avatar_url_request_jsont = 91 + Jsont.Object.( 92 + map (fun avatar_url -> { avatar_url }) 93 + |> mem "avatar_url" Jsont.string 94 + |> finish) 95 + 96 + let set_avatar_url client ~avatar_url = 97 + match Client.user_id client with 98 + | None -> Error (Error.Network_error "Not logged in") 99 + | Some user_id -> 100 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 101 + let path = Printf.sprintf "/profile/%s/avatar_url" (Uri.pct_encode user_id_str) in 102 + let request = { avatar_url } in 103 + match Client.encode_body set_avatar_url_request_jsont request with 104 + | Error e -> Error e 105 + | Ok body -> 106 + match Client.put client ~path ~body () with 107 + | Error e -> Error e 108 + | Ok _ -> Ok ()
+37
lib/matrix_client/profile.mli
··· 1 + (** User profile operations. *) 2 + 3 + (** User profile. *) 4 + type profile = { 5 + displayname : string option; 6 + avatar_url : string option; 7 + } 8 + 9 + (** Get a user's full profile. *) 10 + val get_profile : 11 + Client.t -> 12 + user_id:Matrix_proto.Id.User_id.t -> 13 + (profile, Error.t) result 14 + 15 + (** Get a user's display name. *) 16 + val get_displayname : 17 + Client.t -> 18 + user_id:Matrix_proto.Id.User_id.t -> 19 + (string option, Error.t) result 20 + 21 + (** Get a user's avatar URL. *) 22 + val get_avatar_url : 23 + Client.t -> 24 + user_id:Matrix_proto.Id.User_id.t -> 25 + (string option, Error.t) result 26 + 27 + (** Set the current user's display name. *) 28 + val set_displayname : 29 + Client.t -> 30 + displayname:string -> 31 + (unit, Error.t) result 32 + 33 + (** Set the current user's avatar URL. *) 34 + val set_avatar_url : 35 + Client.t -> 36 + avatar_url:string -> 37 + (unit, Error.t) result
+368
lib/matrix_client/push.ml
··· 1 + (** Push notification operations. *) 2 + 3 + (** Push rule kinds *) 4 + type rule_kind = 5 + | Override 6 + | Underride 7 + | Sender 8 + | Room 9 + | Content 10 + 11 + let rule_kind_to_string = function 12 + | Override -> "override" 13 + | Underride -> "underride" 14 + | Sender -> "sender" 15 + | Room -> "room" 16 + | Content -> "content" 17 + 18 + let rule_kind_of_string = function 19 + | "override" -> Ok Override 20 + | "underride" -> Ok Underride 21 + | "sender" -> Ok Sender 22 + | "room" -> Ok Room 23 + | "content" -> Ok Content 24 + | s -> Error ("Unknown rule kind: " ^ s) 25 + 26 + let rule_kind_jsont = 27 + Jsont.of_of_string ~kind:"rule_kind" 28 + ~enc:rule_kind_to_string 29 + rule_kind_of_string 30 + 31 + (** Push rule action *) 32 + type action = 33 + | Notify 34 + | Dont_notify 35 + | Coalesce 36 + | Set_tweak of string * Jsont.json 37 + 38 + (* For actions, we use a string codec as a simplification *) 39 + let action_jsont : action Jsont.t = 40 + Jsont.string 41 + |> Jsont.map 42 + ~dec:(function 43 + | "notify" -> Notify 44 + | "dont_notify" -> Dont_notify 45 + | "coalesce" -> Coalesce 46 + | _ -> Dont_notify) 47 + ~enc:(function 48 + | Notify -> "notify" 49 + | Dont_notify -> "dont_notify" 50 + | Coalesce -> "coalesce" 51 + | Set_tweak _ -> "notify") 52 + 53 + (** Push rule condition *) 54 + type condition = { 55 + kind : string; 56 + key : string option; 57 + pattern : string option; 58 + is_ : string option; 59 + } 60 + 61 + let condition_jsont = 62 + Jsont.Object.( 63 + map (fun kind key pattern is_ -> { kind; key; pattern; is_ }) 64 + |> mem "kind" Jsont.string ~enc:(fun t -> t.kind) 65 + |> opt_mem "key" Jsont.string ~enc:(fun t -> t.key) 66 + |> opt_mem "pattern" Jsont.string ~enc:(fun t -> t.pattern) 67 + |> opt_mem "is" Jsont.string ~enc:(fun t -> t.is_) 68 + |> finish) 69 + 70 + (** Push rule *) 71 + type rule = { 72 + rule_id : string; 73 + default : bool; 74 + enabled : bool; 75 + actions : action list; 76 + conditions : condition list option; 77 + pattern : string option; 78 + } 79 + 80 + let rule_jsont = 81 + Jsont.Object.( 82 + map (fun rule_id default enabled actions conditions pattern -> 83 + { rule_id; default; enabled; actions; conditions; pattern }) 84 + |> mem "rule_id" Jsont.string ~enc:(fun t -> t.rule_id) 85 + |> mem "default" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.default) 86 + |> mem "enabled" Jsont.bool ~dec_absent:true ~enc:(fun t -> t.enabled) 87 + |> mem "actions" (Jsont.list action_jsont) ~dec_absent:[] ~enc:(fun t -> t.actions) 88 + |> opt_mem "conditions" (Jsont.list condition_jsont) ~enc:(fun t -> t.conditions) 89 + |> opt_mem "pattern" Jsont.string ~enc:(fun t -> t.pattern) 90 + |> finish) 91 + 92 + (** Push ruleset *) 93 + type ruleset = { 94 + override : rule list; 95 + underride : rule list; 96 + sender : rule list; 97 + room : rule list; 98 + content : rule list; 99 + } 100 + 101 + let ruleset_jsont = 102 + Jsont.Object.( 103 + map (fun override underride sender room content -> 104 + { override; underride; sender; room; content }) 105 + |> mem "override" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.override) 106 + |> mem "underride" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.underride) 107 + |> mem "sender" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.sender) 108 + |> mem "room" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.room) 109 + |> mem "content" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.content) 110 + |> finish) 111 + 112 + type push_rules_response = { 113 + global : ruleset; 114 + } 115 + 116 + let push_rules_response_jsont = 117 + Jsont.Object.( 118 + map (fun global -> { global }) 119 + |> mem "global" ruleset_jsont ~enc:(fun t -> t.global) 120 + |> finish) 121 + 122 + (** Get all push rules. *) 123 + let get_push_rules client = 124 + match Client.get client ~path:"/pushrules/" () with 125 + | Error e -> Error e 126 + | Ok body -> Client.decode_response push_rules_response_jsont body 127 + 128 + (** Get a specific push rule. *) 129 + let get_push_rule client ~scope ~kind ~rule_id = 130 + let kind_str = rule_kind_to_string kind in 131 + let path = Printf.sprintf "/pushrules/%s/%s/%s" 132 + (Uri.pct_encode scope) 133 + (Uri.pct_encode kind_str) 134 + (Uri.pct_encode rule_id) 135 + in 136 + match Client.get client ~path () with 137 + | Error e -> Error e 138 + | Ok body -> Client.decode_response rule_jsont body 139 + 140 + (** Delete a push rule. *) 141 + let delete_push_rule client ~scope ~kind ~rule_id = 142 + let kind_str = rule_kind_to_string kind in 143 + let path = Printf.sprintf "/pushrules/%s/%s/%s" 144 + (Uri.pct_encode scope) 145 + (Uri.pct_encode kind_str) 146 + (Uri.pct_encode rule_id) 147 + in 148 + match Client.delete client ~path () with 149 + | Error e -> Error e 150 + | Ok _ -> Ok () 151 + 152 + (** Add or update a push rule. *) 153 + type add_rule_request = { 154 + actions : action list; 155 + conditions : condition list option; 156 + pattern : string option; 157 + } [@@warning "-69"] 158 + 159 + let add_rule_request_jsont = 160 + Jsont.Object.( 161 + map (fun actions conditions pattern -> 162 + { actions; conditions; pattern }) 163 + |> mem "actions" (Jsont.list action_jsont) ~enc:(fun t -> t.actions) 164 + |> opt_mem "conditions" (Jsont.list condition_jsont) ~enc:(fun t -> t.conditions) 165 + |> opt_mem "pattern" Jsont.string ~enc:(fun t -> t.pattern) 166 + |> finish) 167 + 168 + let set_push_rule client ~scope ~kind ~rule_id 169 + ~actions ?conditions ?pattern ?before ?after () = 170 + let kind_str = rule_kind_to_string kind in 171 + let path = Printf.sprintf "/pushrules/%s/%s/%s" 172 + (Uri.pct_encode scope) 173 + (Uri.pct_encode kind_str) 174 + (Uri.pct_encode rule_id) 175 + in 176 + let query = 177 + [] 178 + |> (fun q -> match before with Some b -> ("before", b) :: q | None -> q) 179 + |> (fun q -> match after with Some a -> ("after", a) :: q | None -> q) 180 + in 181 + let query = if query = [] then None else Some query in 182 + let request = { actions; conditions; pattern } in 183 + match Client.encode_body add_rule_request_jsont request with 184 + | Error e -> Error e 185 + | Ok body -> 186 + match Client.put client ~path ~body ?query () with 187 + | Error e -> Error e 188 + | Ok _ -> Ok () 189 + 190 + (** Enable or disable a push rule. *) 191 + type enabled_request = { 192 + enabled : bool; 193 + } [@@warning "-69"] 194 + 195 + let enabled_request_jsont = 196 + Jsont.Object.( 197 + map (fun enabled -> { enabled }) 198 + |> mem "enabled" Jsont.bool ~enc:(fun t -> t.enabled) 199 + |> finish) 200 + 201 + let set_push_rule_enabled client ~scope ~kind ~rule_id ~enabled = 202 + let kind_str = rule_kind_to_string kind in 203 + let path = Printf.sprintf "/pushrules/%s/%s/%s/enabled" 204 + (Uri.pct_encode scope) 205 + (Uri.pct_encode kind_str) 206 + (Uri.pct_encode rule_id) 207 + in 208 + let request = { enabled } in 209 + match Client.encode_body enabled_request_jsont request with 210 + | Error e -> Error e 211 + | Ok body -> 212 + match Client.put client ~path ~body () with 213 + | Error e -> Error e 214 + | Ok _ -> Ok () 215 + 216 + (** Set the actions for a push rule. *) 217 + type actions_request = { 218 + actions : action list; 219 + } [@@warning "-69"] 220 + 221 + let actions_request_jsont = 222 + Jsont.Object.( 223 + map (fun actions -> { actions }) 224 + |> mem "actions" (Jsont.list action_jsont) ~enc:(fun t -> t.actions) 225 + |> finish) 226 + 227 + let set_push_rule_actions client ~scope ~kind ~rule_id ~actions = 228 + let kind_str = rule_kind_to_string kind in 229 + let path = Printf.sprintf "/pushrules/%s/%s/%s/actions" 230 + (Uri.pct_encode scope) 231 + (Uri.pct_encode kind_str) 232 + (Uri.pct_encode rule_id) 233 + in 234 + let request = { actions } in 235 + match Client.encode_body actions_request_jsont request with 236 + | Error e -> Error e 237 + | Ok body -> 238 + match Client.put client ~path ~body () with 239 + | Error e -> Error e 240 + | Ok _ -> Ok () 241 + 242 + (** Pusher types *) 243 + type pusher_kind = 244 + | Http 245 + | Email 246 + 247 + let pusher_kind_to_string = function 248 + | Http -> "http" 249 + | Email -> "email" 250 + 251 + let pusher_kind_of_string = function 252 + | "http" -> Ok Http 253 + | "email" -> Ok Email 254 + | s -> Error ("Unknown pusher kind: " ^ s) 255 + 256 + let pusher_kind_jsont = 257 + Jsont.of_of_string ~kind:"pusher_kind" 258 + ~enc:pusher_kind_to_string 259 + pusher_kind_of_string 260 + 261 + type pusher_data = { 262 + url : string option; 263 + format : string option; 264 + } 265 + 266 + let pusher_data_jsont = 267 + Jsont.Object.( 268 + map (fun url format -> { url; format }) 269 + |> opt_mem "url" Jsont.string ~enc:(fun t -> t.url) 270 + |> opt_mem "format" Jsont.string ~enc:(fun t -> t.format) 271 + |> finish) 272 + 273 + type pusher = { 274 + pushkey : string; 275 + kind : pusher_kind; 276 + app_id : string; 277 + app_display_name : string; 278 + device_display_name : string; 279 + profile_tag : string option; 280 + lang : string; 281 + data : pusher_data; 282 + } 283 + 284 + let pusher_jsont = 285 + Jsont.Object.( 286 + map (fun pushkey kind app_id app_display_name device_display_name 287 + profile_tag lang data -> 288 + { pushkey; kind; app_id; app_display_name; device_display_name; 289 + profile_tag; lang; data }) 290 + |> mem "pushkey" Jsont.string ~enc:(fun t -> t.pushkey) 291 + |> mem "kind" pusher_kind_jsont ~enc:(fun t -> t.kind) 292 + |> mem "app_id" Jsont.string ~enc:(fun t -> t.app_id) 293 + |> mem "app_display_name" Jsont.string ~enc:(fun t -> t.app_display_name) 294 + |> mem "device_display_name" Jsont.string ~enc:(fun t -> t.device_display_name) 295 + |> opt_mem "profile_tag" Jsont.string ~enc:(fun t -> t.profile_tag) 296 + |> mem "lang" Jsont.string ~enc:(fun t -> t.lang) 297 + |> mem "data" pusher_data_jsont ~enc:(fun t -> t.data) 298 + |> finish) 299 + 300 + type pushers_response = { 301 + pushers : pusher list; 302 + } 303 + 304 + let pushers_response_jsont = 305 + Jsont.Object.( 306 + map (fun pushers -> { pushers }) 307 + |> mem "pushers" (Jsont.list pusher_jsont) ~dec_absent:[] ~enc:(fun t -> t.pushers) 308 + |> finish) 309 + 310 + (** Get all pushers for the current user. *) 311 + let get_pushers client = 312 + match Client.get client ~path:"/pushers" () with 313 + | Error e -> Error e 314 + | Ok body -> 315 + match Client.decode_response pushers_response_jsont body with 316 + | Error e -> Error e 317 + | Ok resp -> Ok resp.pushers 318 + 319 + (** Set a pusher. *) 320 + type set_pusher_request = { 321 + pushkey : string; 322 + kind : pusher_kind; 323 + app_id : string; 324 + app_display_name : string; 325 + device_display_name : string; 326 + profile_tag : string option; 327 + lang : string; 328 + data : pusher_data; 329 + append : bool option; 330 + } [@@warning "-69"] 331 + 332 + let set_pusher_request_jsont = 333 + Jsont.Object.( 334 + map (fun pushkey kind app_id app_display_name device_display_name 335 + profile_tag lang data append -> 336 + { pushkey; kind; app_id; app_display_name; device_display_name; 337 + profile_tag; lang; data; append }) 338 + |> mem "pushkey" Jsont.string 339 + |> mem "kind" pusher_kind_jsont 340 + |> mem "app_id" Jsont.string 341 + |> mem "app_display_name" Jsont.string 342 + |> mem "device_display_name" Jsont.string 343 + |> opt_mem "profile_tag" Jsont.string ~enc:(fun t -> t.profile_tag) 344 + |> mem "lang" Jsont.string 345 + |> mem "data" pusher_data_jsont 346 + |> opt_mem "append" Jsont.bool ~enc:(fun t -> t.append) 347 + |> finish) 348 + 349 + let set_pusher client ~pushkey ~kind ~app_id ~app_display_name 350 + ~device_display_name ?profile_tag ~lang ~data ?append () = 351 + let request = { 352 + pushkey; kind; app_id; app_display_name; device_display_name; 353 + profile_tag; lang; data; append 354 + } in 355 + match Client.encode_body set_pusher_request_jsont request with 356 + | Error e -> Error e 357 + | Ok body -> 358 + match Client.post client ~path:"/pushers/set" ~body () with 359 + | Error e -> Error e 360 + | Ok _ -> Ok () 361 + 362 + (** Delete a pusher by setting kind to null. *) 363 + let delete_pusher client ~pushkey ~app_id = 364 + (* Use raw json for the special null kind *) 365 + let body = Printf.sprintf {|{"pushkey":"%s","kind":null,"app_id":"%s"}|} pushkey app_id in 366 + match Client.post client ~path:"/pushers/set" ~body () with 367 + | Error e -> Error e 368 + | Ok _ -> Ok ()
+39
lib/matrix_client/receipts.ml
··· 1 + (** Read receipts. *) 2 + 3 + let send_receipt client ~room_id ~event_id ?(receipt_type = "m.read") () = 4 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 5 + let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in 6 + let path = Printf.sprintf "/rooms/%s/receipt/%s/%s" 7 + (Uri.pct_encode room_id_str) 8 + (Uri.pct_encode receipt_type) 9 + (Uri.pct_encode event_id_str) 10 + in 11 + match Client.post client ~path ~body:"{}" () with 12 + | Error e -> Error e 13 + | Ok _ -> Ok () 14 + 15 + type read_marker_request = { 16 + fully_read : string; 17 + read : string option; 18 + } [@@warning "-69"] 19 + 20 + let read_marker_request_jsont = 21 + Jsont.Object.( 22 + map (fun fully_read read -> { fully_read; read }) 23 + |> mem "m.fully_read" Jsont.string 24 + |> opt_mem "m.read" Jsont.string ~enc:(fun t -> t.read) 25 + |> finish) 26 + 27 + let set_read_marker client ~room_id ~fully_read ?read () = 28 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 29 + let path = Printf.sprintf "/rooms/%s/read_markers" (Uri.pct_encode room_id_str) in 30 + let request = { 31 + fully_read = Matrix_proto.Id.Event_id.to_string fully_read; 32 + read = Option.map Matrix_proto.Id.Event_id.to_string read; 33 + } in 34 + match Client.encode_body read_marker_request_jsont request with 35 + | Error e -> Error e 36 + | Ok body -> 37 + match Client.post client ~path ~body () with 38 + | Error e -> Error e 39 + | Ok _ -> Ok ()
+24
lib/matrix_client/receipts.mli
··· 1 + (** Read receipts. *) 2 + 3 + (** Send a read receipt. 4 + 5 + @param receipt_type The receipt type (usually "m.read" or "m.read.private"). *) 6 + val send_receipt : 7 + Client.t -> 8 + room_id:Matrix_proto.Id.Room_id.t -> 9 + event_id:Matrix_proto.Id.Event_id.t -> 10 + ?receipt_type:string -> 11 + unit -> 12 + (unit, Error.t) result 13 + 14 + (** Set read markers. 15 + 16 + @param fully_read The event ID to mark as fully read. 17 + @param read The event ID to mark as read (optional). *) 18 + val set_read_marker : 19 + Client.t -> 20 + room_id:Matrix_proto.Id.Room_id.t -> 21 + fully_read:Matrix_proto.Id.Event_id.t -> 22 + ?read:Matrix_proto.Id.Event_id.t -> 23 + unit -> 24 + (unit, Error.t) result
+330
lib/matrix_client/relations.ml
··· 1 + (** Event relations: reactions, edits, threads, and replies. *) 2 + 3 + (* Relation types *) 4 + type relation_type = 5 + | Annotation (* m.annotation - reactions *) 6 + | Reference (* m.reference - generic reference *) 7 + | Replace (* m.replace - edits *) 8 + | Thread (* m.thread - threads *) 9 + 10 + let relation_type_to_string = function 11 + | Annotation -> "m.annotation" 12 + | Reference -> "m.reference" 13 + | Replace -> "m.replace" 14 + | Thread -> "m.thread" 15 + 16 + let relation_type_of_string = function 17 + | "m.annotation" -> Ok Annotation 18 + | "m.reference" -> Ok Reference 19 + | "m.replace" -> Ok Replace 20 + | "m.thread" -> Ok Thread 21 + | s -> Error ("Unknown relation type: " ^ s) 22 + 23 + let relation_type_jsont = 24 + Jsont.of_of_string ~kind:"relation_type" 25 + ~enc:relation_type_to_string 26 + relation_type_of_string 27 + [@@warning "-32"] 28 + 29 + (* Reaction *) 30 + type reaction = { 31 + event_id : Matrix_proto.Id.Event_id.t; 32 + key : string; (* emoji or shortcode *) 33 + } 34 + 35 + (* Send a reaction to an event *) 36 + type reaction_content = { 37 + relates_to : reaction_relates_to; 38 + } [@@warning "-69"] 39 + 40 + and reaction_relates_to = { 41 + rel_type : string; 42 + event_id : string; 43 + key : string; 44 + } [@@warning "-69"] 45 + 46 + let reaction_relates_to_jsont = 47 + Jsont.Object.( 48 + map (fun rel_type event_id key -> { rel_type; event_id; key }) 49 + |> mem "rel_type" Jsont.string 50 + |> mem "event_id" Jsont.string 51 + |> mem "key" Jsont.string 52 + |> finish) 53 + 54 + let reaction_content_jsont = 55 + Jsont.Object.( 56 + map (fun relates_to -> { relates_to }) 57 + |> mem "m.relates_to" reaction_relates_to_jsont 58 + |> finish) 59 + 60 + let send_reaction client ~room_id ~event_id ~key = 61 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 62 + let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in 63 + let path = Printf.sprintf "/rooms/%s/send/m.reaction" (Uri.pct_encode room_id_str) in 64 + let content = { 65 + relates_to = { 66 + rel_type = "m.annotation"; 67 + event_id = event_id_str; 68 + key; 69 + } 70 + } in 71 + match Client.encode_body reaction_content_jsont content with 72 + | Error e -> Error e 73 + | Ok body -> 74 + match Client.post client ~path ~body () with 75 + | Error e -> Error e 76 + | Ok body -> 77 + match Client.decode_response Messages.send_response_jsont body with 78 + | Error e -> Error e 79 + | Ok resp -> Ok resp.event_id 80 + 81 + (* Edit a message *) 82 + type edit_content = { 83 + msgtype : string; 84 + body : string; 85 + new_content : edit_new_content; 86 + relates_to : edit_relates_to; 87 + } [@@warning "-69"] 88 + 89 + and edit_new_content = { 90 + msgtype : string; 91 + body : string; 92 + } [@@warning "-69"] 93 + 94 + and edit_relates_to = { 95 + rel_type : string; 96 + event_id : string; 97 + } [@@warning "-69"] 98 + 99 + let edit_new_content_jsont = 100 + Jsont.Object.( 101 + map (fun msgtype body -> { msgtype; body }) 102 + |> mem "msgtype" Jsont.string 103 + |> mem "body" Jsont.string 104 + |> finish) 105 + 106 + let edit_relates_to_jsont = 107 + Jsont.Object.( 108 + map (fun rel_type event_id -> { rel_type; event_id }) 109 + |> mem "rel_type" Jsont.string 110 + |> mem "event_id" Jsont.string 111 + |> finish) 112 + 113 + let edit_content_jsont = 114 + Jsont.Object.( 115 + map (fun msgtype body new_content relates_to -> 116 + { msgtype; body; new_content; relates_to }) 117 + |> mem "msgtype" Jsont.string 118 + |> mem "body" Jsont.string 119 + |> mem "m.new_content" edit_new_content_jsont 120 + |> mem "m.relates_to" edit_relates_to_jsont 121 + |> finish) 122 + 123 + let edit_message client ~room_id ~event_id ~new_body = 124 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 125 + let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in 126 + let path = Printf.sprintf "/rooms/%s/send/m.room.message" (Uri.pct_encode room_id_str) in 127 + let content = { 128 + msgtype = "m.text"; 129 + body = "* " ^ new_body; (* Fallback for clients that don't support edits *) 130 + new_content = { 131 + msgtype = "m.text"; 132 + body = new_body; 133 + }; 134 + relates_to = { 135 + rel_type = "m.replace"; 136 + event_id = event_id_str; 137 + }; 138 + } in 139 + match Client.encode_body edit_content_jsont content with 140 + | Error e -> Error e 141 + | Ok body -> 142 + match Client.post client ~path ~body () with 143 + | Error e -> Error e 144 + | Ok body -> 145 + match Client.decode_response Messages.send_response_jsont body with 146 + | Error e -> Error e 147 + | Ok resp -> Ok resp.event_id 148 + 149 + (* Reply to a message *) 150 + type reply_relates_to = { 151 + in_reply_to : reply_in_reply_to; 152 + } [@@warning "-69"] 153 + 154 + and reply_in_reply_to = { 155 + event_id : string; 156 + } [@@warning "-69"] 157 + 158 + type reply_content = { 159 + msgtype : string; 160 + body : string; 161 + relates_to : reply_relates_to; 162 + } [@@warning "-69"] 163 + 164 + let reply_in_reply_to_jsont = 165 + Jsont.Object.( 166 + map (fun event_id -> { event_id }) 167 + |> mem "event_id" Jsont.string 168 + |> finish) 169 + 170 + let reply_relates_to_jsont = 171 + Jsont.Object.( 172 + map (fun in_reply_to -> { in_reply_to }) 173 + |> mem "m.in_reply_to" reply_in_reply_to_jsont 174 + |> finish) 175 + 176 + let reply_content_jsont = 177 + Jsont.Object.( 178 + map (fun msgtype body relates_to -> { msgtype; body; relates_to }) 179 + |> mem "msgtype" Jsont.string 180 + |> mem "body" Jsont.string 181 + |> mem "m.relates_to" reply_relates_to_jsont 182 + |> finish) 183 + 184 + let send_reply client ~room_id ~event_id ~body = 185 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 186 + let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in 187 + let path = Printf.sprintf "/rooms/%s/send/m.room.message" (Uri.pct_encode room_id_str) in 188 + let content = { 189 + msgtype = "m.text"; 190 + body; 191 + relates_to = { 192 + in_reply_to = { 193 + event_id = event_id_str; 194 + }; 195 + }; 196 + } in 197 + match Client.encode_body reply_content_jsont content with 198 + | Error e -> Error e 199 + | Ok body -> 200 + match Client.post client ~path ~body () with 201 + | Error e -> Error e 202 + | Ok resp_body -> 203 + match Client.decode_response Messages.send_response_jsont resp_body with 204 + | Error e -> Error e 205 + | Ok resp -> Ok resp.event_id 206 + 207 + (* Thread message *) 208 + type thread_relates_to = { 209 + rel_type : string; 210 + event_id : string; 211 + is_falling_back : bool; 212 + in_reply_to : reply_in_reply_to option; 213 + } [@@warning "-69"] 214 + 215 + type thread_content = { 216 + msgtype : string; 217 + body : string; 218 + relates_to : thread_relates_to; 219 + } [@@warning "-69"] 220 + 221 + let thread_relates_to_jsont = 222 + Jsont.Object.( 223 + map (fun rel_type event_id is_falling_back in_reply_to -> 224 + { rel_type; event_id; is_falling_back; in_reply_to }) 225 + |> mem "rel_type" Jsont.string 226 + |> mem "event_id" Jsont.string 227 + |> mem "is_falling_back" Jsont.bool ~dec_absent:true 228 + |> opt_mem "m.in_reply_to" reply_in_reply_to_jsont ~enc:(fun t -> t.in_reply_to) 229 + |> finish) 230 + 231 + let thread_content_jsont = 232 + Jsont.Object.( 233 + map (fun msgtype body relates_to -> { msgtype; body; relates_to }) 234 + |> mem "msgtype" Jsont.string 235 + |> mem "body" Jsont.string 236 + |> mem "m.relates_to" thread_relates_to_jsont 237 + |> finish) 238 + 239 + let send_in_thread client ~room_id ~thread_root_id ?reply_to_id ~body () = 240 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 241 + let thread_root_str = Matrix_proto.Id.Event_id.to_string thread_root_id in 242 + let path = Printf.sprintf "/rooms/%s/send/m.room.message" (Uri.pct_encode room_id_str) in 243 + let in_reply_to = match reply_to_id with 244 + | Some id -> Some { event_id = Matrix_proto.Id.Event_id.to_string id } 245 + | None -> None 246 + in 247 + let content = { 248 + msgtype = "m.text"; 249 + body; 250 + relates_to = { 251 + rel_type = "m.thread"; 252 + event_id = thread_root_str; 253 + is_falling_back = Option.is_none reply_to_id; 254 + in_reply_to; 255 + }; 256 + } in 257 + match Client.encode_body thread_content_jsont content with 258 + | Error e -> Error e 259 + | Ok body -> 260 + match Client.post client ~path ~body () with 261 + | Error e -> Error e 262 + | Ok resp_body -> 263 + match Client.decode_response Messages.send_response_jsont resp_body with 264 + | Error e -> Error e 265 + | Ok resp -> Ok resp.event_id 266 + 267 + (* Get relations for an event *) 268 + type aggregation = { 269 + event_id : Matrix_proto.Id.Event_id.t; 270 + origin_server_ts : int64; 271 + sender : Matrix_proto.Id.User_id.t; 272 + } 273 + 274 + let aggregation_jsont = 275 + Jsont.Object.( 276 + map (fun event_id origin_server_ts sender -> 277 + { event_id; origin_server_ts; sender }) 278 + |> mem "event_id" Matrix_proto.Id.Event_id.jsont 279 + |> mem "origin_server_ts" Jsont.int64 280 + |> mem "sender" Matrix_proto.Id.User_id.jsont 281 + |> finish) 282 + 283 + type relations_response = { 284 + chunk : aggregation list; 285 + next_batch : string option; 286 + prev_batch : string option; 287 + } 288 + 289 + let relations_response_jsont = 290 + Jsont.Object.( 291 + map (fun chunk next_batch prev_batch -> 292 + { chunk; next_batch; prev_batch }) 293 + |> mem "chunk" (Jsont.list aggregation_jsont) ~dec_absent:[] 294 + |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch) 295 + |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch) 296 + |> finish) 297 + 298 + let get_relations client ~room_id ~event_id ?rel_type ?event_type ?limit ?from () = 299 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 300 + let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in 301 + let path = match rel_type, event_type with 302 + | Some rt, Some et -> 303 + Printf.sprintf "/rooms/%s/relations/%s/%s/%s" 304 + (Uri.pct_encode room_id_str) 305 + (Uri.pct_encode event_id_str) 306 + (Uri.pct_encode (relation_type_to_string rt)) 307 + (Uri.pct_encode et) 308 + | Some rt, None -> 309 + Printf.sprintf "/rooms/%s/relations/%s/%s" 310 + (Uri.pct_encode room_id_str) 311 + (Uri.pct_encode event_id_str) 312 + (Uri.pct_encode (relation_type_to_string rt)) 313 + | None, _ -> 314 + Printf.sprintf "/rooms/%s/relations/%s" 315 + (Uri.pct_encode room_id_str) 316 + (Uri.pct_encode event_id_str) 317 + in 318 + let query = 319 + [] 320 + |> (fun q -> match limit with Some l -> ("limit", string_of_int l) :: q | None -> q) 321 + |> (fun q -> match from with Some f -> ("from", f) :: q | None -> q) 322 + in 323 + let query = if query = [] then None else Some query in 324 + match Client.get client ~path ?query () with 325 + | Error e -> Error e 326 + | Ok body -> Client.decode_response relations_response_jsont body 327 + 328 + (* Get all reactions for an event *) 329 + let get_reactions client ~room_id ~event_id = 330 + get_relations client ~room_id ~event_id ~rel_type:Annotation ~event_type:"m.reaction" ()
+120
lib/matrix_client/relations.mli
··· 1 + (** Event relations: reactions, edits, threads, and replies. *) 2 + 3 + (** {1 Relation Types} *) 4 + 5 + (** Types of event relations. *) 6 + type relation_type = 7 + | Annotation (** m.annotation - used for reactions *) 8 + | Reference (** m.reference - generic reference *) 9 + | Replace (** m.replace - used for edits *) 10 + | Thread (** m.thread - used for threads *) 11 + 12 + val relation_type_to_string : relation_type -> string 13 + val relation_type_of_string : string -> (relation_type, string) result 14 + 15 + (** {1 Reactions} *) 16 + 17 + (** A reaction to an event. *) 18 + type reaction = { 19 + event_id : Matrix_proto.Id.Event_id.t; 20 + key : string; (** Emoji or shortcode *) 21 + } 22 + 23 + (** Send a reaction to an event. 24 + 25 + @param room_id The room containing the event. 26 + @param event_id The event to react to. 27 + @param key The reaction key (typically an emoji). *) 28 + val send_reaction : 29 + Client.t -> 30 + room_id:Matrix_proto.Id.Room_id.t -> 31 + event_id:Matrix_proto.Id.Event_id.t -> 32 + key:string -> 33 + (Matrix_proto.Id.Event_id.t, Error.t) result 34 + 35 + (** {1 Edits} *) 36 + 37 + (** Edit a text message. 38 + 39 + @param room_id The room containing the message. 40 + @param event_id The message event to edit. 41 + @param new_body The new message body. *) 42 + val edit_message : 43 + Client.t -> 44 + room_id:Matrix_proto.Id.Room_id.t -> 45 + event_id:Matrix_proto.Id.Event_id.t -> 46 + new_body:string -> 47 + (Matrix_proto.Id.Event_id.t, Error.t) result 48 + 49 + (** {1 Replies} *) 50 + 51 + (** Send a reply to a message. 52 + 53 + @param room_id The room containing the message. 54 + @param event_id The message event to reply to. 55 + @param body The reply text. *) 56 + val send_reply : 57 + Client.t -> 58 + room_id:Matrix_proto.Id.Room_id.t -> 59 + event_id:Matrix_proto.Id.Event_id.t -> 60 + body:string -> 61 + (Matrix_proto.Id.Event_id.t, Error.t) result 62 + 63 + (** {1 Threads} *) 64 + 65 + (** Send a message in a thread. 66 + 67 + @param room_id The room containing the thread. 68 + @param thread_root_id The event ID of the thread root. 69 + @param reply_to_id Optional event to reply to within the thread. 70 + @param body The message text. *) 71 + val send_in_thread : 72 + Client.t -> 73 + room_id:Matrix_proto.Id.Room_id.t -> 74 + thread_root_id:Matrix_proto.Id.Event_id.t -> 75 + ?reply_to_id:Matrix_proto.Id.Event_id.t -> 76 + body:string -> 77 + unit -> 78 + (Matrix_proto.Id.Event_id.t, Error.t) result 79 + 80 + (** {1 Querying Relations} *) 81 + 82 + (** An aggregated event in a relations response. *) 83 + type aggregation = { 84 + event_id : Matrix_proto.Id.Event_id.t; 85 + origin_server_ts : int64; 86 + sender : Matrix_proto.Id.User_id.t; 87 + } 88 + 89 + (** Relations response. *) 90 + type relations_response = { 91 + chunk : aggregation list; 92 + next_batch : string option; 93 + prev_batch : string option; 94 + } 95 + 96 + (** Get relations for an event. 97 + 98 + @param room_id The room containing the event. 99 + @param event_id The event to get relations for. 100 + @param rel_type Filter by relation type. 101 + @param event_type Filter by event type. 102 + @param limit Maximum number of results. 103 + @param from Pagination token. *) 104 + val get_relations : 105 + Client.t -> 106 + room_id:Matrix_proto.Id.Room_id.t -> 107 + event_id:Matrix_proto.Id.Event_id.t -> 108 + ?rel_type:relation_type -> 109 + ?event_type:string -> 110 + ?limit:int -> 111 + ?from:string -> 112 + unit -> 113 + (relations_response, Error.t) result 114 + 115 + (** Get all reactions for an event. *) 116 + val get_reactions : 117 + Client.t -> 118 + room_id:Matrix_proto.Id.Room_id.t -> 119 + event_id:Matrix_proto.Id.Event_id.t -> 120 + (relations_response, Error.t) result
+257
lib/matrix_client/room_preview.ml
··· 1 + (** Room preview operations. 2 + 3 + Get information about a room before joining it. *) 4 + 5 + (** Room preview information *) 6 + type room_preview = { 7 + room_id : Matrix_proto.Id.Room_id.t; 8 + name : string option; 9 + topic : string option; 10 + avatar_url : string option; 11 + canonical_alias : Matrix_proto.Id.Room_alias.t option; 12 + join_rule : Matrix_proto.Event.Join_rule.t option; 13 + num_joined_members : int; 14 + room_type : string option; 15 + world_readable : bool; 16 + guest_can_join : bool; 17 + membership : Matrix_proto.Event.Membership.t option; 18 + } 19 + 20 + (** Summary returned by room summary endpoint (MSC3266) *) 21 + type room_summary = { 22 + room_id : string; 23 + membership : Matrix_proto.Event.Membership.t option; 24 + is_encrypted : bool option; 25 + room_name : string option; 26 + topic : string option; 27 + avatar_url : string option; 28 + canonical_alias : string option; 29 + joined_members_count : int option; 30 + invited_members_count : int option; 31 + room_version : string option; 32 + room_type : string option; 33 + join_rule : Matrix_proto.Event.Join_rule.t option; 34 + guest_can_join : bool option; 35 + world_readable : bool option; 36 + } 37 + 38 + let room_summary_jsont = 39 + Jsont.Object.( 40 + map (fun room_id membership is_encrypted room_name topic avatar_url 41 + canonical_alias joined_members_count invited_members_count 42 + room_version room_type join_rule guest_can_join world_readable -> 43 + { room_id; membership; is_encrypted; room_name; topic; avatar_url; 44 + canonical_alias; joined_members_count; invited_members_count; 45 + room_version; room_type; join_rule; guest_can_join; world_readable }) 46 + |> mem "room_id" Jsont.string ~enc:(fun t -> t.room_id) 47 + |> opt_mem "membership" Matrix_proto.Event.Membership.jsont ~enc:(fun t -> t.membership) 48 + |> opt_mem "is_encrypted" Jsont.bool ~enc:(fun t -> t.is_encrypted) 49 + |> opt_mem "room_name" Jsont.string ~enc:(fun t -> t.room_name) 50 + |> opt_mem "topic" Jsont.string ~enc:(fun t -> t.topic) 51 + |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url) 52 + |> opt_mem "canonical_alias" Jsont.string ~enc:(fun t -> t.canonical_alias) 53 + |> opt_mem "joined_members_count" Jsont.int ~enc:(fun t -> t.joined_members_count) 54 + |> opt_mem "invited_members_count" Jsont.int ~enc:(fun t -> t.invited_members_count) 55 + |> opt_mem "room_version" Jsont.string ~enc:(fun t -> t.room_version) 56 + |> opt_mem "room_type" Jsont.string ~enc:(fun t -> t.room_type) 57 + |> opt_mem "join_rule" Matrix_proto.Event.Join_rule.jsont ~enc:(fun t -> t.join_rule) 58 + |> opt_mem "guest_can_join" Jsont.bool ~enc:(fun t -> t.guest_can_join) 59 + |> opt_mem "world_readable" Jsont.bool ~enc:(fun t -> t.world_readable) 60 + |> finish) 61 + 62 + (** Get room summary (MSC3266). 63 + 64 + This is the preferred way to get room information before joining. *) 65 + let get_summary client ~room_id_or_alias ?via () = 66 + let id_str = match room_id_or_alias with 67 + | `Room_id id -> Matrix_proto.Id.Room_id.to_string id 68 + | `Room_alias alias -> Matrix_proto.Id.Room_alias.to_string alias 69 + in 70 + let path = Printf.sprintf "/rooms/%s/summary" (Uri.pct_encode id_str) in 71 + let query = match via with 72 + | Some servers -> Some (List.map (fun s -> ("via", s)) servers) 73 + | None -> None 74 + in 75 + match Client.get client ~path ?query () with 76 + | Error e -> Error e 77 + | Ok body -> Client.decode_response room_summary_jsont body 78 + 79 + (** Public room from directory listing *) 80 + type public_room = { 81 + room_id : Matrix_proto.Id.Room_id.t; 82 + name : string option; 83 + topic : string option; 84 + avatar_url : string option; 85 + canonical_alias : Matrix_proto.Id.Room_alias.t option; 86 + num_joined_members : int; 87 + world_readable : bool; 88 + guest_can_join : bool; 89 + join_rule : Matrix_proto.Event.Join_rule.t option; 90 + room_type : string option; 91 + } 92 + 93 + let public_room_jsont = 94 + Jsont.Object.( 95 + map (fun room_id name topic avatar_url canonical_alias num_joined_members 96 + world_readable guest_can_join join_rule room_type -> 97 + { room_id; name; topic; avatar_url; canonical_alias; num_joined_members; 98 + world_readable; guest_can_join; join_rule; room_type }) 99 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont ~enc:(fun t -> t.room_id) 100 + |> opt_mem "name" Jsont.string ~enc:(fun t -> t.name) 101 + |> opt_mem "topic" Jsont.string ~enc:(fun t -> t.topic) 102 + |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url) 103 + |> opt_mem "canonical_alias" Matrix_proto.Id.Room_alias.jsont ~enc:(fun t -> t.canonical_alias) 104 + |> mem "num_joined_members" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.num_joined_members) 105 + |> mem "world_readable" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.world_readable) 106 + |> mem "guest_can_join" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.guest_can_join) 107 + |> opt_mem "join_rule" Matrix_proto.Event.Join_rule.jsont ~enc:(fun t -> t.join_rule) 108 + |> opt_mem "room_type" Jsont.string ~enc:(fun t -> t.room_type) 109 + |> finish) 110 + 111 + type public_rooms_response = { 112 + chunk : public_room list; 113 + next_batch : string option; 114 + prev_batch : string option; 115 + total_room_count_estimate : int option; 116 + } 117 + 118 + let public_rooms_response_jsont = 119 + Jsont.Object.( 120 + map (fun chunk next_batch prev_batch total_room_count_estimate -> 121 + { chunk; next_batch; prev_batch; total_room_count_estimate }) 122 + |> mem "chunk" (Jsont.list public_room_jsont) ~dec_absent:[] 123 + ~enc:(fun t -> t.chunk) 124 + |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch) 125 + |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch) 126 + |> opt_mem "total_room_count_estimate" Jsont.int 127 + ~enc:(fun t -> t.total_room_count_estimate) 128 + |> finish) 129 + 130 + (** Get list of public rooms. 131 + 132 + @param limit Maximum number of rooms to return 133 + @param since Pagination token 134 + @param server Server to query for rooms *) 135 + let get_public_rooms client ?limit ?since ?server () = 136 + let path = "/publicRooms" in 137 + let query = 138 + [] 139 + |> (fun q -> match limit with Some l -> ("limit", string_of_int l) :: q | None -> q) 140 + |> (fun q -> match since with Some s -> ("since", s) :: q | None -> q) 141 + |> (fun q -> match server with Some s -> ("server", s) :: q | None -> q) 142 + in 143 + let query = if query = [] then None else Some query in 144 + match Client.get client ~path ?query () with 145 + | Error e -> Error e 146 + | Ok body -> Client.decode_response public_rooms_response_jsont body 147 + 148 + (** Search filter for public rooms *) 149 + type public_rooms_filter = { 150 + generic_search_term : string option; 151 + room_types : string list option; 152 + } 153 + 154 + let public_rooms_filter_jsont = 155 + Jsont.Object.( 156 + map (fun generic_search_term room_types -> 157 + { generic_search_term; room_types }) 158 + |> opt_mem "generic_search_term" Jsont.string ~enc:(fun t -> t.generic_search_term) 159 + |> opt_mem "room_types" (Jsont.list Jsont.string) ~enc:(fun t -> t.room_types) 160 + |> finish) 161 + 162 + type search_public_rooms_request = { 163 + limit : int option; 164 + since : string option; 165 + filter : public_rooms_filter option; 166 + include_all_networks : bool option; 167 + third_party_instance_id : string option; 168 + } [@@warning "-69"] 169 + 170 + let search_public_rooms_request_jsont = 171 + Jsont.Object.( 172 + map (fun limit since filter include_all_networks third_party_instance_id -> 173 + { limit; since; filter; include_all_networks; third_party_instance_id }) 174 + |> opt_mem "limit" Jsont.int ~enc:(fun t -> t.limit) 175 + |> opt_mem "since" Jsont.string ~enc:(fun t -> t.since) 176 + |> opt_mem "filter" public_rooms_filter_jsont ~enc:(fun t -> t.filter) 177 + |> opt_mem "include_all_networks" Jsont.bool ~enc:(fun t -> t.include_all_networks) 178 + |> opt_mem "third_party_instance_id" Jsont.string ~enc:(fun t -> t.third_party_instance_id) 179 + |> finish) 180 + 181 + (** Search public rooms with filters. 182 + 183 + @param search_term Text to search for 184 + @param limit Maximum number of rooms 185 + @param since Pagination token 186 + @param room_types Filter by room type 187 + @param server Server to query *) 188 + let search_public_rooms client ?search_term ?limit ?since ?room_types ?server () = 189 + let path = "/publicRooms" in 190 + let query = match server with 191 + | Some s -> Some [("server", s)] 192 + | None -> None 193 + in 194 + let filter = match search_term, room_types with 195 + | None, None -> None 196 + | _ -> Some { generic_search_term = search_term; room_types } 197 + in 198 + let request = { 199 + limit; 200 + since; 201 + filter; 202 + include_all_networks = None; 203 + third_party_instance_id = None; 204 + } in 205 + match Client.encode_body search_public_rooms_request_jsont request with 206 + | Error e -> Error e 207 + | Ok body -> 208 + match Client.post client ~path ~body ?query () with 209 + | Error e -> Error e 210 + | Ok resp_body -> Client.decode_response public_rooms_response_jsont resp_body 211 + 212 + (** Resolve a room alias to a room ID and servers. 213 + 214 + @param room_alias The room alias to resolve *) 215 + let resolve_alias client ~room_alias = 216 + let alias_str = Matrix_proto.Id.Room_alias.to_string room_alias in 217 + let path = Printf.sprintf "/directory/room/%s" (Uri.pct_encode alias_str) in 218 + let response_jsont = Jsont.Object.( 219 + map (fun room_id servers -> (room_id, servers)) 220 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont 221 + |> mem "servers" (Jsont.list Jsont.string) ~dec_absent:[] 222 + |> finish) 223 + in 224 + match Client.get client ~path () with 225 + | Error e -> Error e 226 + | Ok body -> Client.decode_response response_jsont body 227 + 228 + (** Knock on a room (MSC2403). 229 + 230 + Request to join a room that has knock join rules. 231 + 232 + @param room_id_or_alias The room to knock on 233 + @param reason Optional reason for the knock request 234 + @param via Servers to try *) 235 + let knock client ~room_id_or_alias ?reason ?(via = []) () = 236 + let id_str = match room_id_or_alias with 237 + | `Room_id id -> Matrix_proto.Id.Room_id.to_string id 238 + | `Room_alias alias -> Matrix_proto.Id.Room_alias.to_string alias 239 + in 240 + let path = Printf.sprintf "/knock/%s" (Uri.pct_encode id_str) in 241 + let query = if via = [] then None else Some (List.map (fun s -> ("server_name", s)) via) in 242 + let request_jsont = Jsont.Object.( 243 + map (fun reason -> reason) 244 + |> opt_mem "reason" Jsont.string ~enc:Fun.id 245 + |> finish) 246 + in 247 + let response_jsont = Jsont.Object.( 248 + map (fun room_id -> room_id) 249 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont 250 + |> finish) 251 + in 252 + match Client.encode_body request_jsont reason with 253 + | Error e -> Error e 254 + | Ok body -> 255 + match Client.post client ~path ~body ?query () with 256 + | Error e -> Error e 257 + | Ok resp_body -> Client.decode_response response_jsont resp_body
+461
lib/matrix_client/rooms.ml
··· 1 + (** Room operations. *) 2 + 3 + type visibility = [ `Public | `Private ] 4 + 5 + type preset = 6 + | Private_chat 7 + | Public_chat 8 + | Trusted_private_chat 9 + 10 + let preset_to_string = function 11 + | Private_chat -> "private_chat" 12 + | Public_chat -> "public_chat" 13 + | Trusted_private_chat -> "trusted_private_chat" 14 + 15 + (* Create room *) 16 + type create_request = { 17 + name : string option; 18 + topic : string option; 19 + visibility : string option; 20 + preset : string option; 21 + room_alias_local_part : string option; 22 + invite : string list; 23 + is_direct : bool option; 24 + room_type : string option; 25 + creation_content : Jsont.json option; 26 + } [@@warning "-69"] 27 + 28 + let create_request_jsont : create_request Jsont.t = 29 + let open Jsont.Object in 30 + map (fun name topic visibility preset room_alias_local_part invite is_direct room_type creation_content -> 31 + ({ name; topic; visibility; preset; room_alias_local_part; invite; is_direct; room_type; creation_content } : create_request)) 32 + |> opt_mem "name" Jsont.string ~enc:(fun (t : create_request) -> t.name) 33 + |> opt_mem "topic" Jsont.string ~enc:(fun (t : create_request) -> t.topic) 34 + |> opt_mem "visibility" Jsont.string ~enc:(fun (t : create_request) -> t.visibility) 35 + |> opt_mem "preset" Jsont.string ~enc:(fun (t : create_request) -> t.preset) 36 + |> opt_mem "room_alias_local_part" Jsont.string ~enc:(fun (t : create_request) -> t.room_alias_local_part) 37 + |> mem "invite" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : create_request) -> t.invite) 38 + |> opt_mem "is_direct" Jsont.bool ~enc:(fun (t : create_request) -> t.is_direct) 39 + |> opt_mem "room_type" Jsont.string ~enc:(fun (t : create_request) -> t.room_type) 40 + |> opt_mem "creation_content" Jsont.json ~enc:(fun (t : create_request) -> t.creation_content) 41 + |> finish 42 + 43 + type create_response = { 44 + room_id : Matrix_proto.Id.Room_id.t; 45 + } 46 + 47 + let create_response_jsont = 48 + Jsont.Object.( 49 + map (fun room_id -> { room_id }) 50 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont 51 + |> finish) 52 + 53 + let create client ?name ?topic ?visibility ?preset ?room_alias_local_part ?invite ?is_direct ?room_type () = 54 + let visibility_str = match visibility with 55 + | Some `Public -> Some "public" 56 + | Some `Private -> Some "private" 57 + | None -> None 58 + in 59 + let preset_str = Option.map preset_to_string preset in 60 + let invite_strs = match invite with 61 + | Some ids -> List.map Matrix_proto.Id.User_id.to_string ids 62 + | None -> [] 63 + in 64 + let request = { 65 + name; topic; 66 + visibility = visibility_str; 67 + preset = preset_str; 68 + room_alias_local_part; 69 + invite = invite_strs; 70 + is_direct; 71 + room_type; 72 + creation_content = None; 73 + } in 74 + match Client.encode_body create_request_jsont request with 75 + | Error e -> Error e 76 + | Ok body -> 77 + match Client.post client ~path:"/createRoom" ~body () with 78 + | Error e -> Error e 79 + | Ok body -> 80 + match Client.decode_response create_response_jsont body with 81 + | Error e -> Error e 82 + | Ok resp -> Ok resp.room_id 83 + 84 + (** Alias for create with room_type support *) 85 + let create_room = create 86 + 87 + (* Join room *) 88 + type join_request = { 89 + reason : string option; 90 + } [@@warning "-69"] 91 + 92 + let join_request_jsont = 93 + Jsont.Object.( 94 + map (fun reason -> { reason }) 95 + |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason) 96 + |> finish) 97 + 98 + type join_response = { 99 + room_id : Matrix_proto.Id.Room_id.t; 100 + } 101 + 102 + let join_response_jsont = 103 + Jsont.Object.( 104 + map (fun room_id -> { room_id }) 105 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont 106 + |> finish) 107 + 108 + let join client ~room_id_or_alias ?via ?reason () = 109 + let path = Printf.sprintf "/join/%s" (Uri.pct_encode room_id_or_alias) in 110 + let query = match via with 111 + | Some servers -> Some (List.map (fun s -> ("server_name", s)) servers) 112 + | None -> None 113 + in 114 + let request = { reason } in 115 + match Client.encode_body join_request_jsont request with 116 + | Error e -> Error e 117 + | Ok body -> 118 + match Client.post client ~path ?query ~body () with 119 + | Error e -> Error e 120 + | Ok body -> 121 + match Client.decode_response join_response_jsont body with 122 + | Error e -> Error e 123 + | Ok resp -> Ok resp.room_id 124 + 125 + (* Leave room *) 126 + type leave_request = { 127 + reason : string option; 128 + } [@@warning "-69"] 129 + 130 + let leave_request_jsont = 131 + Jsont.Object.( 132 + map (fun reason -> { reason }) 133 + |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason) 134 + |> finish) 135 + 136 + let leave client ~room_id ?reason () = 137 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 138 + let path = Printf.sprintf "/rooms/%s/leave" (Uri.pct_encode room_id_str) in 139 + let request = { reason } in 140 + match Client.encode_body leave_request_jsont request with 141 + | Error e -> Error e 142 + | Ok body -> 143 + match Client.post client ~path ~body () with 144 + | Error e -> Error e 145 + | Ok _ -> Ok () 146 + 147 + (* Forget room *) 148 + let forget client ~room_id = 149 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 150 + let path = Printf.sprintf "/rooms/%s/forget" (Uri.pct_encode room_id_str) in 151 + match Client.post client ~path ~body:"{}" () with 152 + | Error e -> Error e 153 + | Ok _ -> Ok () 154 + 155 + (* Invite *) 156 + type invite_request = { 157 + user_id : string; 158 + reason : string option; 159 + } [@@warning "-69"] 160 + 161 + let invite_request_jsont = 162 + Jsont.Object.( 163 + map (fun user_id reason -> { user_id; reason }) 164 + |> mem "user_id" Jsont.string 165 + |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason) 166 + |> finish) 167 + 168 + let invite client ~room_id ~user_id ?reason () = 169 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 170 + let path = Printf.sprintf "/rooms/%s/invite" (Uri.pct_encode room_id_str) in 171 + let request = { user_id = Matrix_proto.Id.User_id.to_string user_id; reason } in 172 + match Client.encode_body invite_request_jsont request with 173 + | Error e -> Error e 174 + | Ok body -> 175 + match Client.post client ~path ~body () with 176 + | Error e -> Error e 177 + | Ok _ -> Ok () 178 + 179 + (* Kick *) 180 + type kick_request = { 181 + user_id : string; 182 + reason : string option; 183 + } [@@warning "-69"] 184 + 185 + let kick_request_jsont = 186 + Jsont.Object.( 187 + map (fun user_id reason -> { user_id; reason }) 188 + |> mem "user_id" Jsont.string 189 + |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason) 190 + |> finish) 191 + 192 + let kick client ~room_id ~user_id ?reason () = 193 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 194 + let path = Printf.sprintf "/rooms/%s/kick" (Uri.pct_encode room_id_str) in 195 + let request = { user_id = Matrix_proto.Id.User_id.to_string user_id; reason } in 196 + match Client.encode_body kick_request_jsont request with 197 + | Error e -> Error e 198 + | Ok body -> 199 + match Client.post client ~path ~body () with 200 + | Error e -> Error e 201 + | Ok _ -> Ok () 202 + 203 + (* Ban *) 204 + type ban_request = { 205 + user_id : string; 206 + reason : string option; 207 + } [@@warning "-69"] 208 + 209 + let ban_request_jsont = 210 + Jsont.Object.( 211 + map (fun user_id reason -> { user_id; reason }) 212 + |> mem "user_id" Jsont.string 213 + |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason) 214 + |> finish) 215 + 216 + let ban client ~room_id ~user_id ?reason () = 217 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 218 + let path = Printf.sprintf "/rooms/%s/ban" (Uri.pct_encode room_id_str) in 219 + let request = { user_id = Matrix_proto.Id.User_id.to_string user_id; reason } in 220 + match Client.encode_body ban_request_jsont request with 221 + | Error e -> Error e 222 + | Ok body -> 223 + match Client.post client ~path ~body () with 224 + | Error e -> Error e 225 + | Ok _ -> Ok () 226 + 227 + (* Unban *) 228 + let unban client ~room_id ~user_id ?reason () = 229 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 230 + let path = Printf.sprintf "/rooms/%s/unban" (Uri.pct_encode room_id_str) in 231 + let request = { user_id = Matrix_proto.Id.User_id.to_string user_id; reason } in 232 + match Client.encode_body ban_request_jsont request with 233 + | Error e -> Error e 234 + | Ok body -> 235 + match Client.post client ~path ~body () with 236 + | Error e -> Error e 237 + | Ok _ -> Ok () 238 + 239 + (* Get joined rooms *) 240 + type joined_rooms_response = { 241 + joined_rooms : string list; 242 + } 243 + 244 + let joined_rooms_response_jsont = 245 + Jsont.Object.( 246 + map (fun joined_rooms -> { joined_rooms }) 247 + |> mem "joined_rooms" (Jsont.list Jsont.string) ~dec_absent:[] 248 + |> finish) 249 + 250 + let get_joined_rooms client = 251 + match Client.get client ~path:"/joined_rooms" () with 252 + | Error e -> Error e 253 + | Ok body -> 254 + match Client.decode_response joined_rooms_response_jsont body with 255 + | Error e -> Error e 256 + | Ok resp -> 257 + let room_ids = List.filter_map (fun s -> 258 + match Matrix_proto.Id.Room_id.of_string s with 259 + | Ok id -> Some id 260 + | Error _ -> None 261 + ) resp.joined_rooms in 262 + Ok room_ids 263 + 264 + (* Get members *) 265 + type member = { 266 + user_id : Matrix_proto.Id.User_id.t; 267 + display_name : string option; 268 + avatar_url : string option; 269 + membership : string; 270 + } 271 + 272 + type member_event = { 273 + state_key : string; 274 + content : member_content; 275 + } 276 + and member_content = { 277 + membership : string; 278 + displayname : string option; 279 + avatar_url : string option; 280 + } 281 + 282 + let member_content_jsont = 283 + Jsont.Object.( 284 + map (fun membership displayname avatar_url -> 285 + { membership; displayname; avatar_url }) 286 + |> mem "membership" Jsont.string 287 + |> opt_mem "displayname" Jsont.string ~enc:(fun t -> t.displayname) 288 + |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url) 289 + |> finish) 290 + 291 + let member_event_jsont = 292 + Jsont.Object.( 293 + map (fun state_key content -> { state_key; content }) 294 + |> mem "state_key" Jsont.string 295 + |> mem "content" member_content_jsont 296 + |> finish) 297 + 298 + type members_response = { 299 + chunk : member_event list; 300 + } 301 + 302 + let members_response_jsont = 303 + Jsont.Object.( 304 + map (fun chunk -> { chunk }) 305 + |> mem "chunk" (Jsont.list member_event_jsont) ~dec_absent:[] 306 + |> finish) 307 + 308 + let get_members client ~room_id ?membership ?not_membership () = 309 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 310 + let path = Printf.sprintf "/rooms/%s/members" (Uri.pct_encode room_id_str) in 311 + let query = 312 + [] 313 + |> (fun q -> match membership with Some m -> ("membership", m) :: q | None -> q) 314 + |> (fun q -> match not_membership with Some m -> ("not_membership", m) :: q | None -> q) 315 + in 316 + let query = if query = [] then None else Some query in 317 + match Client.get client ~path ?query () with 318 + | Error e -> Error e 319 + | Ok body -> 320 + match Client.decode_response members_response_jsont body with 321 + | Error e -> Error e 322 + | Ok resp -> 323 + let members = List.filter_map (fun ev -> 324 + match Matrix_proto.Id.User_id.of_string ev.state_key with 325 + | Ok user_id -> 326 + Some { 327 + user_id; 328 + display_name = ev.content.displayname; 329 + avatar_url = ev.content.avatar_url; 330 + membership = ev.content.membership; 331 + } 332 + | Error _ -> None 333 + ) resp.chunk in 334 + Ok members 335 + 336 + (* Public rooms *) 337 + type public_room = { 338 + room_id : Matrix_proto.Id.Room_id.t; 339 + name : string option; 340 + topic : string option; 341 + num_joined_members : int; 342 + world_readable : bool; 343 + guest_can_join : bool; 344 + avatar_url : string option; 345 + canonical_alias : string option; 346 + } 347 + 348 + let public_room_jsont = 349 + Jsont.Object.( 350 + map (fun room_id name topic num_joined_members world_readable guest_can_join avatar_url canonical_alias -> 351 + { room_id; name; topic; num_joined_members; world_readable; guest_can_join; avatar_url; canonical_alias }) 352 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont 353 + |> opt_mem "name" Jsont.string ~enc:(fun t -> t.name) 354 + |> opt_mem "topic" Jsont.string ~enc:(fun t -> t.topic) 355 + |> mem "num_joined_members" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.num_joined_members) 356 + |> mem "world_readable" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.world_readable) 357 + |> mem "guest_can_join" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.guest_can_join) 358 + |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url) 359 + |> opt_mem "canonical_alias" Jsont.string ~enc:(fun t -> t.canonical_alias) 360 + |> finish) 361 + 362 + type public_rooms_response = { 363 + chunk : public_room list; 364 + next_batch : string option; 365 + prev_batch : string option; 366 + total_room_count_estimate : int option; 367 + } 368 + 369 + let public_rooms_response_jsont = 370 + Jsont.Object.( 371 + map (fun chunk next_batch prev_batch total_room_count_estimate -> 372 + { chunk; next_batch; prev_batch; total_room_count_estimate }) 373 + |> mem "chunk" (Jsont.list public_room_jsont) ~dec_absent:[] 374 + |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch) 375 + |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch) 376 + |> opt_mem "total_room_count_estimate" Jsont.int ~enc:(fun t -> t.total_room_count_estimate) 377 + |> finish) 378 + 379 + let get_public_rooms client ?limit ?since ?server () = 380 + let query = 381 + [] 382 + |> (fun q -> match limit with Some l -> ("limit", string_of_int l) :: q | None -> q) 383 + |> (fun q -> match since with Some s -> ("since", s) :: q | None -> q) 384 + |> (fun q -> match server with Some s -> ("server", s) :: q | None -> q) 385 + in 386 + let query = if query = [] then None else Some query in 387 + match Client.get client ~path:"/publicRooms" ?query () with 388 + | Error e -> Error e 389 + | Ok body -> Client.decode_response public_rooms_response_jsont body 390 + 391 + (* Power levels *) 392 + type power_levels = { 393 + ban : int; 394 + events : (string * int) list; 395 + events_default : int; 396 + invite : int; 397 + kick : int; 398 + redact : int; 399 + state_default : int; 400 + users : (string * int) list; 401 + users_default : int; 402 + notifications : (string * int) list; 403 + } 404 + 405 + module StringMap = Map.Make(String) 406 + 407 + let string_int_map_jsont : (string * int) list Jsont.t = 408 + let map_jsont = Jsont.Object.as_string_map Jsont.int in 409 + Jsont.map 410 + ~dec:(fun m -> StringMap.bindings m) 411 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 412 + map_jsont 413 + 414 + let power_levels_jsont : power_levels Jsont.t = 415 + Jsont.Object.( 416 + map (fun ban events events_default invite kick redact state_default users users_default notifications -> 417 + ({ ban; events; events_default; invite; kick; redact; state_default; users; users_default; notifications } : power_levels)) 418 + |> mem "ban" Jsont.int ~dec_absent:50 ~enc:(fun (t : power_levels) -> t.ban) 419 + |> mem "events" string_int_map_jsont ~dec_absent:[] ~enc:(fun (t : power_levels) -> t.events) 420 + |> mem "events_default" Jsont.int ~dec_absent:0 ~enc:(fun (t : power_levels) -> t.events_default) 421 + |> mem "invite" Jsont.int ~dec_absent:0 ~enc:(fun (t : power_levels) -> t.invite) 422 + |> mem "kick" Jsont.int ~dec_absent:50 ~enc:(fun (t : power_levels) -> t.kick) 423 + |> mem "redact" Jsont.int ~dec_absent:50 ~enc:(fun (t : power_levels) -> t.redact) 424 + |> mem "state_default" Jsont.int ~dec_absent:50 ~enc:(fun (t : power_levels) -> t.state_default) 425 + |> mem "users" string_int_map_jsont ~dec_absent:[] ~enc:(fun (t : power_levels) -> t.users) 426 + |> mem "users_default" Jsont.int ~dec_absent:0 ~enc:(fun (t : power_levels) -> t.users_default) 427 + |> mem "notifications" string_int_map_jsont ~dec_absent:[] ~enc:(fun (t : power_levels) -> t.notifications) 428 + |> finish) 429 + 430 + let get_power_levels client ~room_id = 431 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 432 + let path = Printf.sprintf "/rooms/%s/state/m.room.power_levels/" (Uri.pct_encode room_id_str) in 433 + match Client.get client ~path () with 434 + | Error e -> Error e 435 + | Ok body -> Client.decode_response power_levels_jsont body 436 + 437 + let set_power_levels client ~room_id ~power_levels = 438 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 439 + let path = Printf.sprintf "/rooms/%s/state/m.room.power_levels/" (Uri.pct_encode room_id_str) in 440 + match Client.encode_body power_levels_jsont power_levels with 441 + | Error e -> Error e 442 + | Ok body -> 443 + match Client.put client ~path ~body () with 444 + | Error e -> Error e 445 + | Ok _ -> Ok () 446 + 447 + let get_user_power_level power_levels user_id = 448 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 449 + match List.assoc_opt user_id_str power_levels.users with 450 + | Some level -> level 451 + | None -> power_levels.users_default 452 + 453 + let set_user_power_level client ~room_id ~user_id ~level = 454 + match get_power_levels client ~room_id with 455 + | Error e -> Error e 456 + | Ok pl -> 457 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 458 + let users = List.filter (fun (k, _) -> k <> user_id_str) pl.users in 459 + let users = (user_id_str, level) :: users in 460 + let power_levels = { pl with users } in 461 + set_power_levels client ~room_id ~power_levels
+228
lib/matrix_client/rooms.mli
··· 1 + (** Room operations. *) 2 + 3 + (** {1 Room Creation} *) 4 + 5 + (** Room visibility. *) 6 + type visibility = [ `Public | `Private ] 7 + 8 + (** Room preset. *) 9 + type preset = 10 + | Private_chat 11 + | Public_chat 12 + | Trusted_private_chat 13 + 14 + (** Create a new room. 15 + 16 + Returns the room ID of the newly created room. *) 17 + val create : 18 + Client.t -> 19 + ?name:string -> 20 + ?topic:string -> 21 + ?visibility:visibility -> 22 + ?preset:preset -> 23 + ?room_alias_local_part:string -> 24 + ?invite:Matrix_proto.Id.User_id.t list -> 25 + ?is_direct:bool -> 26 + ?room_type:string -> 27 + unit -> 28 + (Matrix_proto.Id.Room_id.t, Error.t) result 29 + 30 + (** Alias for create with room_type support *) 31 + val create_room : 32 + Client.t -> 33 + ?name:string -> 34 + ?topic:string -> 35 + ?visibility:visibility -> 36 + ?preset:preset -> 37 + ?room_alias_local_part:string -> 38 + ?invite:Matrix_proto.Id.User_id.t list -> 39 + ?is_direct:bool -> 40 + ?room_type:string -> 41 + unit -> 42 + (Matrix_proto.Id.Room_id.t, Error.t) result 43 + 44 + (** {1 Joining and Leaving} *) 45 + 46 + (** Join a room by ID or alias. 47 + 48 + Returns the room ID (which may differ from input if an alias was used). *) 49 + val join : 50 + Client.t -> 51 + room_id_or_alias:string -> 52 + ?via:string list -> 53 + ?reason:string -> 54 + unit -> 55 + (Matrix_proto.Id.Room_id.t, Error.t) result 56 + 57 + (** Leave a room. *) 58 + val leave : 59 + Client.t -> 60 + room_id:Matrix_proto.Id.Room_id.t -> 61 + ?reason:string -> 62 + unit -> 63 + (unit, Error.t) result 64 + 65 + (** Forget a room (remove from room list after leaving). *) 66 + val forget : 67 + Client.t -> 68 + room_id:Matrix_proto.Id.Room_id.t -> 69 + (unit, Error.t) result 70 + 71 + (** {1 Membership Management} *) 72 + 73 + (** Invite a user to a room. *) 74 + val invite : 75 + Client.t -> 76 + room_id:Matrix_proto.Id.Room_id.t -> 77 + user_id:Matrix_proto.Id.User_id.t -> 78 + ?reason:string -> 79 + unit -> 80 + (unit, Error.t) result 81 + 82 + (** Kick a user from a room. *) 83 + val kick : 84 + Client.t -> 85 + room_id:Matrix_proto.Id.Room_id.t -> 86 + user_id:Matrix_proto.Id.User_id.t -> 87 + ?reason:string -> 88 + unit -> 89 + (unit, Error.t) result 90 + 91 + (** Ban a user from a room. *) 92 + val ban : 93 + Client.t -> 94 + room_id:Matrix_proto.Id.Room_id.t -> 95 + user_id:Matrix_proto.Id.User_id.t -> 96 + ?reason:string -> 97 + unit -> 98 + (unit, Error.t) result 99 + 100 + (** Unban a user from a room. *) 101 + val unban : 102 + Client.t -> 103 + room_id:Matrix_proto.Id.Room_id.t -> 104 + user_id:Matrix_proto.Id.User_id.t -> 105 + ?reason:string -> 106 + unit -> 107 + (unit, Error.t) result 108 + 109 + (** {1 Room Queries} *) 110 + 111 + (** Get list of joined rooms. *) 112 + val get_joined_rooms : 113 + Client.t -> 114 + (Matrix_proto.Id.Room_id.t list, Error.t) result 115 + 116 + (** Member info. *) 117 + type member = { 118 + user_id : Matrix_proto.Id.User_id.t; 119 + display_name : string option; 120 + avatar_url : string option; 121 + membership : string; 122 + } 123 + 124 + (** Get room members. 125 + 126 + @param membership Filter by membership state (join, invite, leave, ban). 127 + @param not_membership Exclude members with this membership state. *) 128 + val get_members : 129 + Client.t -> 130 + room_id:Matrix_proto.Id.Room_id.t -> 131 + ?membership:string -> 132 + ?not_membership:string -> 133 + unit -> 134 + (member list, Error.t) result 135 + 136 + (** {1 Public Rooms} *) 137 + 138 + (** Public room info. *) 139 + type public_room = { 140 + room_id : Matrix_proto.Id.Room_id.t; 141 + name : string option; 142 + topic : string option; 143 + num_joined_members : int; 144 + world_readable : bool; 145 + guest_can_join : bool; 146 + avatar_url : string option; 147 + canonical_alias : string option; 148 + } 149 + 150 + (** Public rooms response. *) 151 + type public_rooms_response = { 152 + chunk : public_room list; 153 + next_batch : string option; 154 + prev_batch : string option; 155 + total_room_count_estimate : int option; 156 + } 157 + 158 + (** Get public rooms. 159 + 160 + @param limit Maximum number of rooms to return. 161 + @param since Pagination token. 162 + @param server Server to fetch rooms from. *) 163 + val get_public_rooms : 164 + Client.t -> 165 + ?limit:int -> 166 + ?since:string -> 167 + ?server:string -> 168 + unit -> 169 + (public_rooms_response, Error.t) result 170 + 171 + (** {1 Power Levels} *) 172 + 173 + (** Room power levels. *) 174 + type power_levels = { 175 + ban : int; 176 + (** Level required to ban a user. *) 177 + events : (string * int) list; 178 + (** Event type to required power level mapping. *) 179 + events_default : int; 180 + (** Default level required to send events. *) 181 + invite : int; 182 + (** Level required to invite users. *) 183 + kick : int; 184 + (** Level required to kick users. *) 185 + redact : int; 186 + (** Level required to redact events. *) 187 + state_default : int; 188 + (** Default level required to send state events. *) 189 + users : (string * int) list; 190 + (** User ID to power level mapping. *) 191 + users_default : int; 192 + (** Default power level for users. *) 193 + notifications : (string * int) list; 194 + (** Notification power levels (e.g., "room" for @room). *) 195 + } 196 + 197 + (** Get room power levels. *) 198 + val get_power_levels : 199 + Client.t -> 200 + room_id:Matrix_proto.Id.Room_id.t -> 201 + (power_levels, Error.t) result 202 + 203 + (** Set room power levels. 204 + 205 + Requires appropriate permissions. *) 206 + val set_power_levels : 207 + Client.t -> 208 + room_id:Matrix_proto.Id.Room_id.t -> 209 + power_levels:power_levels -> 210 + (unit, Error.t) result 211 + 212 + (** Get a user's power level from power levels state. 213 + 214 + Returns users_default if user not in the users map. *) 215 + val get_user_power_level : 216 + power_levels -> 217 + Matrix_proto.Id.User_id.t -> 218 + int 219 + 220 + (** Set a specific user's power level. 221 + 222 + Fetches current power levels, modifies the user's level, and saves. *) 223 + val set_user_power_level : 224 + Client.t -> 225 + room_id:Matrix_proto.Id.Room_id.t -> 226 + user_id:Matrix_proto.Id.User_id.t -> 227 + level:int -> 228 + (unit, Error.t) result
+466
lib/matrix_client/send_queue.ml
··· 1 + (** Send queue for serialized message sending and offline support. 2 + 3 + This module provides: 4 + - Offline message queueing 5 + - Automatic retry with backoff 6 + - Transaction ID tracking for deduplication 7 + - Local echo support 8 + - Media upload coordination 9 + 10 + Each room has its own queue to serialize sends, preventing race conditions 11 + and ensuring messages are sent in order. *) 12 + 13 + (** {1 Queue Request Types} *) 14 + 15 + (** Type of queued request *) 16 + type request_kind = 17 + | Event of { 18 + event_type : string; 19 + content : Jsont.json; 20 + txn_id : string; 21 + } 22 + | MediaUpload of { 23 + content_type : string; 24 + data_size : int; 25 + local_path : string option; 26 + txn_id : string; 27 + } 28 + | Reaction of { 29 + relates_to : Matrix_proto.Id.Event_id.t; 30 + key : string; 31 + txn_id : string; 32 + } 33 + | Redaction of { 34 + event_id : Matrix_proto.Id.Event_id.t; 35 + reason : string option; 36 + txn_id : string; 37 + } 38 + 39 + (** Get transaction ID from request kind *) 40 + let txn_id_of_kind = function 41 + | Event { txn_id; _ } -> txn_id 42 + | MediaUpload { txn_id; _ } -> txn_id 43 + | Reaction { txn_id; _ } -> txn_id 44 + | Redaction { txn_id; _ } -> txn_id 45 + 46 + (** Request state *) 47 + type request_state = 48 + | Pending (** Waiting to be sent *) 49 + | Sending (** Currently being sent *) 50 + | Sent (** Successfully sent *) 51 + | Failed of string (** Failed with error message *) 52 + | Cancelled (** Cancelled by user *) 53 + 54 + (** Queued request with metadata *) 55 + type queued_request = { 56 + id : int; 57 + room_id : Matrix_proto.Id.Room_id.t; 58 + kind : request_kind; 59 + mutable state : request_state; 60 + created_at : int64; 61 + mutable retry_count : int; 62 + mutable last_error : string option; 63 + (* For dependency tracking *) 64 + mutable depends_on : int option; (** ID of parent request *) 65 + mutable dependents : int list; (** IDs of dependent requests *) 66 + } 67 + 68 + (** Result of sending a request *) 69 + type send_result = 70 + | Sent_ok of { event_id : Matrix_proto.Id.Event_id.t option } 71 + | Send_failed of { error : string; retryable : bool } 72 + | Send_cancelled 73 + 74 + (** {1 Send Handle} *) 75 + 76 + (** Handle for a queued request, allowing cancellation and status checks *) 77 + type send_handle = { 78 + request_id : int; 79 + txn_id : string; 80 + room_id : Matrix_proto.Id.Room_id.t; 81 + queue : room_send_queue; 82 + } 83 + 84 + (** Room-specific send queue *) 85 + and room_send_queue = { 86 + room_id : Matrix_proto.Id.Room_id.t; 87 + mutable requests : queued_request list; 88 + mutable next_id : int; 89 + mutable enabled : bool; 90 + mutable is_processing : bool; 91 + (* Configuration *) 92 + max_retries : int; 93 + retry_delay_ms : int; 94 + (* Callbacks *) 95 + mutable on_state_change : (queued_request -> unit) option; 96 + } 97 + 98 + (** Global send queue manager *) 99 + type t = { 100 + user_id : Matrix_proto.Id.User_id.t; 101 + mutable room_queues : (string * room_send_queue) list; 102 + mutable globally_enabled : bool; 103 + mutable on_error : (queued_request -> string -> unit) option; 104 + } 105 + 106 + (** {1 Queue Creation} *) 107 + 108 + (** Create a new room send queue *) 109 + let create_room_queue ~room_id ?(max_retries = 3) ?(retry_delay_ms = 1000) () = { 110 + room_id; 111 + requests = []; 112 + next_id = 0; 113 + enabled = true; 114 + is_processing = false; 115 + max_retries; 116 + retry_delay_ms; 117 + on_state_change = None; 118 + } 119 + 120 + (** Create a new global send queue manager *) 121 + let create ~user_id = { 122 + user_id; 123 + room_queues = []; 124 + globally_enabled = true; 125 + on_error = None; 126 + } 127 + 128 + (** Get or create a room queue *) 129 + let get_room_queue t room_id = 130 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 131 + match List.assoc_opt room_id_str t.room_queues with 132 + | Some queue -> queue 133 + | None -> 134 + let queue = create_room_queue ~room_id () in 135 + t.room_queues <- (room_id_str, queue) :: t.room_queues; 136 + queue 137 + 138 + (** {1 Enqueueing Requests} *) 139 + 140 + (** Generate a new transaction ID *) 141 + let generate_txn_id () = 142 + let random_bytes = Mirage_crypto_rng.generate 16 in 143 + "m" ^ (Base64.encode_string ~pad:false random_bytes) 144 + 145 + (** Enqueue a request *) 146 + let enqueue (queue : room_send_queue) kind = 147 + let id = queue.next_id in 148 + queue.next_id <- queue.next_id + 1; 149 + let request = { 150 + id; 151 + room_id = queue.room_id; 152 + kind; 153 + state = Pending; 154 + created_at = Int64.of_float (Unix.gettimeofday () *. 1000.0); 155 + retry_count = 0; 156 + last_error = None; 157 + depends_on = None; 158 + dependents = []; 159 + } in 160 + queue.requests <- queue.requests @ [request]; 161 + let handle = { 162 + request_id = id; 163 + txn_id = txn_id_of_kind kind; 164 + room_id = queue.room_id; 165 + queue; 166 + } in 167 + (Option.iter (fun cb -> cb request) queue.on_state_change); 168 + handle 169 + 170 + (** Enqueue a message event *) 171 + let send_message t ~room_id ~event_type ~content = 172 + let queue = get_room_queue t room_id in 173 + let txn_id = generate_txn_id () in 174 + enqueue queue (Event { event_type; content; txn_id }) 175 + 176 + (** Enqueue a text message *) 177 + let send_text t ~room_id ~body = 178 + let content = Jsont.Object ( 179 + [(("msgtype", Jsont.Meta.none), Jsont.String ("m.text", Jsont.Meta.none)); 180 + (("body", Jsont.Meta.none), Jsont.String (body, Jsont.Meta.none))], 181 + Jsont.Meta.none 182 + ) in 183 + send_message t ~room_id ~event_type:"m.room.message" ~content 184 + 185 + (** Enqueue a reaction *) 186 + let send_reaction t ~room_id ~relates_to ~key = 187 + let queue = get_room_queue t room_id in 188 + let txn_id = generate_txn_id () in 189 + enqueue queue (Reaction { relates_to; key; txn_id }) 190 + 191 + (** Enqueue a redaction *) 192 + let send_redaction t ~room_id ~event_id ?reason () = 193 + let queue = get_room_queue t room_id in 194 + let txn_id = generate_txn_id () in 195 + enqueue queue (Redaction { event_id; reason; txn_id }) 196 + 197 + (** {1 Dependencies} *) 198 + 199 + (** Add a dependency between requests *) 200 + let add_dependency ~parent:parent_handle ~child:child_handle = 201 + let queue = parent_handle.queue in 202 + match 203 + List.find_opt (fun r -> r.id = parent_handle.request_id) queue.requests, 204 + List.find_opt (fun r -> r.id = child_handle.request_id) queue.requests 205 + with 206 + | Some parent, Some child -> 207 + child.depends_on <- Some parent.id; 208 + parent.dependents <- child.id :: parent.dependents 209 + | _ -> () 210 + 211 + (** Check if a request's dependencies are satisfied *) 212 + let dependencies_satisfied queue request = 213 + match request.depends_on with 214 + | None -> true 215 + | Some parent_id -> 216 + match List.find_opt (fun r -> r.id = parent_id) queue.requests with 217 + | Some parent -> parent.state = Sent 218 + | None -> true (* Parent removed, assume satisfied *) 219 + 220 + (** {1 Request State Management} *) 221 + 222 + (** Update request state *) 223 + let update_state queue request new_state = 224 + request.state <- new_state; 225 + Option.iter (fun cb -> cb request) queue.on_state_change 226 + 227 + (** Cancel a queued request *) 228 + let cancel handle = 229 + let queue = handle.queue in 230 + match List.find_opt (fun r -> r.id = handle.request_id) queue.requests with 231 + | Some request when request.state = Pending -> 232 + update_state queue request Cancelled; 233 + true 234 + | _ -> false (* Can't cancel if already sending/sent *) 235 + 236 + (** Abort a request (cancel and remove) *) 237 + let abort handle = 238 + if cancel handle then begin 239 + let queue = handle.queue in 240 + queue.requests <- List.filter (fun r -> r.id <> handle.request_id) queue.requests; 241 + true 242 + end else 243 + false 244 + 245 + (** Get request by handle *) 246 + let get_request handle = 247 + List.find_opt (fun r -> r.id = handle.request_id) handle.queue.requests 248 + 249 + (** Check if request is still pending *) 250 + let is_pending handle = 251 + match get_request handle with 252 + | Some r -> r.state = Pending 253 + | None -> false 254 + 255 + (** Check if request was sent *) 256 + let is_sent handle = 257 + match get_request handle with 258 + | Some r -> r.state = Sent 259 + | None -> false 260 + 261 + (** {1 Queue Processing} *) 262 + 263 + (** Get next sendable request from queue *) 264 + let next_sendable queue = 265 + if not queue.enabled then None 266 + else 267 + List.find_opt (fun r -> 268 + r.state = Pending && 269 + dependencies_satisfied queue r && 270 + r.retry_count < queue.max_retries 271 + ) queue.requests 272 + 273 + (** Mark request as being sent *) 274 + let mark_sending queue request = 275 + update_state queue request Sending 276 + 277 + (** Mark request as successfully sent *) 278 + let mark_sent queue request = 279 + update_state queue request Sent 280 + 281 + (** Mark request as failed with optional retry *) 282 + let mark_failed queue request error ~retryable = 283 + request.retry_count <- request.retry_count + 1; 284 + request.last_error <- Some error; 285 + if retryable && request.retry_count < queue.max_retries then 286 + update_state queue request Pending (* Will retry *) 287 + else 288 + update_state queue request (Failed error) 289 + 290 + (** Remove completed/cancelled/failed requests *) 291 + let cleanup_queue queue = 292 + queue.requests <- List.filter (fun r -> 293 + match r.state with 294 + | Sent | Cancelled | Failed _ -> false 295 + | Pending | Sending -> true 296 + ) queue.requests 297 + 298 + (** {1 Queue Statistics} *) 299 + 300 + (** Count of pending requests in a room queue *) 301 + let pending_count queue = 302 + List.length (List.filter (fun r -> r.state = Pending) queue.requests) 303 + 304 + (** Count of all pending requests across all rooms *) 305 + let total_pending t = 306 + List.fold_left (fun acc (_, queue) -> 307 + acc + pending_count queue 308 + ) 0 t.room_queues 309 + 310 + (** Get all pending requests for a room *) 311 + let pending_requests queue = 312 + List.filter (fun r -> r.state = Pending) queue.requests 313 + 314 + (** Get all failed requests for a room *) 315 + let failed_requests queue = 316 + List.filter (fun r -> match r.state with Failed _ -> true | _ -> false) queue.requests 317 + 318 + (** {1 Queue Control} *) 319 + 320 + (** Enable/disable a room queue *) 321 + let set_room_enabled queue enabled = 322 + queue.enabled <- enabled 323 + 324 + (** Enable/disable all queues globally *) 325 + let set_enabled t enabled = 326 + t.globally_enabled <- enabled; 327 + List.iter (fun (_, queue) -> 328 + queue.enabled <- enabled 329 + ) t.room_queues 330 + 331 + (** Check if globally enabled *) 332 + let is_enabled t = t.globally_enabled 333 + 334 + (** Check if a room queue is enabled *) 335 + let is_room_enabled queue = queue.enabled 336 + 337 + (** {1 Event Callbacks} *) 338 + 339 + (** Set callback for state changes *) 340 + let on_state_change queue callback = 341 + queue.on_state_change <- Some callback 342 + 343 + (** Set global error callback *) 344 + let on_error t callback = 345 + t.on_error <- Some callback 346 + 347 + (** {1 Persistence} *) 348 + 349 + (** Serializable queue state *) 350 + type persisted_request = { 351 + p_room_id : string; 352 + p_kind : request_kind; 353 + p_created_at : int64; 354 + p_retry_count : int; 355 + p_depends_on : int option; 356 + } 357 + 358 + (** Convert request to persistable form *) 359 + let request_to_persisted (request : queued_request) = { 360 + p_room_id = Matrix_proto.Id.Room_id.to_string request.room_id; 361 + p_kind = request.kind; 362 + p_created_at = request.created_at; 363 + p_retry_count = request.retry_count; 364 + p_depends_on = request.depends_on; 365 + } 366 + 367 + (** Get all pending requests for persistence *) 368 + let requests_to_persist t = 369 + List.concat_map (fun (_, queue) -> 370 + pending_requests queue |> List.map request_to_persisted 371 + ) t.room_queues 372 + 373 + (** Restore requests from persistence *) 374 + let restore_requests t persisted_requests = 375 + List.iter (fun p -> 376 + match Matrix_proto.Id.Room_id.of_string p.p_room_id with 377 + | Error _ -> () (* Skip invalid room IDs *) 378 + | Ok room_id -> 379 + let queue = get_room_queue t room_id in 380 + let id = queue.next_id in 381 + queue.next_id <- queue.next_id + 1; 382 + let request = { 383 + id; 384 + room_id; 385 + kind = p.p_kind; 386 + state = Pending; 387 + created_at = p.p_created_at; 388 + retry_count = p.p_retry_count; 389 + last_error = None; 390 + depends_on = p.p_depends_on; 391 + dependents = []; 392 + } in 393 + queue.requests <- queue.requests @ [request] 394 + ) persisted_requests 395 + 396 + (** {1 Local Echo} *) 397 + 398 + (** Create a local echo event from a queued request *) 399 + let local_echo_event request = 400 + match request.kind with 401 + | Event { event_type; content; txn_id } -> 402 + Some (event_type, content, txn_id) 403 + | Reaction { relates_to; key; txn_id } -> 404 + let event_id = Matrix_proto.Id.Event_id.to_string relates_to in 405 + let content = Jsont.Object ( 406 + [(("m.relates_to", Jsont.Meta.none), 407 + Jsont.Object ( 408 + [(("rel_type", Jsont.Meta.none), Jsont.String ("m.annotation", Jsont.Meta.none)); 409 + (("event_id", Jsont.Meta.none), Jsont.String (event_id, Jsont.Meta.none)); 410 + (("key", Jsont.Meta.none), Jsont.String (key, Jsont.Meta.none))], 411 + Jsont.Meta.none))], 412 + Jsont.Meta.none 413 + ) in 414 + Some ("m.reaction", content, txn_id) 415 + | MediaUpload _ -> None 416 + | Redaction _ -> None 417 + 418 + (** Check if an event_id matches a transaction ID (for local echo replacement) *) 419 + let matches_txn_id request ~event_id = 420 + (* The event_id might contain the txn_id for local echoes *) 421 + let txn = txn_id_of_kind request.kind in 422 + String.equal (Matrix_proto.Id.Event_id.to_string event_id) ("$" ^ txn) 423 + 424 + (** {1 Retry Logic} *) 425 + 426 + (** Calculate delay for next retry (exponential backoff) *) 427 + let retry_delay queue request = 428 + let base_delay = queue.retry_delay_ms in 429 + let multiplier = 1 lsl request.retry_count in (* 2^retry_count *) 430 + min (base_delay * multiplier) 60000 (* Cap at 60 seconds *) 431 + 432 + (** Check if a request should be retried *) 433 + let should_retry queue request = 434 + request.retry_count < queue.max_retries && 435 + match request.state with 436 + | Failed _ -> false (* Already marked as terminal failure *) 437 + | Pending -> true (* Will be retried *) 438 + | _ -> false 439 + 440 + (** {1 Media Upload Support} *) 441 + 442 + (** Create a media upload request with dependent event send *) 443 + let send_media t ~room_id ~content_type ~data_size ?local_path ~event_content () = 444 + let queue = get_room_queue t room_id in 445 + 446 + (* First, create the upload request *) 447 + let upload_txn_id = generate_txn_id () in 448 + let upload_handle = enqueue queue (MediaUpload { 449 + content_type; 450 + data_size; 451 + local_path; 452 + txn_id = upload_txn_id; 453 + }) in 454 + 455 + (* Then create the event request that depends on it *) 456 + let event_txn_id = generate_txn_id () in 457 + let event_handle = enqueue queue (Event { 458 + event_type = "m.room.message"; 459 + content = event_content; 460 + txn_id = event_txn_id; 461 + }) in 462 + 463 + (* Set up dependency *) 464 + add_dependency ~parent:upload_handle ~child:event_handle; 465 + 466 + (upload_handle, event_handle)
+500
lib/matrix_client/sliding_sync.ml
··· 1 + (** Sliding Sync (MSC3575) - Efficient sync protocol. 2 + 3 + Sliding sync is a more efficient alternative to the traditional /sync 4 + endpoint, designed for clients with many rooms. *) 5 + 6 + (** Room subscription mode *) 7 + type room_subscription = { 8 + required_state : (string * string) list; (* event_type, state_key pairs *) 9 + timeline_limit : int option; 10 + include_old_rooms : bool option; 11 + } 12 + 13 + let state_pair_list_jsont = 14 + (* Decode array of [event_type, state_key] pairs *) 15 + Jsont.list (Jsont.list Jsont.string) 16 + |> Jsont.map 17 + ~dec:(fun pairs -> 18 + List.filter_map (function 19 + | [et; sk] -> Some (et, sk) 20 + | _ -> None) pairs) 21 + ~enc:(fun pairs -> 22 + List.map (fun (et, sk) -> [et; sk]) pairs) 23 + 24 + let room_subscription_jsont = 25 + Jsont.Object.( 26 + map (fun required_state timeline_limit include_old_rooms -> 27 + { required_state; timeline_limit; include_old_rooms }) 28 + |> mem "required_state" state_pair_list_jsont ~dec_absent:[] 29 + ~enc:(fun t -> t.required_state) 30 + |> opt_mem "timeline_limit" Jsont.int ~enc:(fun t -> t.timeline_limit) 31 + |> opt_mem "include_old_rooms" Jsont.bool ~enc:(fun t -> t.include_old_rooms) 32 + |> finish) 33 + 34 + (** List operation for sliding window *) 35 + type list_op = 36 + | Sync of int * int (* start, end - request this range *) 37 + | Insert of int * string (* index, room_id *) 38 + | Delete of int (* index *) 39 + | Invalidate of int * int (* start, end *) 40 + 41 + (** Sliding sync list configuration *) 42 + type list_config = { 43 + ranges : (int * int) list; 44 + sort : string list option; 45 + required_state : (string * string) list; 46 + timeline_limit : int option; 47 + filters : list_filters option; 48 + bump_event_types : string list option; 49 + } 50 + 51 + and list_filters = { 52 + is_dm : bool option; 53 + spaces : string list option; 54 + is_encrypted : bool option; 55 + is_invite : bool option; 56 + room_types : string list option; 57 + not_room_types : string list option; 58 + room_name_like : string option; 59 + tags : string list option; 60 + not_tags : string list option; 61 + } 62 + 63 + let list_filters_jsont = 64 + Jsont.Object.( 65 + map (fun is_dm spaces is_encrypted is_invite room_types not_room_types 66 + room_name_like tags not_tags -> 67 + { is_dm; spaces; is_encrypted; is_invite; room_types; not_room_types; 68 + room_name_like; tags; not_tags }) 69 + |> opt_mem "is_dm" Jsont.bool ~enc:(fun t -> t.is_dm) 70 + |> opt_mem "spaces" (Jsont.list Jsont.string) ~enc:(fun t -> t.spaces) 71 + |> opt_mem "is_encrypted" Jsont.bool ~enc:(fun t -> t.is_encrypted) 72 + |> opt_mem "is_invite" Jsont.bool ~enc:(fun t -> t.is_invite) 73 + |> opt_mem "room_types" (Jsont.list Jsont.string) ~enc:(fun t -> t.room_types) 74 + |> opt_mem "not_room_types" (Jsont.list Jsont.string) ~enc:(fun t -> t.not_room_types) 75 + |> opt_mem "room_name_like" Jsont.string ~enc:(fun t -> t.room_name_like) 76 + |> opt_mem "tags" (Jsont.list Jsont.string) ~enc:(fun t -> t.tags) 77 + |> opt_mem "not_tags" (Jsont.list Jsont.string) ~enc:(fun t -> t.not_tags) 78 + |> finish) 79 + 80 + let range_jsont = 81 + (* Decode [start, end] pair as (int * int) *) 82 + Jsont.list Jsont.int 83 + |> Jsont.map 84 + ~dec:(function 85 + | [a; b] -> (a, b) 86 + | _ -> (0, 0)) 87 + ~enc:(fun (a, b) -> [a; b]) 88 + 89 + let ranges_jsont = 90 + (* List of ranges *) 91 + Jsont.list range_jsont 92 + 93 + let state_pair_jsont = 94 + (* Decode [event_type, state_key] as (string * string) *) 95 + Jsont.list Jsont.string 96 + |> Jsont.map 97 + ~dec:(function 98 + | [et; sk] -> (et, sk) 99 + | _ -> ("", "")) 100 + ~enc:(fun (et, sk) -> [et; sk]) 101 + 102 + let list_config_jsont = 103 + Jsont.Object.( 104 + map (fun ranges sort required_state timeline_limit filters bump_event_types -> 105 + { ranges; sort; required_state; timeline_limit; filters; bump_event_types }) 106 + |> mem "ranges" ranges_jsont ~dec_absent:[] ~enc:(fun t -> t.ranges) 107 + |> opt_mem "sort" (Jsont.list Jsont.string) ~enc:(fun t -> t.sort) 108 + |> mem "required_state" (Jsont.list state_pair_jsont) ~dec_absent:[] 109 + ~enc:(fun t -> t.required_state) 110 + |> opt_mem "timeline_limit" Jsont.int ~enc:(fun t -> t.timeline_limit) 111 + |> opt_mem "filters" list_filters_jsont ~enc:(fun t -> t.filters) 112 + |> opt_mem "bump_event_types" (Jsont.list Jsont.string) 113 + ~enc:(fun t -> t.bump_event_types) 114 + |> finish) 115 + 116 + (** Sliding sync request *) 117 + type request = { 118 + lists : (string * list_config) list; 119 + room_subscriptions : (string * room_subscription) list; 120 + unsubscribe_rooms : string list; 121 + extensions : extensions option; 122 + pos : string option; 123 + timeout : int option; 124 + } 125 + 126 + and extensions = { 127 + to_device : to_device_ext option; 128 + e2ee : e2ee_ext option; 129 + account_data : account_data_ext option; 130 + typing : typing_ext option; 131 + receipts : receipts_ext option; 132 + } 133 + 134 + and to_device_ext = { 135 + enabled : bool; 136 + since : string option; 137 + limit : int option; 138 + } 139 + 140 + and e2ee_ext = { 141 + enabled : bool; 142 + } 143 + 144 + and account_data_ext = { 145 + enabled : bool; 146 + lists : string list option; 147 + rooms : string list option; 148 + } 149 + 150 + and typing_ext = { 151 + enabled : bool; 152 + lists : string list option; 153 + rooms : string list option; 154 + } 155 + 156 + and receipts_ext = { 157 + enabled : bool; 158 + lists : string list option; 159 + rooms : string list option; 160 + } 161 + 162 + let to_device_ext_jsont = 163 + Jsont.Object.( 164 + map (fun enabled since limit -> { enabled; since; limit }) 165 + |> mem "enabled" Jsont.bool ~enc:(fun t -> t.enabled) 166 + |> opt_mem "since" Jsont.string ~enc:(fun t -> t.since) 167 + |> opt_mem "limit" Jsont.int ~enc:(fun t -> t.limit) 168 + |> finish) 169 + 170 + let e2ee_ext_jsont = 171 + Jsont.Object.( 172 + map (fun enabled -> ({ enabled } : e2ee_ext)) 173 + |> mem "enabled" Jsont.bool ~enc:(fun (t : e2ee_ext) -> t.enabled) 174 + |> finish) 175 + 176 + let account_data_ext_jsont = 177 + Jsont.Object.( 178 + map (fun enabled lists rooms -> ({ enabled; lists; rooms } : account_data_ext)) 179 + |> mem "enabled" Jsont.bool ~enc:(fun (t : account_data_ext) -> t.enabled) 180 + |> opt_mem "lists" (Jsont.list Jsont.string) ~enc:(fun (t : account_data_ext) -> t.lists) 181 + |> opt_mem "rooms" (Jsont.list Jsont.string) ~enc:(fun (t : account_data_ext) -> t.rooms) 182 + |> finish) 183 + 184 + let typing_ext_jsont = 185 + Jsont.Object.( 186 + map (fun enabled lists rooms -> ({ enabled; lists; rooms } : typing_ext)) 187 + |> mem "enabled" Jsont.bool ~enc:(fun (t : typing_ext) -> t.enabled) 188 + |> opt_mem "lists" (Jsont.list Jsont.string) ~enc:(fun (t : typing_ext) -> t.lists) 189 + |> opt_mem "rooms" (Jsont.list Jsont.string) ~enc:(fun (t : typing_ext) -> t.rooms) 190 + |> finish) 191 + 192 + let receipts_ext_jsont = 193 + Jsont.Object.( 194 + map (fun enabled lists rooms -> ({ enabled; lists; rooms } : receipts_ext)) 195 + |> mem "enabled" Jsont.bool ~enc:(fun (t : receipts_ext) -> t.enabled) 196 + |> opt_mem "lists" (Jsont.list Jsont.string) ~enc:(fun (t : receipts_ext) -> t.lists) 197 + |> opt_mem "rooms" (Jsont.list Jsont.string) ~enc:(fun (t : receipts_ext) -> t.rooms) 198 + |> finish) 199 + 200 + let extensions_jsont = 201 + Jsont.Object.( 202 + map (fun to_device e2ee account_data typing receipts -> 203 + { to_device; e2ee; account_data; typing; receipts }) 204 + |> opt_mem "to_device" to_device_ext_jsont ~enc:(fun t -> t.to_device) 205 + |> opt_mem "e2ee" e2ee_ext_jsont ~enc:(fun t -> t.e2ee) 206 + |> opt_mem "account_data" account_data_ext_jsont ~enc:(fun t -> t.account_data) 207 + |> opt_mem "typing" typing_ext_jsont ~enc:(fun t -> t.typing) 208 + |> opt_mem "receipts" receipts_ext_jsont ~enc:(fun t -> t.receipts) 209 + |> finish) 210 + 211 + module StringMap = Map.Make(String) 212 + 213 + let string_map_jsont value_jsont = 214 + Jsont.Object.as_string_map value_jsont 215 + |> Jsont.map 216 + ~dec:(fun m -> StringMap.bindings m) 217 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 218 + 219 + let request_jsont = 220 + Jsont.Object.( 221 + map (fun lists room_subscriptions unsubscribe_rooms extensions pos timeout -> 222 + { lists; room_subscriptions; unsubscribe_rooms; extensions; pos; timeout }) 223 + |> mem "lists" (string_map_jsont list_config_jsont) ~dec_absent:[] 224 + ~enc:(fun t -> t.lists) 225 + |> mem "room_subscriptions" (string_map_jsont room_subscription_jsont) ~dec_absent:[] 226 + ~enc:(fun t -> t.room_subscriptions) 227 + |> mem "unsubscribe_rooms" (Jsont.list Jsont.string) ~dec_absent:[] 228 + ~enc:(fun t -> t.unsubscribe_rooms) 229 + |> opt_mem "extensions" extensions_jsont ~enc:(fun t -> t.extensions) 230 + |> opt_mem "pos" Jsont.string ~enc:(fun t -> t.pos) 231 + |> opt_mem "timeout" Jsont.int ~enc:(fun t -> t.timeout) 232 + |> finish) 233 + 234 + (** Sliding sync response room data *) 235 + type room_response = { 236 + name : string option; 237 + avatar : string option; 238 + heroes : hero list option; 239 + is_dm : bool option; 240 + initial : bool option; 241 + required_state : Jsont.json list; 242 + timeline : Jsont.json list; 243 + prev_batch : string option; 244 + limited : bool option; 245 + joined_count : int option; 246 + invited_count : int option; 247 + notification_count : int option; 248 + highlight_count : int option; 249 + num_live : int option; 250 + timestamp : int64 option; 251 + } 252 + 253 + and hero = { 254 + user_id : string; 255 + name : string option; 256 + avatar : string option; 257 + } 258 + 259 + let hero_jsont = 260 + Jsont.Object.( 261 + map (fun user_id name avatar -> ({ user_id; name; avatar } : hero)) 262 + |> mem "user_id" Jsont.string ~enc:(fun (t : hero) -> t.user_id) 263 + |> opt_mem "name" Jsont.string ~enc:(fun (t : hero) -> t.name) 264 + |> opt_mem "avatar" Jsont.string ~enc:(fun (t : hero) -> t.avatar) 265 + |> finish) 266 + 267 + let room_response_jsont = 268 + Jsont.Object.( 269 + map (fun name avatar heroes is_dm initial required_state timeline 270 + prev_batch limited joined_count invited_count notification_count 271 + highlight_count num_live timestamp -> 272 + ({ name; avatar; heroes; is_dm; initial; required_state; timeline; 273 + prev_batch; limited; joined_count; invited_count; notification_count; 274 + highlight_count; num_live; timestamp } : room_response)) 275 + |> opt_mem "name" Jsont.string ~enc:(fun (t : room_response) -> t.name) 276 + |> opt_mem "avatar" Jsont.string ~enc:(fun (t : room_response) -> t.avatar) 277 + |> opt_mem "heroes" (Jsont.list hero_jsont) ~enc:(fun (t : room_response) -> t.heroes) 278 + |> opt_mem "is_dm" Jsont.bool ~enc:(fun (t : room_response) -> t.is_dm) 279 + |> opt_mem "initial" Jsont.bool ~enc:(fun (t : room_response) -> t.initial) 280 + |> mem "required_state" (Jsont.list Jsont.json) ~dec_absent:[] 281 + ~enc:(fun (t : room_response) -> t.required_state) 282 + |> mem "timeline" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun (t : room_response) -> t.timeline) 283 + |> opt_mem "prev_batch" Jsont.string ~enc:(fun (t : room_response) -> t.prev_batch) 284 + |> opt_mem "limited" Jsont.bool ~enc:(fun (t : room_response) -> t.limited) 285 + |> opt_mem "joined_count" Jsont.int ~enc:(fun (t : room_response) -> t.joined_count) 286 + |> opt_mem "invited_count" Jsont.int ~enc:(fun (t : room_response) -> t.invited_count) 287 + |> opt_mem "notification_count" Jsont.int ~enc:(fun (t : room_response) -> t.notification_count) 288 + |> opt_mem "highlight_count" Jsont.int ~enc:(fun (t : room_response) -> t.highlight_count) 289 + |> opt_mem "num_live" Jsont.int ~enc:(fun (t : room_response) -> t.num_live) 290 + |> opt_mem "timestamp" Jsont.int64 ~enc:(fun (t : room_response) -> t.timestamp) 291 + |> finish) 292 + 293 + (** Sliding sync list response *) 294 + type list_response = { 295 + count : int; 296 + ops : list_op_response list; 297 + } 298 + 299 + and list_op_response = { 300 + op : string; 301 + range : (int * int) option; 302 + index : int option; 303 + room_ids : string list option; 304 + room_id : string option; 305 + } 306 + 307 + let list_op_response_jsont = 308 + Jsont.Object.( 309 + map (fun op range index room_ids room_id -> 310 + { op; range; index; room_ids; room_id }) 311 + |> mem "op" Jsont.string ~enc:(fun t -> t.op) 312 + |> opt_mem "range" range_jsont ~enc:(fun t -> t.range) 313 + |> opt_mem "index" Jsont.int ~enc:(fun t -> t.index) 314 + |> opt_mem "room_ids" (Jsont.list Jsont.string) ~enc:(fun t -> t.room_ids) 315 + |> opt_mem "room_id" Jsont.string ~enc:(fun t -> t.room_id) 316 + |> finish) 317 + 318 + let list_response_jsont = 319 + Jsont.Object.( 320 + map (fun count ops -> { count; ops }) 321 + |> mem "count" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.count) 322 + |> mem "ops" (Jsont.list list_op_response_jsont) ~dec_absent:[] ~enc:(fun t -> t.ops) 323 + |> finish) 324 + 325 + (** Extensions response *) 326 + type extensions_response = { 327 + to_device : to_device_response option; 328 + e2ee : e2ee_response option; 329 + account_data : account_data_response option; 330 + typing : typing_response option; 331 + receipts : receipts_response option; 332 + } 333 + 334 + and to_device_response = { 335 + next_batch : string; 336 + events : Jsont.json list; 337 + } 338 + 339 + and e2ee_response = { 340 + device_lists : device_lists option; 341 + device_one_time_keys_count : (string * int) list; 342 + device_unused_fallback_key_types : string list; 343 + } 344 + 345 + and device_lists = { 346 + changed : string list; 347 + left : string list; 348 + } 349 + 350 + and account_data_response = { 351 + global : Jsont.json list; 352 + rooms : (string * Jsont.json list) list; 353 + } 354 + 355 + and typing_response = { 356 + rooms : (string * string list) list; (* room_id -> typing user_ids *) 357 + } 358 + 359 + and receipts_response = { 360 + rooms : (string * Jsont.json) list; (* room_id -> receipt content *) 361 + } 362 + 363 + let to_device_response_jsont = 364 + Jsont.Object.( 365 + map (fun next_batch events -> { next_batch; events }) 366 + |> mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch) 367 + |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.events) 368 + |> finish) 369 + 370 + let device_lists_jsont = 371 + Jsont.Object.( 372 + map (fun changed left -> { changed; left }) 373 + |> mem "changed" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.changed) 374 + |> mem "left" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.left) 375 + |> finish) 376 + 377 + let int_map_jsont = 378 + Jsont.Object.as_string_map Jsont.int 379 + |> Jsont.map 380 + ~dec:(fun m -> StringMap.bindings m) 381 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 382 + 383 + let e2ee_response_jsont = 384 + Jsont.Object.( 385 + map (fun device_lists device_one_time_keys_count device_unused_fallback_key_types -> 386 + { device_lists; device_one_time_keys_count; device_unused_fallback_key_types }) 387 + |> opt_mem "device_lists" device_lists_jsont ~enc:(fun t -> t.device_lists) 388 + |> mem "device_one_time_keys_count" int_map_jsont ~dec_absent:[] 389 + ~enc:(fun t -> t.device_one_time_keys_count) 390 + |> mem "device_unused_fallback_key_types" (Jsont.list Jsont.string) ~dec_absent:[] 391 + ~enc:(fun t -> t.device_unused_fallback_key_types) 392 + |> finish) 393 + 394 + let json_list_map_jsont = 395 + Jsont.Object.as_string_map (Jsont.list Jsont.json) 396 + |> Jsont.map 397 + ~dec:(fun m -> StringMap.bindings m) 398 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 399 + 400 + let account_data_response_jsont = 401 + Jsont.Object.( 402 + map (fun global rooms -> { global; rooms }) 403 + |> mem "global" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.global) 404 + |> mem "rooms" json_list_map_jsont ~dec_absent:[] ~enc:(fun t -> t.rooms) 405 + |> finish) 406 + 407 + let string_list_map_jsont = 408 + Jsont.Object.as_string_map (Jsont.list Jsont.string) 409 + |> Jsont.map 410 + ~dec:(fun m -> StringMap.bindings m) 411 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 412 + 413 + let typing_response_jsont = 414 + Jsont.Object.( 415 + map (fun rooms -> ({ rooms } : typing_response)) 416 + |> mem "rooms" string_list_map_jsont ~dec_absent:[] ~enc:(fun (t : typing_response) -> t.rooms) 417 + |> finish) 418 + 419 + let json_map_jsont = 420 + Jsont.Object.as_string_map Jsont.json 421 + |> Jsont.map 422 + ~dec:(fun m -> StringMap.bindings m) 423 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 424 + 425 + let receipts_response_jsont = 426 + Jsont.Object.( 427 + map (fun rooms -> ({ rooms } : receipts_response)) 428 + |> mem "rooms" json_map_jsont ~dec_absent:[] ~enc:(fun (t : receipts_response) -> t.rooms) 429 + |> finish) 430 + 431 + let extensions_response_jsont = 432 + Jsont.Object.( 433 + map (fun to_device e2ee account_data typing receipts -> 434 + { to_device; e2ee; account_data; typing; receipts }) 435 + |> opt_mem "to_device" to_device_response_jsont ~enc:(fun t -> t.to_device) 436 + |> opt_mem "e2ee" e2ee_response_jsont ~enc:(fun t -> t.e2ee) 437 + |> opt_mem "account_data" account_data_response_jsont ~enc:(fun t -> t.account_data) 438 + |> opt_mem "typing" typing_response_jsont ~enc:(fun t -> t.typing) 439 + |> opt_mem "receipts" receipts_response_jsont ~enc:(fun t -> t.receipts) 440 + |> finish) 441 + 442 + (** Full sliding sync response *) 443 + type response = { 444 + pos : string; 445 + lists : (string * list_response) list; 446 + rooms : (string * room_response) list; 447 + extensions : extensions_response option; 448 + } 449 + 450 + let response_jsont = 451 + Jsont.Object.( 452 + map (fun pos lists rooms extensions -> 453 + { pos; lists; rooms; extensions }) 454 + |> mem "pos" Jsont.string ~enc:(fun t -> t.pos) 455 + |> mem "lists" (string_map_jsont list_response_jsont) ~dec_absent:[] 456 + ~enc:(fun t -> t.lists) 457 + |> mem "rooms" (string_map_jsont room_response_jsont) ~dec_absent:[] 458 + ~enc:(fun t -> t.rooms) 459 + |> opt_mem "extensions" extensions_response_jsont ~enc:(fun t -> t.extensions) 460 + |> finish) 461 + 462 + (** Perform a sliding sync request. 463 + 464 + This is the main entry point for sliding sync. The client should maintain 465 + the position token and include it in subsequent requests. *) 466 + let sync client ~request () = 467 + match Client.encode_body request_jsont request with 468 + | Error e -> Error e 469 + | Ok body -> 470 + (* Note: timeout handling would need to be done at the HTTP client level *) 471 + let _ = request.timeout in 472 + match Client.post client ~path:"/sync" ~body () with 473 + | Error e -> Error e 474 + | Ok resp_body -> Client.decode_response response_jsont resp_body 475 + 476 + (** Create a default request for initial sync. *) 477 + let initial_request ?(timeline_limit = 20) ?(room_limit = 20) () = 478 + { 479 + lists = [ 480 + ("all_rooms", { 481 + ranges = [(0, room_limit - 1)]; 482 + sort = Some ["by_recency"; "by_name"]; 483 + required_state = [("m.room.name", ""); ("m.room.avatar", "")]; 484 + timeline_limit = Some timeline_limit; 485 + filters = None; 486 + bump_event_types = Some ["m.room.message"; "m.room.encrypted"]; 487 + }) 488 + ]; 489 + room_subscriptions = []; 490 + unsubscribe_rooms = []; 491 + extensions = Some { 492 + to_device = Some { enabled = true; since = None; limit = Some 100 }; 493 + e2ee = Some { enabled = true }; 494 + account_data = Some { enabled = true; lists = None; rooms = None }; 495 + typing = Some { enabled = true; lists = None; rooms = None }; 496 + receipts = Some { enabled = true; lists = None; rooms = None }; 497 + }; 498 + pos = None; 499 + timeout = Some 30000; 500 + }
+212
lib/matrix_client/spaces.ml
··· 1 + (** Space operations for Matrix spaces (MSC1772). 2 + 3 + Spaces are special rooms with type "m.space" that contain child rooms 4 + via m.space.child state events. *) 5 + 6 + (** Space hierarchy information *) 7 + type space_room = { 8 + room_id : Matrix_proto.Id.Room_id.t; 9 + name : string option; 10 + topic : string option; 11 + canonical_alias : Matrix_proto.Id.Room_alias.t option; 12 + avatar_url : string option; 13 + num_joined_members : int; 14 + room_type : string option; 15 + join_rule : Matrix_proto.Event.Join_rule.t option; 16 + children_state : child_state list; 17 + world_readable : bool; 18 + guest_can_join : bool; 19 + } 20 + 21 + and child_state = { 22 + state_key : string; 23 + via : string list; 24 + order : string option; 25 + suggested : bool; 26 + } 27 + 28 + let child_state_jsont = 29 + Jsont.Object.( 30 + map (fun state_key via order suggested -> 31 + { state_key; via; order; suggested }) 32 + |> mem "state_key" Jsont.string ~enc:(fun t -> t.state_key) 33 + |> mem "via" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.via) 34 + |> opt_mem "order" Jsont.string ~enc:(fun t -> t.order) 35 + |> mem "suggested" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.suggested) 36 + |> finish) 37 + 38 + let space_room_jsont = 39 + Jsont.Object.( 40 + map (fun room_id name topic canonical_alias avatar_url num_joined_members 41 + room_type join_rule children_state world_readable guest_can_join -> 42 + { room_id; name; topic; canonical_alias; avatar_url; num_joined_members; 43 + room_type; join_rule; children_state; world_readable; guest_can_join }) 44 + |> mem "room_id" Matrix_proto.Id.Room_id.jsont ~enc:(fun t -> t.room_id) 45 + |> opt_mem "name" Jsont.string ~enc:(fun t -> t.name) 46 + |> opt_mem "topic" Jsont.string ~enc:(fun t -> t.topic) 47 + |> opt_mem "canonical_alias" Matrix_proto.Id.Room_alias.jsont ~enc:(fun t -> t.canonical_alias) 48 + |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url) 49 + |> mem "num_joined_members" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.num_joined_members) 50 + |> opt_mem "room_type" Jsont.string ~enc:(fun t -> t.room_type) 51 + |> opt_mem "join_rule" Matrix_proto.Event.Join_rule.jsont ~enc:(fun t -> t.join_rule) 52 + |> mem "children_state" (Jsont.list child_state_jsont) ~dec_absent:[] 53 + ~enc:(fun t -> t.children_state) 54 + |> mem "world_readable" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.world_readable) 55 + |> mem "guest_can_join" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.guest_can_join) 56 + |> finish) 57 + 58 + (** Response from GET /_matrix/client/v1/rooms/{roomId}/hierarchy *) 59 + type hierarchy_response = { 60 + rooms : space_room list; 61 + next_batch : string option; 62 + } 63 + 64 + let hierarchy_response_jsont = 65 + Jsont.Object.( 66 + map (fun rooms next_batch -> { rooms; next_batch }) 67 + |> mem "rooms" (Jsont.list space_room_jsont) ~dec_absent:[] ~enc:(fun t -> t.rooms) 68 + |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch) 69 + |> finish) 70 + 71 + (** Get the hierarchy of a space. 72 + 73 + @param room_id The space room ID 74 + @param suggested_only If true, only return suggested rooms 75 + @param limit Maximum number of rooms to return per request 76 + @param max_depth Maximum depth to recurse into the hierarchy 77 + @param from Pagination token *) 78 + let get_hierarchy client ~room_id ?suggested_only ?limit ?max_depth ?from () = 79 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 80 + let path = Printf.sprintf "/rooms/%s/hierarchy" (Uri.pct_encode room_id_str) in 81 + let query = 82 + [] 83 + |> (fun q -> match suggested_only with 84 + | Some true -> ("suggested_only", "true") :: q 85 + | _ -> q) 86 + |> (fun q -> match limit with 87 + | Some l -> ("limit", string_of_int l) :: q 88 + | None -> q) 89 + |> (fun q -> match max_depth with 90 + | Some d -> ("max_depth", string_of_int d) :: q 91 + | None -> q) 92 + |> (fun q -> match from with 93 + | Some f -> ("from", f) :: q 94 + | None -> q) 95 + in 96 + let query = if query = [] then None else Some query in 97 + match Client.get client ~path ?query () with 98 + | Error e -> Error e 99 + | Ok body -> Client.decode_response hierarchy_response_jsont body 100 + 101 + (** Add a child room to a space. 102 + 103 + @param space_id The parent space room ID 104 + @param child_id The child room ID 105 + @param via Server names to route through 106 + @param order Optional ordering string 107 + @param suggested Whether the room is suggested *) 108 + (* Response type for state setting *) 109 + type set_state_response = { 110 + event_id : Matrix_proto.Id.Event_id.t; 111 + } 112 + 113 + let set_state_response_jsont = 114 + Jsont.Object.( 115 + map (fun event_id -> { event_id }) 116 + |> mem "event_id" Matrix_proto.Id.Event_id.jsont 117 + |> finish) 118 + 119 + let add_child client ~space_id ~child_id ?(via = []) ?order ?(suggested = false) () = 120 + let space_id_str = Matrix_proto.Id.Room_id.to_string space_id in 121 + let child_id_str = Matrix_proto.Id.Room_id.to_string child_id in 122 + let path = Printf.sprintf "/rooms/%s/state/m.space.child/%s" 123 + (Uri.pct_encode space_id_str) 124 + (Uri.pct_encode child_id_str) 125 + in 126 + let content : Matrix_proto.Event.Space_child_content.t = { 127 + via = if via = [] then None else Some via; 128 + order; 129 + suggested = if suggested then Some true else None; 130 + } in 131 + match Client.encode_body Matrix_proto.Event.Space_child_content.jsont content with 132 + | Error e -> Error e 133 + | Ok body -> 134 + match Client.put client ~path ~body () with 135 + | Error e -> Error e 136 + | Ok resp_body -> 137 + match Client.decode_response set_state_response_jsont resp_body with 138 + | Error e -> Error e 139 + | Ok resp -> Ok resp.event_id 140 + 141 + (** Remove a child room from a space. *) 142 + let remove_child client ~space_id ~child_id = 143 + let space_id_str = Matrix_proto.Id.Room_id.to_string space_id in 144 + let child_id_str = Matrix_proto.Id.Room_id.to_string child_id in 145 + let path = Printf.sprintf "/rooms/%s/state/m.space.child/%s" 146 + (Uri.pct_encode space_id_str) 147 + (Uri.pct_encode child_id_str) 148 + in 149 + (* Send empty content to remove the child *) 150 + match Client.put client ~path ~body:"{}" () with 151 + | Error e -> Error e 152 + | Ok _ -> Ok () 153 + 154 + (** Set the parent space for a room. 155 + 156 + @param room_id The child room ID 157 + @param parent_id The parent space ID 158 + @param via Server names to route through 159 + @param canonical Whether this is the canonical parent *) 160 + let set_parent client ~room_id ~parent_id ?(via = []) ?(canonical = false) () = 161 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 162 + let parent_id_str = Matrix_proto.Id.Room_id.to_string parent_id in 163 + let path = Printf.sprintf "/rooms/%s/state/m.space.parent/%s" 164 + (Uri.pct_encode room_id_str) 165 + (Uri.pct_encode parent_id_str) 166 + in 167 + let content : Matrix_proto.Event.Space_parent_content.t = { 168 + via = if via = [] then None else Some via; 169 + canonical = if canonical then Some true else None; 170 + } in 171 + match Client.encode_body Matrix_proto.Event.Space_parent_content.jsont content with 172 + | Error e -> Error e 173 + | Ok body -> 174 + match Client.put client ~path ~body () with 175 + | Error e -> Error e 176 + | Ok resp_body -> 177 + match Client.decode_response set_state_response_jsont resp_body with 178 + | Error e -> Error e 179 + | Ok resp -> Ok resp.event_id 180 + 181 + (** Remove a parent space from a room. *) 182 + let remove_parent client ~room_id ~parent_id = 183 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 184 + let parent_id_str = Matrix_proto.Id.Room_id.to_string parent_id in 185 + let path = Printf.sprintf "/rooms/%s/state/m.space.parent/%s" 186 + (Uri.pct_encode room_id_str) 187 + (Uri.pct_encode parent_id_str) 188 + in 189 + match Client.put client ~path ~body:"{}" () with 190 + | Error e -> Error e 191 + | Ok _ -> Ok () 192 + 193 + (** Check if a room is a space. *) 194 + let is_space room_type = 195 + match room_type with 196 + | Some "m.space" -> true 197 + | _ -> false 198 + 199 + (** Create a new space. 200 + 201 + @param name The space name 202 + @param topic Optional topic 203 + @param visibility Room visibility (public or private) 204 + @param invite List of users to invite *) 205 + let create_space client ~name ?topic ?(visibility = `Private) ?(invite = []) () = 206 + Rooms.create_room client 207 + ~name 208 + ?topic 209 + ~visibility 210 + ~invite 211 + ~room_type:"m.space" 212 + ()
+139
lib/matrix_client/state.ml
··· 1 + (** Room state operations. *) 2 + 3 + (* Get all state *) 4 + let get_state client ~room_id = 5 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 6 + let path = Printf.sprintf "/rooms/%s/state" (Uri.pct_encode room_id_str) in 7 + match Client.get client ~path () with 8 + | Error e -> Error e 9 + | Ok body -> Client.decode_response (Jsont.list Matrix_proto.Event.Raw_event.jsont) body 10 + 11 + (* Get specific state event *) 12 + let get_state_event client ~room_id ~event_type ?(state_key = "") () = 13 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 14 + let path = Printf.sprintf "/rooms/%s/state/%s/%s" 15 + (Uri.pct_encode room_id_str) 16 + (Uri.pct_encode event_type) 17 + (Uri.pct_encode state_key) 18 + in 19 + match Client.get client ~path () with 20 + | Error e -> Error e 21 + | Ok body -> Client.decode_response Jsont.json body 22 + 23 + (* Set state event *) 24 + type set_state_response = { 25 + event_id : Matrix_proto.Id.Event_id.t; 26 + } 27 + 28 + let set_state_response_jsont = 29 + Jsont.Object.( 30 + map (fun event_id -> { event_id }) 31 + |> mem "event_id" Matrix_proto.Id.Event_id.jsont 32 + |> finish) 33 + 34 + let set_state client ~room_id ~event_type ?(state_key = "") ~content () = 35 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 36 + let path = Printf.sprintf "/rooms/%s/state/%s/%s" 37 + (Uri.pct_encode room_id_str) 38 + (Uri.pct_encode event_type) 39 + (Uri.pct_encode state_key) 40 + in 41 + match Client.encode_body Jsont.json content with 42 + | Error e -> Error e 43 + | Ok body -> 44 + match Client.put client ~path ~body () with 45 + | Error e -> Error e 46 + | Ok body -> 47 + match Client.decode_response set_state_response_jsont body with 48 + | Error e -> Error e 49 + | Ok resp -> Ok resp.event_id 50 + 51 + (* Convenience: room name *) 52 + type name_content = { 53 + name : string; 54 + } [@@warning "-69"] 55 + 56 + let name_content_jsont = 57 + Jsont.Object.( 58 + map (fun name -> { name }) 59 + |> mem "name" Jsont.string 60 + |> finish) 61 + 62 + let get_name client ~room_id = 63 + match get_state_event client ~room_id ~event_type:"m.room.name" () with 64 + | Error (Error.Matrix_error { errcode = Error.M_NOT_FOUND; _ }) -> Ok None 65 + | Error e -> Error e 66 + | Ok json -> 67 + match Jsont_bytesrw.decode_string name_content_jsont 68 + (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with 69 + | Ok c -> Ok (Some c.name) 70 + | Error _ -> Ok None 71 + 72 + let set_name client ~room_id ~name = 73 + let content = { name } in 74 + match Client.encode_body name_content_jsont content with 75 + | Error e -> Error e 76 + | Ok body -> 77 + match Client.decode_response Jsont.json body with 78 + | Error e -> Error e 79 + | Ok json -> set_state client ~room_id ~event_type:"m.room.name" ~content:json () 80 + 81 + (* Convenience: room topic *) 82 + type topic_content = { 83 + topic : string; 84 + } [@@warning "-69"] 85 + 86 + let topic_content_jsont = 87 + Jsont.Object.( 88 + map (fun topic -> { topic }) 89 + |> mem "topic" Jsont.string 90 + |> finish) 91 + 92 + let get_topic client ~room_id = 93 + match get_state_event client ~room_id ~event_type:"m.room.topic" () with 94 + | Error (Error.Matrix_error { errcode = Error.M_NOT_FOUND; _ }) -> Ok None 95 + | Error e -> Error e 96 + | Ok json -> 97 + match Jsont_bytesrw.decode_string topic_content_jsont 98 + (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with 99 + | Ok c -> Ok (Some c.topic) 100 + | Error _ -> Ok None 101 + 102 + let set_topic client ~room_id ~topic = 103 + let content = { topic } in 104 + match Client.encode_body topic_content_jsont content with 105 + | Error e -> Error e 106 + | Ok body -> 107 + match Client.decode_response Jsont.json body with 108 + | Error e -> Error e 109 + | Ok json -> set_state client ~room_id ~event_type:"m.room.topic" ~content:json () 110 + 111 + (* Convenience: room avatar *) 112 + type avatar_content = { 113 + url : string; 114 + } [@@warning "-69"] 115 + 116 + let avatar_content_jsont = 117 + Jsont.Object.( 118 + map (fun url -> { url }) 119 + |> mem "url" Jsont.string 120 + |> finish) 121 + 122 + let get_avatar client ~room_id = 123 + match get_state_event client ~room_id ~event_type:"m.room.avatar" () with 124 + | Error (Error.Matrix_error { errcode = Error.M_NOT_FOUND; _ }) -> Ok None 125 + | Error e -> Error e 126 + | Ok json -> 127 + match Jsont_bytesrw.decode_string avatar_content_jsont 128 + (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with 129 + | Ok c -> Ok (Some c.url) 130 + | Error _ -> Ok None 131 + 132 + let set_avatar client ~room_id ~url = 133 + let content = { url } in 134 + match Client.encode_body avatar_content_jsont content with 135 + | Error e -> Error e 136 + | Ok body -> 137 + match Client.decode_response Jsont.json body with 138 + | Error e -> Error e 139 + | Ok json -> set_state client ~room_id ~event_type:"m.room.avatar" ~content:json ()
+71
lib/matrix_client/state.mli
··· 1 + (** Room state operations. *) 2 + 3 + (** Get all current state events for a room. *) 4 + val get_state : 5 + Client.t -> 6 + room_id:Matrix_proto.Id.Room_id.t -> 7 + (Matrix_proto.Event.Raw_event.t list, Error.t) result 8 + 9 + (** Get a specific state event. 10 + 11 + @param state_key The state key (empty string for events without a key). *) 12 + val get_state_event : 13 + Client.t -> 14 + room_id:Matrix_proto.Id.Room_id.t -> 15 + event_type:string -> 16 + ?state_key:string -> 17 + unit -> 18 + (Jsont.json, Error.t) result 19 + 20 + (** Set a state event. 21 + 22 + @param state_key The state key (empty string for events without a key). *) 23 + val set_state : 24 + Client.t -> 25 + room_id:Matrix_proto.Id.Room_id.t -> 26 + event_type:string -> 27 + ?state_key:string -> 28 + content:Jsont.json -> 29 + unit -> 30 + (Matrix_proto.Id.Event_id.t, Error.t) result 31 + 32 + (** {1 Convenience Functions} *) 33 + 34 + (** Get room name. *) 35 + val get_name : 36 + Client.t -> 37 + room_id:Matrix_proto.Id.Room_id.t -> 38 + (string option, Error.t) result 39 + 40 + (** Set room name. *) 41 + val set_name : 42 + Client.t -> 43 + room_id:Matrix_proto.Id.Room_id.t -> 44 + name:string -> 45 + (Matrix_proto.Id.Event_id.t, Error.t) result 46 + 47 + (** Get room topic. *) 48 + val get_topic : 49 + Client.t -> 50 + room_id:Matrix_proto.Id.Room_id.t -> 51 + (string option, Error.t) result 52 + 53 + (** Set room topic. *) 54 + val set_topic : 55 + Client.t -> 56 + room_id:Matrix_proto.Id.Room_id.t -> 57 + topic:string -> 58 + (Matrix_proto.Id.Event_id.t, Error.t) result 59 + 60 + (** Get room avatar URL. *) 61 + val get_avatar : 62 + Client.t -> 63 + room_id:Matrix_proto.Id.Room_id.t -> 64 + (string option, Error.t) result 65 + 66 + (** Set room avatar URL. *) 67 + val set_avatar : 68 + Client.t -> 69 + room_id:Matrix_proto.Id.Room_id.t -> 70 + url:string -> 71 + (Matrix_proto.Id.Event_id.t, Error.t) result
+1001
lib/matrix_client/store.ml
··· 1 + (** Persistent storage layer for Matrix SDK state. 2 + 3 + This module provides pluggable storage interfaces matching the Rust SDK's 4 + storage architecture. Implementations can be provided for: 5 + - In-memory (default, non-persistent) 6 + - SQLite (persistent file-based) 7 + - Custom backends 8 + 9 + Storage is split into separate stores: 10 + - STATE_STORE: Room state, user profiles, sync tokens 11 + - CRYPTO_STORE: E2EE keys, sessions, device info 12 + - EVENT_CACHE_STORE: Event history and pagination *) 13 + 14 + (** {1 Key-Value Data Types} *) 15 + 16 + (** Types of data that can be stored in the state store's KV section *) 17 + type kv_data = 18 + | Sync_token of string 19 + | Filter of { filter_id : string; filter : Jsont.json } 20 + | User_avatar_url of { user_id : Matrix_proto.Id.User_id.t; url : string option } 21 + | Recently_visited_rooms of Matrix_proto.Id.Room_id.t list 22 + | Composer_draft of { 23 + room_id : Matrix_proto.Id.Room_id.t; 24 + plain_text : string; 25 + html_text : string option; 26 + draft_type : [ `New | `Edit of Matrix_proto.Id.Event_id.t | `Reply of Matrix_proto.Id.Event_id.t ]; 27 + } 28 + 29 + (** {1 State Changes Aggregate} *) 30 + 31 + (** Aggregated state changes from a sync response *) 32 + type state_changes = { 33 + sync_token : string option; 34 + account_data : (string * Jsont.json) list; (* event_type -> content *) 35 + presence : (string * Jsont.json) list; (* user_id -> presence event *) 36 + room_infos : (string * room_info) list; (* room_id -> room info *) 37 + room_state : (string * (string * (string * Jsont.json) list) list) list; 38 + (* room_id -> event_type -> state_key -> event *) 39 + room_account_data : (string * (string * Jsont.json) list) list; 40 + (* room_id -> event_type -> content *) 41 + stripped_state : (string * (string * (string * Jsont.json) list) list) list; 42 + (* For invited rooms *) 43 + receipts : (string * Jsont.json) list; (* room_id -> receipt content *) 44 + profiles : (string * (string * member_profile) list) list; 45 + (* room_id -> user_id -> profile *) 46 + } 47 + 48 + and room_info = { 49 + room_id : Matrix_proto.Id.Room_id.t; 50 + room_type : string option; 51 + name : string option; 52 + topic : string option; 53 + avatar_url : string option; 54 + canonical_alias : Matrix_proto.Id.Room_alias.t option; 55 + joined_member_count : int; 56 + invited_member_count : int; 57 + is_encrypted : bool; 58 + is_direct : bool; 59 + prev_batch : string option; 60 + notification_count : int; 61 + highlight_count : int; 62 + } 63 + 64 + and member_profile = { 65 + display_name : string option; 66 + avatar_url : string option; 67 + } 68 + 69 + let empty_state_changes = { 70 + sync_token = None; 71 + account_data = []; 72 + presence = []; 73 + room_infos = []; 74 + room_state = []; 75 + room_account_data = []; 76 + stripped_state = []; 77 + receipts = []; 78 + profiles = []; 79 + } 80 + 81 + (** {1 STATE_STORE Module Type} *) 82 + 83 + (** Module type for room state storage *) 84 + module type STATE_STORE = sig 85 + type t 86 + type error 87 + 88 + (** Create a new store *) 89 + val create : unit -> t 90 + 91 + (** {2 State Changes} *) 92 + 93 + (** Save aggregated state changes from sync *) 94 + val save_changes : t -> state_changes -> (unit, error) result 95 + 96 + (** {2 Sync Token} *) 97 + 98 + val get_sync_token : t -> string option 99 + val set_sync_token : t -> string -> unit 100 + 101 + (** {2 Room State} *) 102 + 103 + (** Get a specific state event *) 104 + val get_state_event : 105 + t -> 106 + room_id:Matrix_proto.Id.Room_id.t -> 107 + event_type:string -> 108 + state_key:string -> 109 + Jsont.json option 110 + 111 + (** Get all state events of a type for a room *) 112 + val get_state_events : 113 + t -> 114 + room_id:Matrix_proto.Id.Room_id.t -> 115 + event_type:string -> 116 + (string * Jsont.json) list (* state_key -> event *) 117 + 118 + (** Get room info *) 119 + val get_room_info : t -> Matrix_proto.Id.Room_id.t -> room_info option 120 + 121 + (** Get all room IDs *) 122 + val get_room_ids : t -> Matrix_proto.Id.Room_id.t list 123 + 124 + (** {2 User Profiles} *) 125 + 126 + (** Get user profile in a room *) 127 + val get_profile : 128 + t -> 129 + room_id:Matrix_proto.Id.Room_id.t -> 130 + user_id:Matrix_proto.Id.User_id.t -> 131 + member_profile option 132 + 133 + (** {2 Account Data} *) 134 + 135 + val get_account_data : t -> event_type:string -> Jsont.json option 136 + val get_room_account_data : 137 + t -> room_id:Matrix_proto.Id.Room_id.t -> event_type:string -> Jsont.json option 138 + 139 + (** {2 Receipts} *) 140 + 141 + val get_receipts : t -> room_id:Matrix_proto.Id.Room_id.t -> Jsont.json option 142 + 143 + (** {2 Presence} *) 144 + 145 + val get_presence : t -> user_id:Matrix_proto.Id.User_id.t -> Jsont.json option 146 + 147 + (** {2 Key-Value Store} *) 148 + 149 + val get_kv : t -> string -> kv_data option 150 + val set_kv : t -> string -> kv_data -> unit 151 + val remove_kv : t -> string -> unit 152 + 153 + (** {2 Cleanup} *) 154 + 155 + val clear : t -> unit 156 + end 157 + 158 + (** {1 Crypto Changes Aggregate} *) 159 + 160 + (** Device key information *) 161 + type device_keys = { 162 + user_id : Matrix_proto.Id.User_id.t; 163 + device_id : Matrix_proto.Id.Device_id.t; 164 + algorithms : string list; 165 + keys : (string * string) list; (* key_id -> key *) 166 + signatures : (string * (string * string) list) list; (* user_id -> key_id -> sig *) 167 + unsigned : Jsont.json option; 168 + } 169 + 170 + (** Cross-signing identity keys *) 171 + type cross_signing_keys = { 172 + master_key : (string * string) list option; 173 + self_signing_key : (string * string) list option; 174 + user_signing_key : (string * string) list option; 175 + } 176 + 177 + (** Olm session data *) 178 + type olm_session = { 179 + session_id : string; 180 + sender_key : string; 181 + pickle : string; (* Serialized session state *) 182 + created_at : int64; 183 + last_used_at : int64; 184 + } 185 + 186 + (** Megolm inbound group session *) 187 + type inbound_group_session = { 188 + session_id : string; 189 + room_id : Matrix_proto.Id.Room_id.t; 190 + sender_key : string; 191 + signing_key : string option; 192 + pickle : string; 193 + imported : bool; 194 + backed_up : bool; 195 + history_visibility : string option; 196 + algorithm : string; 197 + } 198 + 199 + (** Megolm outbound group session *) 200 + type outbound_group_session = { 201 + session_id : string; 202 + room_id : Matrix_proto.Id.Room_id.t; 203 + pickle : string; 204 + creation_time : int64; 205 + message_count : int; 206 + shared_with : (string * (string * int) list) list; (* user_id -> device_id -> index *) 207 + } 208 + 209 + (** Tracked user for key updates *) 210 + type tracked_user = { 211 + user_id : Matrix_proto.Id.User_id.t; 212 + dirty : bool; 213 + } 214 + 215 + (** Aggregated crypto changes *) 216 + type crypto_changes = { 217 + account_pickle : string option; 218 + private_identity : cross_signing_keys option; 219 + olm_sessions : olm_session list; 220 + inbound_group_sessions : inbound_group_session list; 221 + outbound_group_sessions : outbound_group_session list; 222 + devices_new : device_keys list; 223 + devices_changed : device_keys list; 224 + devices_deleted : (Matrix_proto.Id.User_id.t * Matrix_proto.Id.Device_id.t) list; 225 + tracked_users : tracked_user list; 226 + message_hashes : (string * string) list; (* hash -> event_id *) 227 + backup_decryption_key : string option; 228 + } 229 + 230 + let empty_crypto_changes = { 231 + account_pickle = None; 232 + private_identity = None; 233 + olm_sessions = []; 234 + inbound_group_sessions = []; 235 + outbound_group_sessions = []; 236 + devices_new = []; 237 + devices_changed = []; 238 + devices_deleted = []; 239 + tracked_users = []; 240 + message_hashes = []; 241 + backup_decryption_key = None; 242 + } 243 + 244 + (** {1 CRYPTO_STORE Module Type} *) 245 + 246 + (** Module type for E2EE key storage *) 247 + module type CRYPTO_STORE = sig 248 + type t 249 + type error 250 + 251 + (** Create a new store *) 252 + val create : unit -> t 253 + 254 + (** {2 Account} *) 255 + 256 + (** Load the pickled Olm account *) 257 + val load_account : t -> string option 258 + 259 + (** Save the pickled Olm account *) 260 + val save_account : t -> string -> unit 261 + 262 + (** {2 Batch Changes} *) 263 + 264 + val save_changes : t -> crypto_changes -> (unit, error) result 265 + 266 + (** {2 Olm Sessions} *) 267 + 268 + (** Get Olm sessions for a sender key *) 269 + val get_sessions : t -> sender_key:string -> olm_session list 270 + 271 + (** Get a specific Olm session *) 272 + val get_session : t -> session_id:string -> olm_session option 273 + 274 + (** {2 Megolm Sessions} *) 275 + 276 + (** Get an inbound group session *) 277 + val get_inbound_group_session : 278 + t -> 279 + room_id:Matrix_proto.Id.Room_id.t -> 280 + session_id:string -> 281 + inbound_group_session option 282 + 283 + (** Get all inbound group sessions for a room *) 284 + val get_inbound_group_sessions_for_room : 285 + t -> room_id:Matrix_proto.Id.Room_id.t -> inbound_group_session list 286 + 287 + (** Get sessions needing backup *) 288 + val get_sessions_for_backup : t -> limit:int -> inbound_group_session list 289 + 290 + (** Mark sessions as backed up *) 291 + val mark_sessions_backed_up : t -> session_ids:string list -> unit 292 + 293 + (** Get outbound group session for a room *) 294 + val get_outbound_group_session : 295 + t -> room_id:Matrix_proto.Id.Room_id.t -> outbound_group_session option 296 + 297 + (** {2 Device Keys} *) 298 + 299 + (** Get a specific device's keys *) 300 + val get_device : 301 + t -> 302 + user_id:Matrix_proto.Id.User_id.t -> 303 + device_id:Matrix_proto.Id.Device_id.t -> 304 + device_keys option 305 + 306 + (** Get all devices for a user *) 307 + val get_user_devices : t -> user_id:Matrix_proto.Id.User_id.t -> device_keys list 308 + 309 + (** {2 Cross-Signing} *) 310 + 311 + (** Get user's cross-signing keys *) 312 + val get_user_identity : 313 + t -> user_id:Matrix_proto.Id.User_id.t -> cross_signing_keys option 314 + 315 + (** Get own cross-signing identity (private keys) *) 316 + val get_private_identity : t -> cross_signing_keys option 317 + 318 + (** {2 User Tracking} *) 319 + 320 + (** Get all tracked users *) 321 + val get_tracked_users : t -> tracked_user list 322 + 323 + (** Mark a user as needing key update *) 324 + val mark_user_dirty : t -> user_id:Matrix_proto.Id.User_id.t -> unit 325 + 326 + (** {2 Replay Protection} *) 327 + 328 + (** Check if a message hash is known (replay protection) *) 329 + val is_message_known : t -> hash:string -> bool 330 + 331 + (** {2 Backup} *) 332 + 333 + val get_backup_key : t -> string option 334 + val set_backup_key : t -> string -> unit 335 + 336 + (** {2 Custom Storage} *) 337 + 338 + val get_custom_value : t -> string -> string option 339 + val set_custom_value : t -> string -> string -> unit 340 + val remove_custom_value : t -> string -> unit 341 + 342 + (** {2 Cleanup} *) 343 + 344 + val clear : t -> unit 345 + end 346 + 347 + (** {1 EVENT_CACHE_STORE Module Type} *) 348 + 349 + (** Cached event entry *) 350 + type cached_event = { 351 + event_id : Matrix_proto.Id.Event_id.t; 352 + room_id : Matrix_proto.Id.Room_id.t; 353 + sender : Matrix_proto.Id.User_id.t; 354 + origin_server_ts : int64; 355 + event_type : string; 356 + content : Jsont.json; 357 + unsigned : Jsont.json option; 358 + } 359 + 360 + (** Gap in event history (for pagination) *) 361 + type event_gap = { 362 + prev_token : string; 363 + } 364 + 365 + (** Chunk of events in linked list *) 366 + type event_chunk = { 367 + chunk_id : int; 368 + events : cached_event list; 369 + gap : event_gap option; (* Gap before this chunk if any *) 370 + prev_chunk_id : int option; 371 + next_chunk_id : int option; 372 + } 373 + 374 + (** Module type for event history storage *) 375 + module type EVENT_CACHE_STORE = sig 376 + type t 377 + type error 378 + 379 + (** Create a new store *) 380 + val create : unit -> t 381 + 382 + (** {2 Event Storage} *) 383 + 384 + (** Save an event to cache *) 385 + val save_event : t -> cached_event -> unit 386 + 387 + (** Get an event by ID *) 388 + val get_event : 389 + t -> room_id:Matrix_proto.Id.Room_id.t -> event_id:Matrix_proto.Id.Event_id.t -> 390 + cached_event option 391 + 392 + (** Get events for a room *) 393 + val get_room_events : 394 + t -> 395 + room_id:Matrix_proto.Id.Room_id.t -> 396 + ?limit:int -> 397 + ?event_type:string -> 398 + unit -> 399 + cached_event list 400 + 401 + (** {2 Linked Chunk Management} *) 402 + 403 + (** Get the most recent chunk for a room *) 404 + val get_last_chunk : t -> room_id:Matrix_proto.Id.Room_id.t -> event_chunk option 405 + 406 + (** Load a specific chunk *) 407 + val get_chunk : t -> room_id:Matrix_proto.Id.Room_id.t -> chunk_id:int -> event_chunk option 408 + 409 + (** Save a chunk *) 410 + val save_chunk : t -> room_id:Matrix_proto.Id.Room_id.t -> event_chunk -> unit 411 + 412 + (** {2 Deduplication} *) 413 + 414 + (** Filter out events that are already cached *) 415 + val filter_duplicates : 416 + t -> 417 + room_id:Matrix_proto.Id.Room_id.t -> 418 + event_ids:Matrix_proto.Id.Event_id.t list -> 419 + Matrix_proto.Id.Event_id.t list 420 + 421 + (** {2 Relations} *) 422 + 423 + (** Find events related to a given event (replies, edits, reactions) *) 424 + val find_event_relations : 425 + t -> 426 + room_id:Matrix_proto.Id.Room_id.t -> 427 + event_id:Matrix_proto.Id.Event_id.t -> 428 + ?rel_type:string -> 429 + unit -> 430 + cached_event list 431 + 432 + (** {2 Cleanup} *) 433 + 434 + (** Remove all events for a room *) 435 + val remove_room : t -> room_id:Matrix_proto.Id.Room_id.t -> unit 436 + 437 + (** Clear all cached events *) 438 + val clear : t -> unit 439 + end 440 + 441 + (** {1 In-Memory Implementations} *) 442 + 443 + (** In-memory state store implementation *) 444 + module Memory_state_store : STATE_STORE = struct 445 + type error = [ `Storage_error of string ] 446 + 447 + type t = { 448 + mutable sync_token : string option; 449 + account_data : (string, Jsont.json) Hashtbl.t; 450 + presence : (string, Jsont.json) Hashtbl.t; 451 + room_infos : (string, room_info) Hashtbl.t; 452 + room_state : (string, (string, (string, Jsont.json) Hashtbl.t) Hashtbl.t) Hashtbl.t; 453 + room_account_data : (string, (string, Jsont.json) Hashtbl.t) Hashtbl.t; 454 + receipts : (string, Jsont.json) Hashtbl.t; 455 + profiles : (string, (string, member_profile) Hashtbl.t) Hashtbl.t; 456 + kv_store : (string, kv_data) Hashtbl.t; 457 + } 458 + 459 + let create () = { 460 + sync_token = None; 461 + account_data = Hashtbl.create 16; 462 + presence = Hashtbl.create 64; 463 + room_infos = Hashtbl.create 64; 464 + room_state = Hashtbl.create 64; 465 + room_account_data = Hashtbl.create 64; 466 + receipts = Hashtbl.create 64; 467 + profiles = Hashtbl.create 64; 468 + kv_store = Hashtbl.create 16; 469 + } 470 + 471 + let save_changes t (changes : state_changes) = 472 + (* Sync token *) 473 + (match changes.sync_token with 474 + | Some token -> t.sync_token <- Some token 475 + | None -> ()); 476 + 477 + (* Account data *) 478 + List.iter (fun (event_type, content) -> 479 + Hashtbl.replace t.account_data event_type content 480 + ) changes.account_data; 481 + 482 + (* Presence *) 483 + List.iter (fun (user_id, event) -> 484 + Hashtbl.replace t.presence user_id event 485 + ) changes.presence; 486 + 487 + (* Room infos *) 488 + List.iter (fun (room_id, info) -> 489 + Hashtbl.replace t.room_infos room_id info 490 + ) changes.room_infos; 491 + 492 + (* Room state *) 493 + List.iter (fun (room_id, event_types) -> 494 + let room_tbl = 495 + match Hashtbl.find_opt t.room_state room_id with 496 + | Some tbl -> tbl 497 + | None -> 498 + let tbl = Hashtbl.create 16 in 499 + Hashtbl.add t.room_state room_id tbl; 500 + tbl 501 + in 502 + List.iter (fun (event_type, state_keys) -> 503 + let type_tbl = 504 + match Hashtbl.find_opt room_tbl event_type with 505 + | Some tbl -> tbl 506 + | None -> 507 + let tbl = Hashtbl.create 8 in 508 + Hashtbl.add room_tbl event_type tbl; 509 + tbl 510 + in 511 + List.iter (fun (state_key, event) -> 512 + Hashtbl.replace type_tbl state_key event 513 + ) state_keys 514 + ) event_types 515 + ) changes.room_state; 516 + 517 + (* Room account data *) 518 + List.iter (fun (room_id, events) -> 519 + let tbl = 520 + match Hashtbl.find_opt t.room_account_data room_id with 521 + | Some tbl -> tbl 522 + | None -> 523 + let tbl = Hashtbl.create 8 in 524 + Hashtbl.add t.room_account_data room_id tbl; 525 + tbl 526 + in 527 + List.iter (fun (event_type, content) -> 528 + Hashtbl.replace tbl event_type content 529 + ) events 530 + ) changes.room_account_data; 531 + 532 + (* Receipts *) 533 + List.iter (fun (room_id, content) -> 534 + Hashtbl.replace t.receipts room_id content 535 + ) changes.receipts; 536 + 537 + (* Profiles *) 538 + List.iter (fun (room_id, users) -> 539 + let tbl = 540 + match Hashtbl.find_opt t.profiles room_id with 541 + | Some tbl -> tbl 542 + | None -> 543 + let tbl = Hashtbl.create 64 in 544 + Hashtbl.add t.profiles room_id tbl; 545 + tbl 546 + in 547 + List.iter (fun (user_id, profile) -> 548 + Hashtbl.replace tbl user_id profile 549 + ) users 550 + ) changes.profiles; 551 + 552 + Ok () 553 + 554 + let get_sync_token t = t.sync_token 555 + let set_sync_token t token = t.sync_token <- Some token 556 + 557 + let get_state_event t ~room_id ~event_type ~state_key = 558 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 559 + match Hashtbl.find_opt t.room_state room_id_str with 560 + | None -> None 561 + | Some room_tbl -> 562 + match Hashtbl.find_opt room_tbl event_type with 563 + | None -> None 564 + | Some type_tbl -> Hashtbl.find_opt type_tbl state_key 565 + 566 + let get_state_events t ~room_id ~event_type = 567 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 568 + match Hashtbl.find_opt t.room_state room_id_str with 569 + | None -> [] 570 + | Some room_tbl -> 571 + match Hashtbl.find_opt room_tbl event_type with 572 + | None -> [] 573 + | Some type_tbl -> Hashtbl.fold (fun k v acc -> (k, v) :: acc) type_tbl [] 574 + 575 + let get_room_info t room_id = 576 + Hashtbl.find_opt t.room_infos (Matrix_proto.Id.Room_id.to_string room_id) 577 + 578 + let get_room_ids t = 579 + Hashtbl.fold (fun id_str _info acc -> 580 + match Matrix_proto.Id.Room_id.of_string id_str with 581 + | Ok id -> id :: acc 582 + | Error _ -> acc 583 + ) t.room_infos [] 584 + 585 + let get_profile t ~room_id ~user_id = 586 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 587 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 588 + match Hashtbl.find_opt t.profiles room_id_str with 589 + | None -> None 590 + | Some tbl -> Hashtbl.find_opt tbl user_id_str 591 + 592 + let get_account_data t ~event_type = 593 + Hashtbl.find_opt t.account_data event_type 594 + 595 + let get_room_account_data t ~room_id ~event_type = 596 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 597 + match Hashtbl.find_opt t.room_account_data room_id_str with 598 + | None -> None 599 + | Some tbl -> Hashtbl.find_opt tbl event_type 600 + 601 + let get_receipts t ~room_id = 602 + Hashtbl.find_opt t.receipts (Matrix_proto.Id.Room_id.to_string room_id) 603 + 604 + let get_presence t ~user_id = 605 + Hashtbl.find_opt t.presence (Matrix_proto.Id.User_id.to_string user_id) 606 + 607 + let get_kv t key = Hashtbl.find_opt t.kv_store key 608 + let set_kv t key value = Hashtbl.replace t.kv_store key value 609 + let remove_kv t key = Hashtbl.remove t.kv_store key 610 + 611 + let clear t = 612 + t.sync_token <- None; 613 + Hashtbl.clear t.account_data; 614 + Hashtbl.clear t.presence; 615 + Hashtbl.clear t.room_infos; 616 + Hashtbl.clear t.room_state; 617 + Hashtbl.clear t.room_account_data; 618 + Hashtbl.clear t.receipts; 619 + Hashtbl.clear t.profiles; 620 + Hashtbl.clear t.kv_store 621 + end 622 + 623 + (** In-memory crypto store implementation *) 624 + module Memory_crypto_store : CRYPTO_STORE = struct 625 + type error = [ `Storage_error of string ] 626 + 627 + type t = { 628 + mutable account_pickle : string option; 629 + mutable private_identity : cross_signing_keys option; 630 + olm_sessions : (string, olm_session list) Hashtbl.t; (* sender_key -> sessions *) 631 + olm_sessions_by_id : (string, olm_session) Hashtbl.t; (* session_id -> session *) 632 + inbound_sessions : (string, inbound_group_session) Hashtbl.t; (* room_id:session_id -> session *) 633 + outbound_sessions : (string, outbound_group_session) Hashtbl.t; (* room_id -> session *) 634 + devices : (string, device_keys) Hashtbl.t; (* user_id:device_id -> keys *) 635 + user_identities : (string, cross_signing_keys) Hashtbl.t; (* user_id -> keys *) 636 + tracked_users : (string, tracked_user) Hashtbl.t; 637 + message_hashes : (string, string) Hashtbl.t; 638 + mutable backup_key : string option; 639 + custom_values : (string, string) Hashtbl.t; 640 + } 641 + 642 + let create () = { 643 + account_pickle = None; 644 + private_identity = None; 645 + olm_sessions = Hashtbl.create 64; 646 + olm_sessions_by_id = Hashtbl.create 256; 647 + inbound_sessions = Hashtbl.create 256; 648 + outbound_sessions = Hashtbl.create 64; 649 + devices = Hashtbl.create 256; 650 + user_identities = Hashtbl.create 64; 651 + tracked_users = Hashtbl.create 64; 652 + message_hashes = Hashtbl.create 1024; 653 + backup_key = None; 654 + custom_values = Hashtbl.create 16; 655 + } 656 + 657 + let load_account t = t.account_pickle 658 + let save_account t pickle = t.account_pickle <- Some pickle 659 + 660 + let save_changes t (changes : crypto_changes) = 661 + (* Account *) 662 + (match changes.account_pickle with 663 + | Some pickle -> t.account_pickle <- Some pickle 664 + | None -> ()); 665 + 666 + (* Private identity *) 667 + (match changes.private_identity with 668 + | Some identity -> t.private_identity <- Some identity 669 + | None -> ()); 670 + 671 + (* Olm sessions *) 672 + List.iter (fun (session : olm_session) -> 673 + Hashtbl.replace t.olm_sessions_by_id session.session_id session; 674 + let sessions : olm_session list = 675 + match Hashtbl.find_opt t.olm_sessions session.sender_key with 676 + | Some lst -> session :: List.filter (fun (s : olm_session) -> s.session_id <> session.session_id) lst 677 + | None -> [session] 678 + in 679 + Hashtbl.replace t.olm_sessions session.sender_key sessions 680 + ) changes.olm_sessions; 681 + 682 + (* Inbound group sessions *) 683 + List.iter (fun (session : inbound_group_session) -> 684 + let key = Matrix_proto.Id.Room_id.to_string session.room_id ^ ":" ^ session.session_id in 685 + Hashtbl.replace t.inbound_sessions key session 686 + ) changes.inbound_group_sessions; 687 + 688 + (* Outbound group sessions *) 689 + List.iter (fun (session : outbound_group_session) -> 690 + let key = Matrix_proto.Id.Room_id.to_string session.room_id in 691 + Hashtbl.replace t.outbound_sessions key session 692 + ) changes.outbound_group_sessions; 693 + 694 + (* New devices *) 695 + List.iter (fun (device : device_keys) -> 696 + let key = 697 + Matrix_proto.Id.User_id.to_string device.user_id ^ ":" ^ 698 + Matrix_proto.Id.Device_id.to_string device.device_id 699 + in 700 + Hashtbl.replace t.devices key device 701 + ) changes.devices_new; 702 + 703 + (* Changed devices *) 704 + List.iter (fun (device : device_keys) -> 705 + let key = 706 + Matrix_proto.Id.User_id.to_string device.user_id ^ ":" ^ 707 + Matrix_proto.Id.Device_id.to_string device.device_id 708 + in 709 + Hashtbl.replace t.devices key device 710 + ) changes.devices_changed; 711 + 712 + (* Deleted devices *) 713 + List.iter (fun (user_id, device_id) -> 714 + let key = 715 + Matrix_proto.Id.User_id.to_string user_id ^ ":" ^ 716 + Matrix_proto.Id.Device_id.to_string device_id 717 + in 718 + Hashtbl.remove t.devices key 719 + ) changes.devices_deleted; 720 + 721 + (* Tracked users *) 722 + List.iter (fun (user : tracked_user) -> 723 + Hashtbl.replace t.tracked_users 724 + (Matrix_proto.Id.User_id.to_string user.user_id) user 725 + ) changes.tracked_users; 726 + 727 + (* Message hashes *) 728 + List.iter (fun (hash, event_id) -> 729 + Hashtbl.replace t.message_hashes hash event_id 730 + ) changes.message_hashes; 731 + 732 + (* Backup key *) 733 + (match changes.backup_decryption_key with 734 + | Some key -> t.backup_key <- Some key 735 + | None -> ()); 736 + 737 + Ok () 738 + 739 + let get_sessions t ~sender_key = 740 + match Hashtbl.find_opt t.olm_sessions sender_key with 741 + | Some sessions -> sessions 742 + | None -> [] 743 + 744 + let get_session t ~session_id = 745 + Hashtbl.find_opt t.olm_sessions_by_id session_id 746 + 747 + let get_inbound_group_session t ~room_id ~session_id = 748 + let key = Matrix_proto.Id.Room_id.to_string room_id ^ ":" ^ session_id in 749 + Hashtbl.find_opt t.inbound_sessions key 750 + 751 + let get_inbound_group_sessions_for_room t ~room_id = 752 + let room_prefix = Matrix_proto.Id.Room_id.to_string room_id ^ ":" in 753 + Hashtbl.fold (fun key session acc -> 754 + if String.length key >= String.length room_prefix && 755 + String.sub key 0 (String.length room_prefix) = room_prefix 756 + then session :: acc 757 + else acc 758 + ) t.inbound_sessions [] 759 + 760 + let get_sessions_for_backup t ~limit = 761 + let unbacked_up = Hashtbl.fold (fun _key session acc -> 762 + if not session.backed_up then session :: acc else acc 763 + ) t.inbound_sessions [] in 764 + if List.length unbacked_up <= limit then unbacked_up 765 + else 766 + let rec take n lst = 767 + if n <= 0 then [] 768 + else match lst with 769 + | [] -> [] 770 + | x :: xs -> x :: take (n - 1) xs 771 + in 772 + take limit unbacked_up 773 + 774 + let mark_sessions_backed_up t ~session_ids = 775 + List.iter (fun session_id -> 776 + Hashtbl.iter (fun key (session : inbound_group_session) -> 777 + if session.session_id = session_id then 778 + Hashtbl.replace t.inbound_sessions key { session with backed_up = true } 779 + ) t.inbound_sessions 780 + ) session_ids 781 + 782 + let get_outbound_group_session t ~room_id = 783 + Hashtbl.find_opt t.outbound_sessions (Matrix_proto.Id.Room_id.to_string room_id) 784 + 785 + let get_device t ~user_id ~device_id = 786 + let key = 787 + Matrix_proto.Id.User_id.to_string user_id ^ ":" ^ 788 + Matrix_proto.Id.Device_id.to_string device_id 789 + in 790 + Hashtbl.find_opt t.devices key 791 + 792 + let get_user_devices t ~user_id = 793 + let user_prefix = Matrix_proto.Id.User_id.to_string user_id ^ ":" in 794 + Hashtbl.fold (fun key device acc -> 795 + if String.length key >= String.length user_prefix && 796 + String.sub key 0 (String.length user_prefix) = user_prefix 797 + then device :: acc 798 + else acc 799 + ) t.devices [] 800 + 801 + let get_user_identity t ~user_id = 802 + Hashtbl.find_opt t.user_identities (Matrix_proto.Id.User_id.to_string user_id) 803 + 804 + let get_private_identity t = t.private_identity 805 + 806 + let get_tracked_users t = 807 + Hashtbl.fold (fun _key user acc -> user :: acc) t.tracked_users [] 808 + 809 + let mark_user_dirty t ~user_id = 810 + let key = Matrix_proto.Id.User_id.to_string user_id in 811 + match Hashtbl.find_opt t.tracked_users key with 812 + | Some user -> Hashtbl.replace t.tracked_users key { user with dirty = true } 813 + | None -> Hashtbl.add t.tracked_users key { user_id; dirty = true } 814 + 815 + let is_message_known t ~hash = 816 + Hashtbl.mem t.message_hashes hash 817 + 818 + let get_backup_key t = t.backup_key 819 + let set_backup_key t key = t.backup_key <- Some key 820 + 821 + let get_custom_value t key = Hashtbl.find_opt t.custom_values key 822 + let set_custom_value t key value = Hashtbl.replace t.custom_values key value 823 + let remove_custom_value t key = Hashtbl.remove t.custom_values key 824 + 825 + let clear t = 826 + t.account_pickle <- None; 827 + t.private_identity <- None; 828 + Hashtbl.clear t.olm_sessions; 829 + Hashtbl.clear t.olm_sessions_by_id; 830 + Hashtbl.clear t.inbound_sessions; 831 + Hashtbl.clear t.outbound_sessions; 832 + Hashtbl.clear t.devices; 833 + Hashtbl.clear t.user_identities; 834 + Hashtbl.clear t.tracked_users; 835 + Hashtbl.clear t.message_hashes; 836 + t.backup_key <- None; 837 + Hashtbl.clear t.custom_values 838 + end 839 + 840 + (** In-memory event cache store implementation *) 841 + module Memory_event_cache_store : EVENT_CACHE_STORE = struct 842 + type error = [ `Storage_error of string ] 843 + 844 + type t = { 845 + events : (string, cached_event) Hashtbl.t; (* room_id:event_id -> event *) 846 + room_events : (string, cached_event list) Hashtbl.t; (* room_id -> events (newest first) *) 847 + chunks : (string, event_chunk) Hashtbl.t; (* room_id:chunk_id -> chunk *) 848 + last_chunks : (string, int) Hashtbl.t; (* room_id -> last chunk id *) 849 + mutable next_chunk_id : int; (* For generating new chunk IDs *) 850 + } [@@warning "-69"] 851 + 852 + let create () = { 853 + events = Hashtbl.create 1024; 854 + room_events = Hashtbl.create 64; 855 + chunks = Hashtbl.create 128; 856 + last_chunks = Hashtbl.create 64; 857 + next_chunk_id = 0; 858 + } 859 + 860 + let event_key room_id event_id = 861 + Matrix_proto.Id.Room_id.to_string room_id ^ ":" ^ 862 + Matrix_proto.Id.Event_id.to_string event_id 863 + 864 + let chunk_key room_id chunk_id = 865 + Matrix_proto.Id.Room_id.to_string room_id ^ ":" ^ string_of_int chunk_id 866 + 867 + let save_event t event = 868 + let key = event_key event.room_id event.event_id in 869 + Hashtbl.replace t.events key event; 870 + (* Also add to room events list *) 871 + let room_id_str = Matrix_proto.Id.Room_id.to_string event.room_id in 872 + let events = match Hashtbl.find_opt t.room_events room_id_str with 873 + | Some lst -> event :: lst 874 + | None -> [event] 875 + in 876 + Hashtbl.replace t.room_events room_id_str events 877 + 878 + let get_event t ~room_id ~event_id = 879 + Hashtbl.find_opt t.events (event_key room_id event_id) 880 + 881 + let get_room_events t ~room_id ?limit ?event_type () = 882 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 883 + match Hashtbl.find_opt t.room_events room_id_str with 884 + | None -> [] 885 + | Some events -> 886 + let filtered = match event_type with 887 + | None -> events 888 + | Some et -> List.filter (fun e -> e.event_type = et) events 889 + in 890 + match limit with 891 + | None -> filtered 892 + | Some n -> 893 + let rec take n lst = match n, lst with 894 + | 0, _ | _, [] -> [] 895 + | n, x :: xs -> x :: take (n - 1) xs 896 + in 897 + take n filtered 898 + 899 + let get_last_chunk t ~room_id = 900 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 901 + match Hashtbl.find_opt t.last_chunks room_id_str with 902 + | None -> None 903 + | Some chunk_id -> Hashtbl.find_opt t.chunks (chunk_key room_id chunk_id) 904 + 905 + let get_chunk t ~room_id ~chunk_id = 906 + Hashtbl.find_opt t.chunks (chunk_key room_id chunk_id) 907 + 908 + let save_chunk t ~room_id chunk = 909 + let key = chunk_key room_id chunk.chunk_id in 910 + Hashtbl.replace t.chunks key chunk; 911 + (* Update last chunk if this is newer *) 912 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 913 + match Hashtbl.find_opt t.last_chunks room_id_str with 914 + | None -> Hashtbl.add t.last_chunks room_id_str chunk.chunk_id 915 + | Some last_id when chunk.chunk_id > last_id -> 916 + Hashtbl.replace t.last_chunks room_id_str chunk.chunk_id 917 + | _ -> () 918 + 919 + let filter_duplicates t ~room_id ~event_ids = 920 + List.filter (fun event_id -> 921 + not (Hashtbl.mem t.events (event_key room_id event_id)) 922 + ) event_ids 923 + 924 + let find_event_relations t ~room_id ~event_id ?rel_type () = 925 + let target_id = Matrix_proto.Id.Event_id.to_string event_id in 926 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 927 + match Hashtbl.find_opt t.room_events room_id_str with 928 + | None -> [] 929 + | Some events -> 930 + List.filter (fun event -> 931 + (* Check if event has m.relates_to pointing to target *) 932 + match event.content with 933 + | Jsont.Object (fields, _) -> 934 + let has_relation = 935 + List.exists (fun ((name, _), value) -> 936 + name = "m.relates_to" && 937 + match value with 938 + | Jsont.Object (rel_fields, _) -> 939 + List.exists (fun ((n, _), v) -> 940 + n = "event_id" && 941 + match v with 942 + | Jsont.String (s, _) -> s = target_id 943 + | _ -> false 944 + ) rel_fields && 945 + (match rel_type with 946 + | None -> true 947 + | Some rt -> 948 + List.exists (fun ((n, _), v) -> 949 + n = "rel_type" && 950 + match v with 951 + | Jsont.String (s, _) -> s = rt 952 + | _ -> false 953 + ) rel_fields) 954 + | _ -> false 955 + ) fields 956 + in 957 + has_relation 958 + | _ -> false 959 + ) events 960 + 961 + let remove_room t ~room_id = 962 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 963 + (* Remove all events for the room *) 964 + let keys_to_remove = Hashtbl.fold (fun key _event acc -> 965 + if String.length key > String.length room_id_str + 1 && 966 + String.sub key 0 (String.length room_id_str + 1) = room_id_str ^ ":" 967 + then key :: acc 968 + else acc 969 + ) t.events [] in 970 + List.iter (Hashtbl.remove t.events) keys_to_remove; 971 + (* Remove room events list *) 972 + Hashtbl.remove t.room_events room_id_str; 973 + (* Remove chunks *) 974 + let chunk_keys_to_remove = Hashtbl.fold (fun key _chunk acc -> 975 + if String.length key > String.length room_id_str + 1 && 976 + String.sub key 0 (String.length room_id_str + 1) = room_id_str ^ ":" 977 + then key :: acc 978 + else acc 979 + ) t.chunks [] in 980 + List.iter (Hashtbl.remove t.chunks) chunk_keys_to_remove; 981 + (* Remove last chunk ref *) 982 + Hashtbl.remove t.last_chunks room_id_str 983 + 984 + let clear t = 985 + Hashtbl.clear t.events; 986 + Hashtbl.clear t.room_events; 987 + Hashtbl.clear t.chunks; 988 + Hashtbl.clear t.last_chunks; 989 + t.next_chunk_id <- 0 990 + end 991 + 992 + (** {1 Store Creation Helpers} *) 993 + 994 + (** Create in-memory state store *) 995 + let create_memory_state_store = Memory_state_store.create 996 + 997 + (** Create in-memory crypto store *) 998 + let create_memory_crypto_store = Memory_crypto_store.create 999 + 1000 + (** Create in-memory event cache store *) 1001 + let create_memory_event_cache_store = Memory_event_cache_store.create
+246
lib/matrix_client/sync.ml
··· 1 + (** Sync operations and long-polling loop. *) 2 + 3 + (* Sync parameters *) 4 + type params = { 5 + filter : string option; 6 + since : string option; 7 + full_state : bool; 8 + set_presence : [ `Online | `Offline | `Unavailable ] option; 9 + timeout : int; 10 + } 11 + 12 + let default_params = { 13 + filter = None; 14 + since = None; 15 + full_state = false; 16 + set_presence = None; 17 + timeout = 30000; 18 + } 19 + 20 + let presence_to_string = function 21 + | `Online -> "online" 22 + | `Offline -> "offline" 23 + | `Unavailable -> "unavailable" 24 + 25 + let sync client ?(params = default_params) () = 26 + let query = [] 27 + |> (fun q -> match params.filter with Some f -> ("filter", f) :: q | None -> q) 28 + |> (fun q -> match params.since with Some s -> ("since", s) :: q | None -> q) 29 + |> (fun q -> if params.full_state then ("full_state", "true") :: q else q) 30 + |> (fun q -> match params.set_presence with Some p -> ("set_presence", presence_to_string p) :: q | None -> q) 31 + |> (fun q -> ("timeout", string_of_int params.timeout) :: q) 32 + in 33 + let query = if query = [] then None else Some query in 34 + match Client.get client ~path:"/sync" ?query () with 35 + | Error e -> Error e 36 + | Ok body -> Client.decode_response Matrix_proto.Sync.Response.jsont body 37 + 38 + (* Sync loop *) 39 + type action = 40 + | Continue 41 + | Stop 42 + | Retry_after of int 43 + 44 + type callbacks = { 45 + on_sync : Matrix_proto.Sync.Response.t -> action; 46 + on_error : Error.t -> action; 47 + } 48 + 49 + let rec sync_loop client clock params callbacks since = 50 + let params = { params with since } in 51 + match sync client ~params () with 52 + | Error e -> 53 + (match callbacks.on_error e with 54 + | Continue -> sync_loop client clock params callbacks since 55 + | Stop -> () 56 + | Retry_after ms -> 57 + Eio.Time.sleep clock (float_of_int ms /. 1000.0); 58 + sync_loop client clock params callbacks since) 59 + | Ok response -> 60 + (match callbacks.on_sync response with 61 + | Continue -> sync_loop client clock params callbacks (Some response.next_batch) 62 + | Stop -> () 63 + | Retry_after ms -> 64 + Eio.Time.sleep clock (float_of_int ms /. 1000.0); 65 + sync_loop client clock params callbacks (Some response.next_batch)) 66 + 67 + let sync_forever client ~clock ?initial_since ?(params = default_params) ~callbacks () = 68 + sync_loop client clock params callbacks initial_since 69 + 70 + (* Filter types *) 71 + type event_filter = { 72 + limit : int option; 73 + not_senders : string list; 74 + not_types : string list; 75 + senders : string list; 76 + types : string list; 77 + } 78 + 79 + type room_event_filter = { 80 + limit : int option; 81 + not_senders : string list; 82 + not_types : string list; 83 + senders : string list; 84 + types : string list; 85 + lazy_load_members : bool; 86 + include_redundant_members : bool; 87 + not_rooms : string list; 88 + rooms : string list; 89 + contains_url : bool option; 90 + } 91 + 92 + type room_filter = { 93 + not_rooms : string list; 94 + rooms : string list; 95 + ephemeral : room_event_filter option; 96 + include_leave : bool; 97 + state : room_event_filter option; 98 + timeline : room_event_filter option; 99 + account_data : room_event_filter option; 100 + } 101 + 102 + type filter = { 103 + event_fields : string list; 104 + event_format : [ `Client | `Federation ]; 105 + presence : event_filter option; 106 + account_data : event_filter option; 107 + room : room_filter option; 108 + } 109 + 110 + let default_event_filter = { 111 + limit = None; 112 + not_senders = []; 113 + not_types = []; 114 + senders = []; 115 + types = []; 116 + } 117 + 118 + let default_room_event_filter = { 119 + limit = None; 120 + not_senders = []; 121 + not_types = []; 122 + senders = []; 123 + types = []; 124 + lazy_load_members = true; 125 + include_redundant_members = false; 126 + not_rooms = []; 127 + rooms = []; 128 + contains_url = None; 129 + } 130 + 131 + let default_room_filter = { 132 + not_rooms = []; 133 + rooms = []; 134 + ephemeral = None; 135 + include_leave = false; 136 + state = None; 137 + timeline = None; 138 + account_data = None; 139 + } 140 + 141 + let default_filter = { 142 + event_fields = []; 143 + event_format = `Client; 144 + presence = None; 145 + account_data = None; 146 + room = None; 147 + } 148 + 149 + (* Filter JSON codecs *) 150 + let event_filter_jsont : event_filter Jsont.t = 151 + let open Jsont.Object in 152 + map (fun limit not_senders not_types senders types -> 153 + ({ limit; not_senders; not_types; senders; types } : event_filter)) 154 + |> opt_mem "limit" Jsont.int ~enc:(fun (t : event_filter) -> t.limit) 155 + |> mem "not_senders" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : event_filter) -> t.not_senders) 156 + |> mem "not_types" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : event_filter) -> t.not_types) 157 + |> mem "senders" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : event_filter) -> t.senders) 158 + |> mem "types" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : event_filter) -> t.types) 159 + |> finish 160 + 161 + let room_event_filter_jsont : room_event_filter Jsont.t = 162 + let open Jsont.Object in 163 + map (fun limit not_senders not_types senders types lazy_load_members 164 + include_redundant_members not_rooms rooms contains_url -> 165 + ({ limit; not_senders; not_types; senders; types; lazy_load_members; 166 + include_redundant_members; not_rooms; rooms; contains_url } : room_event_filter)) 167 + |> opt_mem "limit" Jsont.int ~enc:(fun (t : room_event_filter) -> t.limit) 168 + |> mem "not_senders" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : room_event_filter) -> t.not_senders) 169 + |> mem "not_types" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : room_event_filter) -> t.not_types) 170 + |> mem "senders" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : room_event_filter) -> t.senders) 171 + |> mem "types" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : room_event_filter) -> t.types) 172 + |> mem "lazy_load_members" Jsont.bool ~dec_absent:false ~enc:(fun (t : room_event_filter) -> t.lazy_load_members) 173 + |> mem "include_redundant_members" Jsont.bool ~dec_absent:false ~enc:(fun (t : room_event_filter) -> t.include_redundant_members) 174 + |> mem "not_rooms" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : room_event_filter) -> t.not_rooms) 175 + |> mem "rooms" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : room_event_filter) -> t.rooms) 176 + |> opt_mem "contains_url" Jsont.bool ~enc:(fun (t : room_event_filter) -> t.contains_url) 177 + |> finish 178 + 179 + let room_filter_jsont : room_filter Jsont.t = 180 + let open Jsont.Object in 181 + map (fun not_rooms rooms ephemeral include_leave state timeline account_data -> 182 + ({ not_rooms; rooms; ephemeral; include_leave; state; timeline; account_data } : room_filter)) 183 + |> mem "not_rooms" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : room_filter) -> t.not_rooms) 184 + |> mem "rooms" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : room_filter) -> t.rooms) 185 + |> opt_mem "ephemeral" room_event_filter_jsont ~enc:(fun (t : room_filter) -> t.ephemeral) 186 + |> mem "include_leave" Jsont.bool ~dec_absent:false ~enc:(fun (t : room_filter) -> t.include_leave) 187 + |> opt_mem "state" room_event_filter_jsont ~enc:(fun (t : room_filter) -> t.state) 188 + |> opt_mem "timeline" room_event_filter_jsont ~enc:(fun (t : room_filter) -> t.timeline) 189 + |> opt_mem "account_data" room_event_filter_jsont ~enc:(fun (t : room_filter) -> t.account_data) 190 + |> finish 191 + 192 + let event_format_jsont : [ `Client | `Federation ] Jsont.t = 193 + Jsont.of_of_string ~kind:"event_format" 194 + ~enc:(function `Client -> "client" | `Federation -> "federation") 195 + (function 196 + | "client" -> Ok `Client 197 + | "federation" -> Ok `Federation 198 + | s -> Error ("Unknown event_format: " ^ s)) 199 + 200 + let filter_jsont : filter Jsont.t = 201 + let open Jsont.Object in 202 + map (fun event_fields event_format presence account_data room -> 203 + ({ event_fields; event_format; presence; account_data; room } : filter)) 204 + |> mem "event_fields" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : filter) -> t.event_fields) 205 + |> mem "event_format" event_format_jsont ~dec_absent:`Client ~enc:(fun (t : filter) -> t.event_format) 206 + |> opt_mem "presence" event_filter_jsont ~enc:(fun (t : filter) -> t.presence) 207 + |> opt_mem "account_data" event_filter_jsont ~enc:(fun (t : filter) -> t.account_data) 208 + |> opt_mem "room" room_filter_jsont ~enc:(fun (t : filter) -> t.room) 209 + |> finish 210 + 211 + (* Filter API *) 212 + type filter_response = { 213 + filter_id : string; 214 + } [@@warning "-69"] 215 + 216 + let filter_response_jsont = 217 + Jsont.Object.( 218 + map (fun filter_id -> { filter_id }) 219 + |> mem "filter_id" Jsont.string 220 + |> finish) 221 + 222 + let create_filter client ~filter = 223 + match Client.user_id client with 224 + | None -> Error (Error.Network_error "Not logged in") 225 + | Some user_id -> 226 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 227 + let path = Printf.sprintf "/user/%s/filter" user_id_str in 228 + match Client.encode_body filter_jsont filter with 229 + | Error e -> Error e 230 + | Ok body -> 231 + match Client.post client ~path ~body () with 232 + | Error e -> Error e 233 + | Ok body -> 234 + match Client.decode_response filter_response_jsont body with 235 + | Error e -> Error e 236 + | Ok resp -> Ok resp.filter_id 237 + 238 + let get_filter client ~filter_id = 239 + match Client.user_id client with 240 + | None -> Error (Error.Network_error "Not logged in") 241 + | Some user_id -> 242 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 243 + let path = Printf.sprintf "/user/%s/filter/%s" user_id_str filter_id in 244 + match Client.get client ~path () with 245 + | Error e -> Error e 246 + | Ok body -> Client.decode_response filter_jsont body
+135
lib/matrix_client/sync.mli
··· 1 + (** Sync operations and long-polling loop. *) 2 + 3 + (** {1 Sync API} *) 4 + 5 + (** Sync parameters. *) 6 + type params = { 7 + filter : string option; 8 + (** Filter ID to use (from {!create_filter}). *) 9 + since : string option; 10 + (** Sync token from previous sync. *) 11 + full_state : bool; 12 + (** If true, return full state even if since is provided. *) 13 + set_presence : [ `Online | `Offline | `Unavailable ] option; 14 + (** Presence state to set. *) 15 + timeout : int; 16 + (** Long-poll timeout in milliseconds. 0 for immediate return. *) 17 + } 18 + 19 + (** Default sync parameters: 30 second timeout, no filter. *) 20 + val default_params : params 21 + 22 + (** Perform a single sync request. 23 + 24 + Returns the sync response from the server. The [next_batch] field 25 + should be used as [since] for the next sync request. *) 26 + val sync : 27 + Client.t -> 28 + ?params:params -> 29 + unit -> 30 + (Matrix_proto.Sync.Response.t, Error.t) result 31 + 32 + (** {1 Sync Loop} *) 33 + 34 + (** Action to take after processing a sync response or error. *) 35 + type action = 36 + | Continue (** Continue syncing *) 37 + | Stop (** Stop the sync loop *) 38 + | Retry_after of int (** Retry after N milliseconds *) 39 + 40 + (** Callbacks for the sync loop. *) 41 + type callbacks = { 42 + on_sync : Matrix_proto.Sync.Response.t -> action; 43 + (** Called for each successful sync response. *) 44 + on_error : Error.t -> action; 45 + (** Called when sync fails. *) 46 + } 47 + 48 + (** Run a continuous sync loop. 49 + 50 + This function blocks and continuously syncs with the server, 51 + calling the appropriate callback for each response or error. 52 + The loop continues until a callback returns [Stop]. 53 + 54 + @param clock Eio clock for sleeping on Retry_after. 55 + @param initial_since Starting sync token (None for initial sync). 56 + @param params Sync parameters to use (timeout, filter, etc.). *) 57 + val sync_forever : 58 + Client.t -> 59 + clock:_ Eio.Time.clock -> 60 + ?initial_since:string -> 61 + ?params:params -> 62 + callbacks:callbacks -> 63 + unit -> 64 + unit 65 + 66 + (** {1 Filters} *) 67 + 68 + (** Event filter for sync. *) 69 + type event_filter = { 70 + limit : int option; 71 + not_senders : string list; 72 + not_types : string list; 73 + senders : string list; 74 + types : string list; 75 + } 76 + 77 + (** Room event filter. *) 78 + type room_event_filter = { 79 + limit : int option; 80 + not_senders : string list; 81 + not_types : string list; 82 + senders : string list; 83 + types : string list; 84 + lazy_load_members : bool; 85 + include_redundant_members : bool; 86 + not_rooms : string list; 87 + rooms : string list; 88 + contains_url : bool option; 89 + } 90 + 91 + (** Room filter. *) 92 + type room_filter = { 93 + not_rooms : string list; 94 + rooms : string list; 95 + ephemeral : room_event_filter option; 96 + include_leave : bool; 97 + state : room_event_filter option; 98 + timeline : room_event_filter option; 99 + account_data : room_event_filter option; 100 + } 101 + 102 + (** Complete sync filter. *) 103 + type filter = { 104 + event_fields : string list; 105 + event_format : [ `Client | `Federation ]; 106 + presence : event_filter option; 107 + account_data : event_filter option; 108 + room : room_filter option; 109 + } 110 + 111 + (** Default empty event filter. *) 112 + val default_event_filter : event_filter 113 + 114 + (** Default room event filter with lazy loading enabled. *) 115 + val default_room_event_filter : room_event_filter 116 + 117 + (** Default room filter. *) 118 + val default_room_filter : room_filter 119 + 120 + (** Default filter. *) 121 + val default_filter : filter 122 + 123 + (** Create a filter on the homeserver. 124 + 125 + Returns the filter ID that can be used in sync requests. *) 126 + val create_filter : 127 + Client.t -> 128 + filter:filter -> 129 + (string, Error.t) result 130 + 131 + (** Get an existing filter from the homeserver. *) 132 + val get_filter : 133 + Client.t -> 134 + filter_id:string -> 135 + (filter, Error.t) result
+423
lib/matrix_client/timeline.ml
··· 1 + (** Room timeline management and event caching. 2 + 3 + This module provides: 4 + - Event storage and retrieval 5 + - Timeline pagination (forward and backward) 6 + - Room state tracking 7 + - Event deduplication *) 8 + 9 + (** A linked chunk data structure for efficient timeline operations. 10 + Based on the matrix-rust-sdk LinkedChunk pattern. *) 11 + module LinkedChunk = struct 12 + type 'a chunk = { 13 + mutable items : 'a list; 14 + mutable prev : 'a chunk option; 15 + mutable next : 'a chunk option; 16 + id : int; 17 + max_size : int; 18 + } 19 + 20 + type 'a t = { 21 + mutable head : 'a chunk option; 22 + mutable tail : 'a chunk option; 23 + mutable next_id : int; 24 + max_chunk_size : int; 25 + } 26 + 27 + let create ?(max_chunk_size = 50) () = { 28 + head = None; 29 + tail = None; 30 + next_id = 0; 31 + max_chunk_size; 32 + } 33 + 34 + let create_chunk t = 35 + let chunk = { 36 + items = []; 37 + prev = None; 38 + next = None; 39 + id = t.next_id; 40 + max_size = t.max_chunk_size; 41 + } in 42 + t.next_id <- t.next_id + 1; 43 + chunk 44 + 45 + (** Push item to the back of the timeline *) 46 + let push_back t item = 47 + match t.tail with 48 + | None -> 49 + let chunk = create_chunk t in 50 + chunk.items <- [item]; 51 + t.head <- Some chunk; 52 + t.tail <- Some chunk 53 + | Some tail -> 54 + if List.length tail.items >= tail.max_size then begin 55 + (* Create new chunk *) 56 + let chunk = create_chunk t in 57 + chunk.items <- [item]; 58 + chunk.prev <- Some tail; 59 + tail.next <- Some chunk; 60 + t.tail <- Some chunk 61 + end else 62 + tail.items <- tail.items @ [item] 63 + 64 + (** Push item to the front of the timeline *) 65 + let push_front t item = 66 + match t.head with 67 + | None -> 68 + let chunk = create_chunk t in 69 + chunk.items <- [item]; 70 + t.head <- Some chunk; 71 + t.tail <- Some chunk 72 + | Some head -> 73 + if List.length head.items >= head.max_size then begin 74 + (* Create new chunk *) 75 + let chunk = create_chunk t in 76 + chunk.items <- [item]; 77 + chunk.next <- Some head; 78 + head.prev <- Some chunk; 79 + t.head <- Some chunk 80 + end else 81 + head.items <- item :: head.items 82 + 83 + (** Push items to the front (for back-pagination) *) 84 + let push_front_items t items = 85 + List.iter (fun item -> push_front t item) (List.rev items) 86 + 87 + (** Iterate over all items from oldest to newest *) 88 + let iter f t = 89 + let rec iter_chunk = function 90 + | None -> () 91 + | Some chunk -> 92 + List.iter f chunk.items; 93 + iter_chunk chunk.next 94 + in 95 + iter_chunk t.head 96 + 97 + (** Iterate from newest to oldest *) 98 + let iter_rev f t = 99 + let rec iter_chunk = function 100 + | None -> () 101 + | Some chunk -> 102 + List.iter f (List.rev chunk.items); 103 + iter_chunk chunk.prev 104 + in 105 + iter_chunk t.tail 106 + 107 + (** Get last N items *) 108 + let last_n t n = 109 + let result = ref [] in 110 + let count = ref 0 in 111 + iter_rev (fun item -> 112 + if !count < n then begin 113 + result := item :: !result; 114 + incr count 115 + end 116 + ) t; 117 + !result 118 + 119 + (** Total number of items *) 120 + let length t = 121 + let count = ref 0 in 122 + iter (fun _ -> incr count) t; 123 + !count 124 + 125 + (** Find an item by predicate *) 126 + let find_opt pred t = 127 + let result = ref None in 128 + let rec search_chunk = function 129 + | None -> () 130 + | Some chunk -> 131 + (match List.find_opt pred chunk.items with 132 + | Some item -> result := Some item 133 + | None -> search_chunk chunk.next) 134 + in 135 + search_chunk t.head; 136 + !result 137 + 138 + (** Clear all items *) 139 + let clear t = 140 + t.head <- None; 141 + t.tail <- None 142 + end 143 + 144 + (** Timeline event wrapper with metadata *) 145 + type event_item = { 146 + event : Jsont.json; (* Raw event JSON *) 147 + event_id : Matrix_proto.Id.Event_id.t; 148 + sender : Matrix_proto.Id.User_id.t; 149 + origin_server_ts : int64; 150 + event_type : string; (* e.g., "m.room.message" *) 151 + (* Local metadata *) 152 + local_echo : bool; (* true if this is a local echo not yet confirmed *) 153 + decrypted : bool; (* true if this was decrypted from E2EE *) 154 + } 155 + 156 + (** Room state entry *) 157 + type state_entry = { 158 + event_type : string; 159 + state_key : string; 160 + content : Jsont.json; 161 + sender : Matrix_proto.Id.User_id.t; 162 + event_id : Matrix_proto.Id.Event_id.t option; 163 + } 164 + 165 + (** Room timeline with state tracking *) 166 + type t = { 167 + room_id : Matrix_proto.Id.Room_id.t; 168 + (* Timeline events *) 169 + events : event_item LinkedChunk.t; 170 + (* Room state: (event_type, state_key) -> state_entry *) 171 + mutable state : ((string * string) * state_entry) list; 172 + (* Pagination tokens *) 173 + mutable prev_batch : string option; 174 + mutable next_batch : string option; 175 + (* Event ID index for deduplication *) 176 + mutable event_ids : string list; 177 + (* Maximum events to keep in memory *) 178 + max_events : int; 179 + (* Room summary *) 180 + mutable name : string option; 181 + mutable topic : string option; 182 + mutable avatar_url : string option; 183 + mutable canonical_alias : Matrix_proto.Id.Room_alias.t option; 184 + mutable joined_member_count : int; 185 + mutable invited_member_count : int; 186 + mutable is_encrypted : bool; 187 + mutable is_direct : bool; 188 + mutable notification_count : int; 189 + mutable highlight_count : int; 190 + } 191 + 192 + (** Create a new timeline for a room *) 193 + let create ~room_id ?(max_events = 1000) () = { 194 + room_id; 195 + events = LinkedChunk.create (); 196 + state = []; 197 + prev_batch = None; 198 + next_batch = None; 199 + event_ids = []; 200 + max_events; 201 + name = None; 202 + topic = None; 203 + avatar_url = None; 204 + canonical_alias = None; 205 + joined_member_count = 0; 206 + invited_member_count = 0; 207 + is_encrypted = false; 208 + is_direct = false; 209 + notification_count = 0; 210 + highlight_count = 0; 211 + } 212 + 213 + (** Check if an event is already in the timeline *) 214 + let has_event t event_id = 215 + let id_str = Matrix_proto.Id.Event_id.to_string event_id in 216 + List.mem id_str t.event_ids 217 + 218 + (** Add an event to the timeline *) 219 + let add_event t ~event ~event_id ~sender ~origin_server_ts ~event_type ?(local_echo = false) ?(decrypted = false) () = 220 + let id_str = Matrix_proto.Id.Event_id.to_string event_id in 221 + if not (List.mem id_str t.event_ids) then begin 222 + let item = { event; event_id; sender; origin_server_ts; event_type; local_echo; decrypted } in 223 + LinkedChunk.push_back t.events item; 224 + t.event_ids <- id_str :: t.event_ids; 225 + (* Trim if over limit *) 226 + if LinkedChunk.length t.events > t.max_events then begin 227 + (* TODO: Remove oldest events *) 228 + () 229 + end 230 + end 231 + 232 + (** Add events from back-pagination (older events) *) 233 + let add_events_back t events = 234 + List.iter (fun (event, event_id, sender, origin_server_ts, event_type, decrypted) -> 235 + let id_str = Matrix_proto.Id.Event_id.to_string event_id in 236 + if not (List.mem id_str t.event_ids) then begin 237 + let item = { event; event_id; sender; origin_server_ts; event_type; local_echo = false; decrypted } in 238 + LinkedChunk.push_front t.events item; 239 + t.event_ids <- id_str :: t.event_ids 240 + end 241 + ) events 242 + 243 + (** Helper to get a string field from JSON object *) 244 + let get_json_string_field content field = 245 + match content with 246 + | Jsont.Object (fields, _meta) -> 247 + (* name is (string * Meta.t) and mem is (name * json) *) 248 + let find_field name = 249 + List.find_opt (fun ((n, _meta), _v) -> String.equal n name) fields 250 + in 251 + (match find_field field with 252 + | Some (_, Jsont.String (s, _)) -> Some s 253 + | _ -> None) 254 + | _ -> None 255 + 256 + (** Update room state from a state event *) 257 + let update_state t ~event_type ~state_key ~content ~sender ?event_id () = 258 + let key = (event_type, state_key) in 259 + let entry = { event_type; state_key; content; sender; event_id } in 260 + t.state <- (key, entry) :: List.filter (fun (k, _) -> k <> key) t.state; 261 + (* Update summary fields based on state *) 262 + match event_type with 263 + | "m.room.name" -> 264 + t.name <- get_json_string_field content "name" 265 + | "m.room.topic" -> 266 + t.topic <- get_json_string_field content "topic" 267 + | "m.room.avatar" -> 268 + t.avatar_url <- get_json_string_field content "url" 269 + | "m.room.canonical_alias" -> 270 + (match get_json_string_field content "alias" with 271 + | Some alias -> 272 + (match Matrix_proto.Id.Room_alias.of_string alias with 273 + | Ok a -> t.canonical_alias <- Some a 274 + | Error _ -> ()) 275 + | None -> ()) 276 + | "m.room.encryption" -> 277 + t.is_encrypted <- true 278 + | _ -> () 279 + 280 + (** Get state for a specific event type and state key *) 281 + let get_state t ~event_type ~state_key = 282 + List.assoc_opt (event_type, state_key) t.state 283 + 284 + (** Get all state for an event type *) 285 + let get_state_by_type t ~event_type = 286 + List.filter_map (fun ((et, _sk), entry) -> 287 + if et = event_type then Some entry else None 288 + ) t.state 289 + 290 + (** Get room members from state *) 291 + let get_members t = 292 + get_state_by_type t ~event_type:"m.room.member" 293 + 294 + (** Get the last N events *) 295 + let get_last_events t n = 296 + LinkedChunk.last_n t.events n 297 + 298 + (** Get all events *) 299 + let get_all_events t = 300 + let result = ref [] in 301 + LinkedChunk.iter (fun item -> result := item :: !result) t.events; 302 + List.rev !result 303 + 304 + (** Find an event by ID *) 305 + let find_event t event_id = 306 + let target = Matrix_proto.Id.Event_id.to_string event_id in 307 + LinkedChunk.find_opt (fun (item : event_item) -> 308 + String.equal (Matrix_proto.Id.Event_id.to_string item.event_id) target 309 + ) t.events 310 + 311 + (** Get room display name (computed from state) *) 312 + let display_name t = 313 + match t.name with 314 + | Some name -> Some name 315 + | None -> 316 + match t.canonical_alias with 317 + | Some alias -> Some (Matrix_proto.Id.Room_alias.to_string alias) 318 + | None -> None (* Would compute from heroes in a full implementation *) 319 + 320 + (** Set pagination token for back-pagination *) 321 + let set_prev_batch t token = 322 + t.prev_batch <- Some token 323 + 324 + (** Set pagination token for forward sync *) 325 + let set_next_batch t token = 326 + t.next_batch <- Some token 327 + 328 + (** Update from sync response *) 329 + let update_from_sync t ~joined_count ~invited_count ~notification_count ~highlight_count = 330 + t.joined_member_count <- joined_count; 331 + t.invited_member_count <- invited_count; 332 + t.notification_count <- notification_count; 333 + t.highlight_count <- highlight_count 334 + 335 + (** Clear timeline (but keep state) *) 336 + let clear_timeline t = 337 + LinkedChunk.clear t.events; 338 + t.event_ids <- []; 339 + t.prev_batch <- None 340 + 341 + (** Replace local echo with confirmed event *) 342 + let confirm_local_echo t ~local_event_id ~confirmed_event_id = 343 + let local_id = Matrix_proto.Id.Event_id.to_string local_event_id in 344 + let confirmed_id = Matrix_proto.Id.Event_id.to_string confirmed_event_id in 345 + (* Find and update the local echo *) 346 + let found = ref false in 347 + LinkedChunk.iter (fun (item : event_item) -> 348 + if String.equal (Matrix_proto.Id.Event_id.to_string item.event_id) local_id then begin 349 + (* Can't mutate in LinkedChunk, so we just track dedup *) 350 + found := true 351 + end 352 + ) t.events; 353 + if !found then begin 354 + t.event_ids <- List.filter (fun id -> id <> local_id) t.event_ids; 355 + t.event_ids <- confirmed_id :: t.event_ids 356 + end 357 + 358 + (* Alias for the timeline create function before Cache module shadows it *) 359 + let create_timeline = create 360 + 361 + (** Room timeline cache - manages timelines for multiple rooms *) 362 + module Cache = struct 363 + type cache = { 364 + mutable rooms : (string * t) list; 365 + max_rooms : int; 366 + } 367 + 368 + let create ?(max_rooms = 100) () = { 369 + rooms = []; 370 + max_rooms; 371 + } 372 + 373 + let get_or_create cache room_id = 374 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 375 + match List.assoc_opt room_id_str cache.rooms with 376 + | Some timeline -> timeline 377 + | None -> 378 + let timeline = create_timeline ~room_id () in 379 + cache.rooms <- (room_id_str, timeline) :: cache.rooms; 380 + (* LRU eviction if needed *) 381 + if List.length cache.rooms > cache.max_rooms then 382 + cache.rooms <- List.rev (List.tl (List.rev cache.rooms)); 383 + timeline 384 + 385 + let get cache room_id = 386 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 387 + List.assoc_opt room_id_str cache.rooms 388 + 389 + let remove cache room_id = 390 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 391 + cache.rooms <- List.filter (fun (id, _) -> id <> room_id_str) cache.rooms 392 + 393 + let all_room_ids cache = 394 + List.filter_map (fun (id_str, _) -> 395 + match Matrix_proto.Id.Room_id.of_string id_str with 396 + | Ok id -> Some id 397 + | Error _ -> None 398 + ) cache.rooms 399 + end 400 + 401 + (** Pagination helper for fetching older messages *) 402 + let paginate_back client t ~limit = 403 + match t.prev_batch with 404 + | None -> Ok [] (* No more messages *) 405 + | Some from -> 406 + match Messages.get_messages client ~room_id:t.room_id ~from ~dir:Messages.Backward ~limit () with 407 + | Error e -> Error e 408 + | Ok response -> 409 + (* Update prev_batch for next pagination *) 410 + t.prev_batch <- response.Messages.end_; 411 + (* Add events to timeline *) 412 + let events = List.filter_map (fun (raw_event : Matrix_proto.Event.Raw_event.t) -> 413 + (* Extract fields from the Raw_event structure *) 414 + match raw_event.event_id with 415 + | Some event_id -> 416 + let event_type = Matrix_proto.Event.Event_type.to_string raw_event.type_ in 417 + let ts = raw_event.origin_server_ts in (* Timestamp.t is int64 *) 418 + (* tuple: (event, event_id, sender, ts, event_type, decrypted) *) 419 + Some (raw_event.content, event_id, raw_event.sender, ts, event_type, false) 420 + | None -> None 421 + ) response.Messages.chunk in 422 + add_events_back t events; 423 + Ok events
+31
lib/matrix_client/typing.ml
··· 1 + (** Typing notifications. *) 2 + 3 + type typing_request = { 4 + typing : bool; 5 + timeout : int option; 6 + } [@@warning "-69"] 7 + 8 + let typing_request_jsont = 9 + Jsont.Object.( 10 + map (fun typing timeout -> { typing; timeout }) 11 + |> mem "typing" Jsont.bool 12 + |> opt_mem "timeout" Jsont.int ~enc:(fun t -> t.timeout) 13 + |> finish) 14 + 15 + let set_typing client ~room_id ~typing ?timeout () = 16 + match Client.user_id client with 17 + | None -> Error (Error.Network_error "Not logged in") 18 + | Some user_id -> 19 + let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in 20 + let user_id_str = Matrix_proto.Id.User_id.to_string user_id in 21 + let path = Printf.sprintf "/rooms/%s/typing/%s" 22 + (Uri.pct_encode room_id_str) 23 + (Uri.pct_encode user_id_str) 24 + in 25 + let request = { typing; timeout } in 26 + match Client.encode_body typing_request_jsont request with 27 + | Error e -> Error e 28 + | Ok body -> 29 + match Client.put client ~path ~body () with 30 + | Error e -> Error e 31 + | Ok _ -> Ok ()
+13
lib/matrix_client/typing.mli
··· 1 + (** Typing notifications. *) 2 + 3 + (** Set typing status in a room. 4 + 5 + @param typing Whether the user is typing. 6 + @param timeout Typing timeout in milliseconds (default 30000). *) 7 + val set_typing : 8 + Client.t -> 9 + room_id:Matrix_proto.Id.Room_id.t -> 10 + typing:bool -> 11 + ?timeout:int -> 12 + unit -> 13 + (unit, Error.t) result
+435
lib/matrix_client/uiaa.ml
··· 1 + (** User-Interactive Authentication API (UIAA). 2 + 3 + UIAA is Matrix's mechanism for protecting sensitive operations that require 4 + additional verification beyond just an access token. Operations like: 5 + - Deleting devices 6 + - Changing passwords 7 + - Adding 3PIDs 8 + - Deactivating accounts 9 + 10 + When these operations return a 401 with UIAA challenge, clients must 11 + complete authentication stages before the operation can proceed. *) 12 + 13 + (** Authentication stage types *) 14 + type auth_type = 15 + | Password 16 + | Recaptcha 17 + | OAuth2 18 + | Email_identity 19 + | Msisdn (* Phone number *) 20 + | Dummy 21 + | Registration_token 22 + | Terms 23 + | Sso 24 + | Sso_fallback 25 + | Custom of string 26 + 27 + let auth_type_of_string = function 28 + | "m.login.password" -> Password 29 + | "m.login.recaptcha" -> Recaptcha 30 + | "m.login.oauth2" -> OAuth2 31 + | "m.login.email.identity" -> Email_identity 32 + | "m.login.msisdn" -> Msisdn 33 + | "m.login.dummy" -> Dummy 34 + | "m.login.registration_token" -> Registration_token 35 + | "m.login.terms" -> Terms 36 + | "m.login.sso" -> Sso 37 + | "org.matrix.login.sso.fallback" -> Sso_fallback 38 + | s -> Custom s 39 + 40 + let auth_type_to_string = function 41 + | Password -> "m.login.password" 42 + | Recaptcha -> "m.login.recaptcha" 43 + | OAuth2 -> "m.login.oauth2" 44 + | Email_identity -> "m.login.email.identity" 45 + | Msisdn -> "m.login.msisdn" 46 + | Dummy -> "m.login.dummy" 47 + | Registration_token -> "m.login.registration_token" 48 + | Terms -> "m.login.terms" 49 + | Sso -> "m.login.sso" 50 + | Sso_fallback -> "org.matrix.login.sso.fallback" 51 + | Custom s -> s 52 + 53 + (** Authentication flow - a sequence of stages that must be completed *) 54 + type auth_flow = { 55 + stages : auth_type list; 56 + } 57 + 58 + (* Internal type for JSON parsing *) 59 + type auth_flow_json = { 60 + stages_json : string list; 61 + } 62 + 63 + let auth_flow_jsont = 64 + let json_type = 65 + Jsont.Object.( 66 + map (fun stages_json -> { stages_json }) 67 + |> mem "stages" (Jsont.list Jsont.string) ~dec_absent:[] 68 + ~enc:(fun t -> t.stages_json) 69 + |> finish) 70 + in 71 + Jsont.map 72 + ~dec:(fun flow -> { stages = List.map auth_type_of_string flow.stages_json }) 73 + ~enc:(fun flow -> { stages_json = List.map auth_type_to_string flow.stages }) 74 + json_type 75 + 76 + (** UIAA response from server when authentication is required *) 77 + type uiaa_response = { 78 + session : string option; 79 + flows : auth_flow list; 80 + completed : auth_type list; 81 + params : Jsont.json option; 82 + error : string option; 83 + errcode : string option; 84 + } 85 + 86 + (* Internal type for JSON parsing *) 87 + type uiaa_response_json = { 88 + session_json : string option; 89 + flows_json : auth_flow list; 90 + completed_json : string list; 91 + params_json : Jsont.json option; 92 + error_json : string option; 93 + errcode_json : string option; 94 + } 95 + 96 + let uiaa_response_jsont = 97 + let json_type = 98 + Jsont.Object.( 99 + map (fun session_json flows_json completed_json params_json error_json errcode_json -> 100 + { session_json; flows_json; completed_json; params_json; error_json; errcode_json }) 101 + |> opt_mem "session" Jsont.string ~enc:(fun t -> t.session_json) 102 + |> mem "flows" (Jsont.list auth_flow_jsont) ~dec_absent:[] ~enc:(fun t -> t.flows_json) 103 + |> mem "completed" (Jsont.list Jsont.string) ~dec_absent:[] 104 + ~enc:(fun t -> t.completed_json) 105 + |> opt_mem "params" Jsont.json ~enc:(fun t -> t.params_json) 106 + |> opt_mem "error" Jsont.string ~enc:(fun t -> t.error_json) 107 + |> opt_mem "errcode" Jsont.string ~enc:(fun t -> t.errcode_json) 108 + |> finish) 109 + in 110 + Jsont.map 111 + ~dec:(fun r -> { 112 + session = r.session_json; 113 + flows = r.flows_json; 114 + completed = List.map auth_type_of_string r.completed_json; 115 + params = r.params_json; 116 + error = r.error_json; 117 + errcode = r.errcode_json; 118 + }) 119 + ~enc:(fun r -> { 120 + session_json = r.session; 121 + flows_json = r.flows; 122 + completed_json = List.map auth_type_to_string r.completed; 123 + params_json = r.params; 124 + error_json = r.error; 125 + errcode_json = r.errcode; 126 + }) 127 + json_type 128 + 129 + (** Authentication data to send in response to UIAA challenge *) 130 + type auth_data = 131 + | Password_auth of { 132 + identifier : user_identifier; 133 + password : string; 134 + session : string option; 135 + } 136 + | Recaptcha_auth of { 137 + response : string; 138 + session : string option; 139 + } 140 + | Email_identity_auth of { 141 + threepid_creds : threepid_creds; 142 + session : string option; 143 + } 144 + | Msisdn_auth of { 145 + threepid_creds : threepid_creds; 146 + session : string option; 147 + } 148 + | Dummy_auth of { 149 + session : string option; 150 + } 151 + | Token_auth of { 152 + token : string; 153 + session : string option; 154 + } 155 + | Terms_auth of { 156 + session : string option; 157 + } 158 + 159 + and user_identifier = 160 + | User of string (* user_id *) 161 + | ThirdParty of { medium : string; address : string } 162 + | Phone of { country : string; phone : string } 163 + 164 + and threepid_creds = { 165 + sid : string; 166 + client_secret : string; 167 + id_server : string option; 168 + id_access_token : string option; 169 + } 170 + 171 + (** Encode user identifier to JSON *) 172 + let user_identifier_to_json = function 173 + | User user_id -> 174 + Printf.sprintf {|{"type":"m.id.user","user":"%s"}|} user_id 175 + | ThirdParty { medium; address } -> 176 + Printf.sprintf {|{"type":"m.id.thirdparty","medium":"%s","address":"%s"}|} 177 + medium address 178 + | Phone { country; phone } -> 179 + Printf.sprintf {|{"type":"m.id.phone","country":"%s","phone":"%s"}|} 180 + country phone 181 + 182 + (** Encode auth data to JSON object for request body *) 183 + let auth_data_to_json = function 184 + | Password_auth { identifier; password; session } -> 185 + let session_part = match session with 186 + | Some s -> Printf.sprintf {|,"session":"%s"|} s 187 + | None -> "" 188 + in 189 + Printf.sprintf {|{"type":"m.login.password","identifier":%s,"password":"%s"%s}|} 190 + (user_identifier_to_json identifier) 191 + password 192 + session_part 193 + | Recaptcha_auth { response; session } -> 194 + let session_part = match session with 195 + | Some s -> Printf.sprintf {|,"session":"%s"|} s 196 + | None -> "" 197 + in 198 + Printf.sprintf {|{"type":"m.login.recaptcha","response":"%s"%s}|} 199 + response session_part 200 + | Email_identity_auth { threepid_creds = creds; session } -> 201 + let session_part = match session with 202 + | Some s -> Printf.sprintf {|,"session":"%s"|} s 203 + | None -> "" 204 + in 205 + let id_server_part = match creds.id_server with 206 + | Some s -> Printf.sprintf {|,"id_server":"%s"|} s 207 + | None -> "" 208 + in 209 + let id_token_part = match creds.id_access_token with 210 + | Some s -> Printf.sprintf {|,"id_access_token":"%s"|} s 211 + | None -> "" 212 + in 213 + Printf.sprintf 214 + {|{"type":"m.login.email.identity","threepid_creds":{"sid":"%s","client_secret":"%s"%s%s}%s}|} 215 + creds.sid creds.client_secret id_server_part id_token_part session_part 216 + | Msisdn_auth { threepid_creds = creds; session } -> 217 + let session_part = match session with 218 + | Some s -> Printf.sprintf {|,"session":"%s"|} s 219 + | None -> "" 220 + in 221 + let id_server_part = match creds.id_server with 222 + | Some s -> Printf.sprintf {|,"id_server":"%s"|} s 223 + | None -> "" 224 + in 225 + let id_token_part = match creds.id_access_token with 226 + | Some s -> Printf.sprintf {|,"id_access_token":"%s"|} s 227 + | None -> "" 228 + in 229 + Printf.sprintf 230 + {|{"type":"m.login.msisdn","threepid_creds":{"sid":"%s","client_secret":"%s"%s%s}%s}|} 231 + creds.sid creds.client_secret id_server_part id_token_part session_part 232 + | Dummy_auth { session } -> 233 + let session_part = match session with 234 + | Some s -> Printf.sprintf {|,"session":"%s"|} s 235 + | None -> "" 236 + in 237 + Printf.sprintf {|{"type":"m.login.dummy"%s}|} session_part 238 + | Token_auth { token; session } -> 239 + let session_part = match session with 240 + | Some s -> Printf.sprintf {|,"session":"%s"|} s 241 + | None -> "" 242 + in 243 + Printf.sprintf {|{"type":"m.login.registration_token","token":"%s"%s}|} 244 + token session_part 245 + | Terms_auth { session } -> 246 + let session_part = match session with 247 + | Some s -> Printf.sprintf {|,"session":"%s"|} s 248 + | None -> "" 249 + in 250 + Printf.sprintf {|{"type":"m.login.terms"%s}|} session_part 251 + 252 + (** Result of a UIAA operation *) 253 + type 'a uiaa_result = 254 + | Uiaa_success of 'a 255 + | Uiaa_auth_required of uiaa_response 256 + | Uiaa_error of Error.t 257 + 258 + (** Check if a response is a UIAA challenge (401 with flows) *) 259 + let is_uiaa_response status_code body = 260 + status_code = 401 && 261 + String.length body > 0 && 262 + (String.contains body 'f' && String.contains body 'l') (* Quick check for "flows" *) 263 + 264 + (** Parse a UIAA response from error body *) 265 + let parse_uiaa_response body = 266 + match Jsont_bytesrw.decode_string uiaa_response_jsont body with 267 + | Ok r -> Some r 268 + | Error _ -> None 269 + 270 + (** Create password authentication data *) 271 + let password_auth ~user_id ~password ?session () = 272 + Password_auth { 273 + identifier = User user_id; 274 + password; 275 + session; 276 + } 277 + 278 + (** Create dummy authentication (for flows that allow it) *) 279 + let dummy_auth ?session () = 280 + Dummy_auth { session } 281 + 282 + (** Create recaptcha authentication *) 283 + let recaptcha_auth ~response ?session () = 284 + Recaptcha_auth { response; session } 285 + 286 + (** Create email identity authentication *) 287 + let email_identity_auth ~sid ~client_secret ?id_server ?id_access_token ?session () = 288 + Email_identity_auth { 289 + threepid_creds = { sid; client_secret; id_server; id_access_token }; 290 + session; 291 + } 292 + 293 + (** Create registration token authentication *) 294 + let token_auth ~token ?session () = 295 + Token_auth { token; session } 296 + 297 + (** Create terms acceptance authentication *) 298 + let terms_auth ?session () = 299 + Terms_auth { session } 300 + 301 + (** Find the simplest flow to complete (fewest stages) *) 302 + let find_simplest_flow uiaa = 303 + match uiaa.flows with 304 + | [] -> None 305 + | flows -> 306 + Some (List.fold_left (fun acc flow -> 307 + if List.length flow.stages < List.length acc.stages then flow else acc 308 + ) (List.hd flows) flows) 309 + 310 + (** Check if a flow contains only the given auth types *) 311 + let flow_contains_only flow types = 312 + List.for_all (fun stage -> List.mem stage types) flow.stages 313 + 314 + (** Check if password-only auth is available *) 315 + let has_password_only_flow uiaa = 316 + List.exists (fun flow -> 317 + flow_contains_only flow [Password] || 318 + flow_contains_only flow [Password; Dummy] 319 + ) uiaa.flows 320 + 321 + (** Check if dummy auth is available (often used in development) *) 322 + let has_dummy_flow uiaa = 323 + List.exists (fun flow -> 324 + flow_contains_only flow [Dummy] 325 + ) uiaa.flows 326 + 327 + (** Get the remaining stages to complete *) 328 + let remaining_stages uiaa flow = 329 + List.filter (fun stage -> not (List.mem stage uiaa.completed)) flow.stages 330 + 331 + (** UIAA-protected request wrapper. 332 + 333 + This function handles the UIAA flow automatically: 334 + 1. Makes the initial request 335 + 2. If 401 with UIAA, calls the auth_callback to get auth data 336 + 3. Retries the request with auth data 337 + 4. Repeats until success or failure *) 338 + let with_uiaa ~make_request ~auth_callback = 339 + match make_request None with 340 + | Ok result -> Uiaa_success result 341 + | Error e -> 342 + (* Check if this is a UIAA challenge *) 343 + match e with 344 + | Error.Http_error { status = 401; body; _ } -> 345 + (match parse_uiaa_response body with 346 + | Some uiaa -> 347 + (* Get auth data from callback *) 348 + (match auth_callback uiaa with 349 + | Some auth_data -> 350 + (* Retry with auth *) 351 + (match make_request (Some (auth_data_to_json auth_data)) with 352 + | Ok result -> Uiaa_success result 353 + | Error e2 -> 354 + (* Check for another UIAA challenge (multi-stage) *) 355 + (match e2 with 356 + | Error.Http_error { status = 401; body = body2; _ } -> 357 + (match parse_uiaa_response body2 with 358 + | Some uiaa2 -> Uiaa_auth_required uiaa2 359 + | None -> Uiaa_error e2) 360 + | _ -> Uiaa_error e2)) 361 + | None -> 362 + Uiaa_auth_required uiaa) 363 + | None -> Uiaa_error e) 364 + | _ -> Uiaa_error e 365 + 366 + (** Helper to add auth field to a request body *) 367 + let add_auth_to_body body auth_json = 368 + if String.length body < 2 then 369 + Printf.sprintf {|{"auth":%s}|} auth_json 370 + else 371 + (* Insert auth field into existing JSON object *) 372 + let trimmed = String.trim body in 373 + if String.get trimmed 0 = '{' then 374 + let content = String.sub trimmed 1 (String.length trimmed - 2) in 375 + if String.length (String.trim content) = 0 then 376 + Printf.sprintf {|{"auth":%s}|} auth_json 377 + else 378 + Printf.sprintf {|{"auth":%s,%s}|} auth_json (String.trim content) 379 + else 380 + body 381 + 382 + (** Verify email for 3PID binding. 383 + First step: request token to be sent to email *) 384 + type request_token_response = { 385 + sid : string; 386 + submit_url : string option; 387 + } 388 + 389 + let request_token_response_jsont = 390 + Jsont.Object.( 391 + map (fun sid submit_url -> { sid; submit_url }) 392 + |> mem "sid" Jsont.string ~enc:(fun t -> t.sid) 393 + |> opt_mem "submit_url" Jsont.string ~enc:(fun t -> t.submit_url) 394 + |> finish) 395 + 396 + (** Request a token for email validation *) 397 + let request_email_token client ~email ~client_secret ~send_attempt ?next_link () = 398 + let path = "/account/3pid/email/requestToken" in 399 + let next_link_part = match next_link with 400 + | Some nl -> Printf.sprintf {|,"next_link":"%s"|} nl 401 + | None -> "" 402 + in 403 + let body = Printf.sprintf 404 + {|{"client_secret":"%s","email":"%s","send_attempt":%d%s}|} 405 + client_secret email send_attempt next_link_part 406 + in 407 + match Client.post client ~path ~body () with 408 + | Error e -> Error e 409 + | Ok resp_body -> Client.decode_response request_token_response_jsont resp_body 410 + 411 + (** Request a token for phone number validation *) 412 + let request_msisdn_token client ~country ~phone_number ~client_secret ~send_attempt ?next_link () = 413 + let path = "/account/3pid/msisdn/requestToken" in 414 + let next_link_part = match next_link with 415 + | Some nl -> Printf.sprintf {|,"next_link":"%s"|} nl 416 + | None -> "" 417 + in 418 + let body = Printf.sprintf 419 + {|{"client_secret":"%s","country":"%s","phone_number":"%s","send_attempt":%d%s}|} 420 + client_secret country phone_number send_attempt next_link_part 421 + in 422 + match Client.post client ~path ~body () with 423 + | Error e -> Error e 424 + | Ok resp_body -> Client.decode_response request_token_response_jsont resp_body 425 + 426 + (** Validate a token (submit to identity server or homeserver) *) 427 + let validate_email_token client ~sid ~client_secret ~token = 428 + let path = "/account/3pid/email/validate" in 429 + let body = Printf.sprintf 430 + {|{"sid":"%s","client_secret":"%s","token":"%s"}|} 431 + sid client_secret token 432 + in 433 + match Client.post client ~path ~body () with 434 + | Error e -> Error e 435 + | Ok _ -> Ok ()
+535
lib/matrix_client/verification.ml
··· 1 + (** Cross-signing and device verification. 2 + 3 + This module implements Matrix cross-signing for identity verification: 4 + - Cross-signing key management (master, self-signing, user-signing keys) 5 + - Device verification (local trust, cross-signing trust) 6 + - User identity verification 7 + - SAS (Short Authentication String) verification protocol *) 8 + 9 + open Mirage_crypto_ec 10 + 11 + (** {1 Trust States} *) 12 + 13 + (** Local trust state for a device *) 14 + type local_trust = 15 + | Verified (** Device manually verified by user *) 16 + | BlackListed (** Device is explicitly distrusted *) 17 + | Ignored (** Trust state is ignored *) 18 + | Unset (** No trust state set *) 19 + 20 + let local_trust_to_int = function 21 + | Verified -> 0 22 + | BlackListed -> 1 23 + | Ignored -> 2 24 + | Unset -> 3 25 + 26 + let local_trust_of_int = function 27 + | 0 -> Verified 28 + | 1 -> BlackListed 29 + | 2 -> Ignored 30 + | _ -> Unset 31 + 32 + (** Own user identity verification state *) 33 + type own_identity_state = 34 + | Never_verified (** Identity never verified *) 35 + | Verification_violation (** Was verified but identity changed *) 36 + | Identity_verified (** Currently verified *) 37 + 38 + (** {1 Cross-Signing Key Types} *) 39 + 40 + (** Cross-signing key usage *) 41 + type key_usage = 42 + | Master 43 + | Self_signing 44 + | User_signing 45 + 46 + let key_usage_to_string = function 47 + | Master -> "master" 48 + | Self_signing -> "self_signing" 49 + | User_signing -> "user_signing" 50 + 51 + let key_usage_of_string = function 52 + | "master" -> Some Master 53 + | "self_signing" -> Some Self_signing 54 + | "user_signing" -> Some User_signing 55 + | _ -> None 56 + 57 + (** Public cross-signing key *) 58 + type cross_signing_pubkey = { 59 + user_id : Matrix_proto.Id.User_id.t; 60 + usage : key_usage list; 61 + keys : (string * string) list; (** key_id -> base64 public key *) 62 + signatures : (string * (string * string) list) list; (** user_id -> (key_id, signature) *) 63 + } 64 + 65 + (** Extract the first Ed25519 key from a cross-signing key *) 66 + let get_ed25519_key csk = 67 + List.find_map (fun (key_id, key) -> 68 + if String.length key_id > 8 && String.sub key_id 0 8 = "ed25519:" then 69 + Some (key_id, key) 70 + else 71 + None 72 + ) csk.keys 73 + 74 + (** {1 Private Cross-Signing Keys} *) 75 + 76 + (** Private key for signing operations *) 77 + type private_key = { 78 + public_key : string; (** Base64 Ed25519 public key *) 79 + secret_key : string; (** Pickled secret key *) 80 + } 81 + 82 + (** Private cross-signing identity (holds the private keys) *) 83 + type private_cross_signing_identity = { 84 + user_id : Matrix_proto.Id.User_id.t; 85 + mutable master_key : private_key option; 86 + mutable self_signing_key : private_key option; 87 + mutable user_signing_key : private_key option; 88 + mutable shared : bool; (** True if public keys uploaded to server *) 89 + } 90 + 91 + (** Create a new private cross-signing identity *) 92 + let create_private_identity ~user_id = { 93 + user_id; 94 + master_key = None; 95 + self_signing_key = None; 96 + user_signing_key = None; 97 + shared = false; 98 + } 99 + 100 + (** Generate a new Ed25519 key pair *) 101 + let generate_ed25519_key () = 102 + let secret_key, public_key = Ed25519.generate () in 103 + let pub_bytes = Ed25519.pub_to_octets public_key in 104 + let secret_bytes = Ed25519.priv_to_octets secret_key in 105 + { 106 + public_key = Base64.encode_string pub_bytes; 107 + secret_key = Base64.encode_string secret_bytes; 108 + } 109 + 110 + (** Generate all cross-signing keys for a user *) 111 + let generate_cross_signing_keys identity = 112 + identity.master_key <- Some (generate_ed25519_key ()); 113 + identity.self_signing_key <- Some (generate_ed25519_key ()); 114 + identity.user_signing_key <- Some (generate_ed25519_key ()) 115 + 116 + (** Sign data with a private key *) 117 + let sign_with_key private_key data = 118 + match Base64.decode private_key.secret_key with 119 + | Error _ -> Error "Invalid secret key encoding" 120 + | Ok secret_bytes -> 121 + match Ed25519.priv_of_octets secret_bytes with 122 + | Error _ -> Error "Invalid secret key" 123 + | Ok secret_key -> 124 + let signature = Ed25519.sign ~key:secret_key data in 125 + Ok (Base64.encode_string ~pad:false signature) 126 + 127 + (** {1 Cross-Signing Public Keys} *) 128 + 129 + (** Master public key *) 130 + type master_pubkey = { 131 + key : cross_signing_pubkey; 132 + } 133 + 134 + (** Self-signing public key *) 135 + type self_signing_pubkey = { 136 + key : cross_signing_pubkey; 137 + } 138 + 139 + (** User-signing public key *) 140 + type user_signing_pubkey = { 141 + key : cross_signing_pubkey; 142 + } 143 + 144 + (** Create a public cross-signing key from private key *) 145 + let pubkey_from_private ~user_id ~usage private_key = 146 + let key_id = "ed25519:" ^ (String.sub private_key.public_key 0 (min 11 (String.length private_key.public_key))) in 147 + { 148 + user_id; 149 + usage = [usage]; 150 + keys = [(key_id, private_key.public_key)]; 151 + signatures = []; 152 + } 153 + 154 + (** {1 Signature Verification} *) 155 + 156 + (** Verify an Ed25519 signature *) 157 + let verify_signature ~public_key_b64 ~signature_b64 ~data = 158 + match Base64.decode public_key_b64, Base64.decode signature_b64 with 159 + | Error _, _ | _, Error _ -> false 160 + | Ok pub_bytes, Ok sig_bytes -> 161 + match Ed25519.pub_of_octets pub_bytes with 162 + | Error _ -> false 163 + | Ok pub_key -> 164 + Ed25519.verify ~key:pub_key sig_bytes ~msg:data 165 + 166 + (** Canonicalize JSON for signing (simplified) *) 167 + let canonicalize_json json = 168 + (* Simplified canonicalization - remove signatures and unsigned *) 169 + Jsont_bytesrw.encode_string Jsont.json json 170 + |> Result.value ~default:"" 171 + 172 + (** Verify that a cross-signing key is signed by another key *) 173 + let verify_cross_signing_signature ~signer_key ~signed_key = 174 + match get_ed25519_key signer_key with 175 + | None -> false 176 + | Some (signer_key_id, _signer_pub) -> 177 + let signer_user_id = Matrix_proto.Id.User_id.to_string signer_key.user_id in 178 + (* Look for signature in signed_key *) 179 + match List.assoc_opt signer_user_id signed_key.signatures with 180 + | None -> false 181 + | Some user_sigs -> 182 + match List.assoc_opt signer_key_id user_sigs with 183 + | None -> false 184 + | Some signature -> 185 + (* Would need to canonicalize and verify - simplified for now *) 186 + String.length signature > 0 187 + 188 + (** {1 Device Verification} *) 189 + 190 + (** Device with verification state *) 191 + type verified_device = { 192 + user_id : Matrix_proto.Id.User_id.t; 193 + device_id : Matrix_proto.Id.Device_id.t; 194 + keys : (string * string) list; 195 + algorithms : string list; 196 + display_name : string option; 197 + mutable local_trust : local_trust; 198 + mutable cross_signing_trusted : bool; 199 + } 200 + 201 + (** Create a verified device from device keys *) 202 + let create_verified_device ~user_id ~device_id ~keys ~algorithms ?display_name () = { 203 + user_id; 204 + device_id; 205 + keys; 206 + algorithms; 207 + display_name; 208 + local_trust = Unset; 209 + cross_signing_trusted = false; 210 + } 211 + 212 + (** Check if a device is verified (locally or via cross-signing) *) 213 + let is_device_verified device = 214 + device.local_trust = Verified || device.cross_signing_trusted 215 + 216 + (** Set local trust state for a device *) 217 + let set_device_local_trust device trust = 218 + device.local_trust <- trust 219 + 220 + (** Check if device is signed by a self-signing key *) 221 + let verify_device_with_self_signing ~self_signing_key ~device = 222 + match get_ed25519_key self_signing_key.key with 223 + | None -> false 224 + | Some (_key_id, _pub_key) -> 225 + (* Would verify signature on device keys *) 226 + (* Simplified - check if device has any signatures from user *) 227 + List.length device.keys > 0 228 + 229 + (** {1 User Identity} *) 230 + 231 + (** Own user identity *) 232 + type own_user_identity = { 233 + user_id : Matrix_proto.Id.User_id.t; 234 + master_key : master_pubkey; 235 + self_signing_key : self_signing_pubkey; 236 + user_signing_key : user_signing_pubkey; 237 + mutable state : own_identity_state; 238 + } 239 + 240 + (** Other user identity *) 241 + type other_user_identity = { 242 + user_id : Matrix_proto.Id.User_id.t; 243 + master_key : master_pubkey; 244 + self_signing_key : self_signing_pubkey; 245 + mutable pinned_master_key : master_pubkey option; (** For detecting changes *) 246 + mutable was_previously_verified : bool; 247 + } 248 + 249 + (** User identity (own or other) *) 250 + type user_identity = 251 + | Own of own_user_identity 252 + | Other of other_user_identity 253 + 254 + (** Get user ID from identity *) 255 + let identity_user_id = function 256 + | Own i -> i.user_id 257 + | Other i -> i.user_id 258 + 259 + (** Check if own identity is verified *) 260 + let is_own_identity_verified identity = 261 + identity.state = Identity_verified 262 + 263 + (** Check if other user identity is verified by us *) 264 + let is_other_identity_verified ~our_user_signing_key identity = 265 + (* Check if their master key is signed by our user-signing key *) 266 + verify_cross_signing_signature 267 + ~signer_key:our_user_signing_key.key 268 + ~signed_key:identity.master_key.key 269 + 270 + (** Check if a user's identity has changed since we pinned it *) 271 + let has_identity_changed identity = 272 + match identity.pinned_master_key with 273 + | None -> false 274 + | Some pinned -> 275 + (* Compare master key public keys *) 276 + match get_ed25519_key identity.master_key.key, get_ed25519_key pinned.key with 277 + | Some (_, k1), Some (_, k2) -> k1 <> k2 278 + | _ -> true 279 + 280 + (** Pin the current master key for future change detection *) 281 + let pin_master_key identity = 282 + identity.pinned_master_key <- Some identity.master_key 283 + 284 + (** {1 SAS Verification Protocol} *) 285 + 286 + (** SAS verification state *) 287 + type sas_state = 288 + | Sas_created 289 + | Sas_started 290 + | Sas_accepted 291 + | Sas_keys_exchanged 292 + | Sas_confirmed 293 + | Sas_mac_received 294 + | Sas_done 295 + | Sas_cancelled of string 296 + 297 + (** Short authentication string output *) 298 + type sas_output = 299 + | Decimal of int * int * int (** Three decimal numbers *) 300 + | Emoji of (int * string) list (** List of (index, description) *) 301 + 302 + (** SAS verification methods *) 303 + type sas_method = 304 + | Decimal_method 305 + | Emoji_method 306 + 307 + (** SAS verification session *) 308 + type sas_session = { 309 + flow_id : string; 310 + mutable state : sas_state; 311 + our_user_id : Matrix_proto.Id.User_id.t; 312 + our_device_id : Matrix_proto.Id.Device_id.t; 313 + their_user_id : Matrix_proto.Id.User_id.t; 314 + their_device_id : Matrix_proto.Id.Device_id.t; 315 + mutable their_public_key : string option; 316 + mutable our_public_key : string option; 317 + mutable sas_bytes : string option; 318 + mutable methods : sas_method list; 319 + } 320 + 321 + (** Generate a random flow ID *) 322 + let generate_flow_id () = 323 + let random_bytes = Mirage_crypto_rng.generate 16 in 324 + Base64.encode_string ~pad:false random_bytes 325 + 326 + (** Create a new SAS verification session *) 327 + let create_sas_session ~our_user_id ~our_device_id ~their_user_id ~their_device_id = 328 + let flow_id = generate_flow_id () in 329 + { 330 + flow_id; 331 + state = Sas_created; 332 + our_user_id; 333 + our_device_id; 334 + their_user_id; 335 + their_device_id; 336 + their_public_key = None; 337 + our_public_key = None; 338 + sas_bytes = None; 339 + methods = [Decimal_method; Emoji_method]; 340 + } 341 + 342 + (** Standard SAS emoji table (simplified - first 20) *) 343 + let sas_emoji_table = [| 344 + (0, "Dog"); 345 + (1, "Cat"); 346 + (2, "Lion"); 347 + (3, "Horse"); 348 + (4, "Unicorn"); 349 + (5, "Pig"); 350 + (6, "Elephant"); 351 + (7, "Rabbit"); 352 + (8, "Panda"); 353 + (9, "Rooster"); 354 + (10, "Penguin"); 355 + (11, "Turtle"); 356 + (12, "Fish"); 357 + (13, "Octopus"); 358 + (14, "Butterfly"); 359 + (15, "Flower"); 360 + (16, "Tree"); 361 + (17, "Cactus"); 362 + (18, "Mushroom"); 363 + (19, "Globe"); 364 + (* ... more emojis in full implementation *) 365 + |] 366 + 367 + (** Derive SAS output from shared bytes *) 368 + let derive_sas_output ~method_type ~sas_bytes = 369 + match method_type with 370 + | Decimal_method -> 371 + (* Extract 5 bytes and compute 3 numbers *) 372 + if String.length sas_bytes < 5 then 373 + Decimal (0, 0, 0) 374 + else 375 + let b0 = Char.code sas_bytes.[0] in 376 + let b1 = Char.code sas_bytes.[1] in 377 + let b2 = Char.code sas_bytes.[2] in 378 + let b3 = Char.code sas_bytes.[3] in 379 + let b4 = Char.code sas_bytes.[4] in 380 + let n1 = ((b0 lsl 5) lor (b1 lsr 3)) + 1000 in 381 + let n2 = (((b1 land 0x07) lsl 10) lor (b2 lsl 2) lor (b3 lsr 6)) + 1000 in 382 + let n3 = (((b3 land 0x3F) lsl 7) lor (b4 lsr 1)) + 1000 in 383 + Decimal (n1, n2, n3) 384 + | Emoji_method -> 385 + (* Extract 6 bytes for 7 emoji indices *) 386 + if String.length sas_bytes < 6 then 387 + Emoji [] 388 + else 389 + let indices = [ 390 + (Char.code sas_bytes.[0] lsr 2) land 0x3F; 391 + ((Char.code sas_bytes.[0] land 0x03) lsl 4) lor ((Char.code sas_bytes.[1] lsr 4) land 0x0F); 392 + ((Char.code sas_bytes.[1] land 0x0F) lsl 2) lor ((Char.code sas_bytes.[2] lsr 6) land 0x03); 393 + Char.code sas_bytes.[2] land 0x3F; 394 + (Char.code sas_bytes.[3] lsr 2) land 0x3F; 395 + ((Char.code sas_bytes.[3] land 0x03) lsl 4) lor ((Char.code sas_bytes.[4] lsr 4) land 0x0F); 396 + ((Char.code sas_bytes.[4] land 0x0F) lsl 2) lor ((Char.code sas_bytes.[5] lsr 6) land 0x03); 397 + ] in 398 + Emoji (List.map (fun i -> 399 + let idx = i mod (Array.length sas_emoji_table) in 400 + sas_emoji_table.(idx) 401 + ) indices) 402 + 403 + (** Get SAS output for display *) 404 + let get_sas_output session method_type = 405 + match session.sas_bytes with 406 + | None -> None 407 + | Some sas_bytes -> Some (derive_sas_output ~method_type ~sas_bytes) 408 + 409 + (** Confirm SAS match *) 410 + let confirm_sas session = 411 + session.state <- Sas_confirmed 412 + 413 + (** Cancel SAS verification *) 414 + let cancel_sas session reason = 415 + session.state <- Sas_cancelled reason 416 + 417 + (** Check if SAS is complete *) 418 + let is_sas_done session = 419 + session.state = Sas_done 420 + 421 + (** {1 QR Code Verification} *) 422 + 423 + (** QR verification mode *) 424 + type qr_mode = 425 + | Self_verifying_master_key_trusts_device 426 + | Self_verifying_device_trusts_master_key 427 + | Verifying_another_user 428 + 429 + (** QR verification state *) 430 + type qr_state = 431 + | Qr_started 432 + | Qr_scanned 433 + | Qr_confirmed 434 + | Qr_reciprocated 435 + | Qr_done 436 + | Qr_cancelled of string 437 + 438 + (** QR verification data *) 439 + type qr_verification = { 440 + flow_id : string; 441 + mutable state : qr_state; 442 + mode : qr_mode; 443 + our_user_id : Matrix_proto.Id.User_id.t; 444 + their_user_id : Matrix_proto.Id.User_id.t; 445 + our_master_key : string; 446 + their_master_key : string option; 447 + mutable secret : string option; 448 + } 449 + 450 + (** Create QR verification for self-verification *) 451 + let create_self_qr_verification ~our_user_id ~our_master_key ~mode = 452 + let flow_id = generate_flow_id () in 453 + let secret = Mirage_crypto_rng.generate 32 |> Base64.encode_string in 454 + { 455 + flow_id; 456 + state = Qr_started; 457 + mode; 458 + our_user_id; 459 + their_user_id = our_user_id; 460 + our_master_key; 461 + their_master_key = Some our_master_key; 462 + secret = Some secret; 463 + } 464 + 465 + (** Create QR verification for verifying another user *) 466 + let create_user_qr_verification ~our_user_id ~their_user_id ~our_master_key ~their_master_key = 467 + let flow_id = generate_flow_id () in 468 + let secret = Mirage_crypto_rng.generate 32 |> Base64.encode_string in 469 + { 470 + flow_id; 471 + state = Qr_started; 472 + mode = Verifying_another_user; 473 + our_user_id; 474 + their_user_id; 475 + our_master_key; 476 + their_master_key = Some their_master_key; 477 + secret = Some secret; 478 + } 479 + 480 + (** {1 Verification Request} *) 481 + 482 + (** Verification request *) 483 + type verification_request = { 484 + flow_id : string; 485 + from_user_id : Matrix_proto.Id.User_id.t; 486 + to_user_id : Matrix_proto.Id.User_id.t; 487 + from_device_id : Matrix_proto.Id.Device_id.t option; 488 + methods : string list; 489 + timestamp : int64; 490 + mutable accepted : bool; 491 + mutable cancelled : bool; 492 + } 493 + 494 + (** Create a verification request *) 495 + let create_verification_request ~from_user_id ~to_user_id ?from_device_id () = 496 + let flow_id = generate_flow_id () in 497 + { 498 + flow_id; 499 + from_user_id; 500 + to_user_id; 501 + from_device_id; 502 + methods = ["m.sas.v1"; "m.qr_code.show.v1"; "m.qr_code.scan.v1"]; 503 + timestamp = Int64.of_float (Unix.gettimeofday () *. 1000.0); 504 + accepted = false; 505 + cancelled = false; 506 + } 507 + 508 + (** Accept a verification request *) 509 + let accept_verification_request request = 510 + request.accepted <- true 511 + 512 + (** Cancel a verification request *) 513 + let cancel_verification_request request = 514 + request.cancelled <- true 515 + 516 + (** {1 Cross-Signing Upload} *) 517 + 518 + (** Data needed to upload cross-signing keys *) 519 + type cross_signing_upload = { 520 + master_key : cross_signing_pubkey; 521 + self_signing_key : cross_signing_pubkey; 522 + user_signing_key : cross_signing_pubkey; 523 + } 524 + 525 + (** Build upload data from private identity *) 526 + let build_cross_signing_upload (identity : private_cross_signing_identity) = 527 + match identity.master_key, identity.self_signing_key, identity.user_signing_key with 528 + | Some master, Some self_signing, Some user_signing -> 529 + let user_id = identity.user_id in 530 + Some { 531 + master_key = pubkey_from_private ~user_id ~usage:Master master; 532 + self_signing_key = pubkey_from_private ~user_id ~usage:Self_signing self_signing; 533 + user_signing_key = pubkey_from_private ~user_id ~usage:User_signing user_signing; 534 + } 535 + | _ -> None
+66
lib/matrix_eio/account.ml
··· 1 + (** Eio-idiomatic account operations. *) 2 + 3 + (** Get account data. 4 + @raise Eio.Io on failure *) 5 + let get_account_data client ~event_type = 6 + Error.unwrap (Matrix_client.Account.get_account_data (Client.base client) ~event_type) 7 + 8 + (** Set account data. 9 + @raise Eio.Io on failure *) 10 + let set_account_data client ~event_type ~content = 11 + Error.unwrap (Matrix_client.Account.set_account_data (Client.base client) 12 + ~event_type ~content) 13 + 14 + (** Get room-specific account data. 15 + @raise Eio.Io on failure *) 16 + let get_room_account_data client ~room_id ~event_type = 17 + Error.unwrap (Matrix_client.Account.get_room_account_data (Client.base client) 18 + ~room_id ~event_type) 19 + 20 + (** Set room-specific account data. 21 + @raise Eio.Io on failure *) 22 + let set_room_account_data client ~room_id ~event_type ~content = 23 + Error.unwrap (Matrix_client.Account.set_room_account_data (Client.base client) 24 + ~room_id ~event_type ~content) 25 + 26 + (** Third-party identifier *) 27 + type threepid = Matrix_client.Account.threepid = { 28 + medium : string; 29 + address : string; 30 + validated_at : int64; 31 + added_at : int64; 32 + } 33 + 34 + (** Get the user's third-party identifiers (email, phone). 35 + @raise Eio.Io on failure *) 36 + let get_3pids client = 37 + Error.unwrap (Matrix_client.Account.get_3pids (Client.base client)) 38 + 39 + (** Change the user's password. 40 + Note: This may require UIAA (User-Interactive Authentication). 41 + @raise Eio.Io on failure or if UIAA is required *) 42 + let change_password client ~new_password ?logout_devices () = 43 + Error.unwrap (Matrix_client.Account.change_password (Client.base client) 44 + ~new_password ?logout_devices ()) 45 + 46 + (** Deactivate the account. 47 + Warning: This is irreversible! 48 + Note: This may require UIAA (User-Interactive Authentication). 49 + @raise Eio.Io on failure or if UIAA is required *) 50 + let deactivate client ?erase () = 51 + Error.unwrap (Matrix_client.Account.deactivate (Client.base client) ?erase ()) 52 + 53 + (** Get the list of ignored users. 54 + @raise Eio.Io on failure *) 55 + let get_ignored_users client = 56 + Error.unwrap (Matrix_client.Account.get_ignored_users (Client.base client)) 57 + 58 + (** Ignore a user. 59 + @raise Eio.Io on failure *) 60 + let ignore_user client ~user_id = 61 + Error.unwrap (Matrix_client.Account.ignore_user (Client.base client) ~user_id) 62 + 63 + (** Unignore a user. 64 + @raise Eio.Io on failure *) 65 + let unignore_user client ~user_id = 66 + Error.unwrap (Matrix_client.Account.unignore_user (Client.base client) ~user_id)
+98
lib/matrix_eio/auth.ml
··· 1 + (** Eio-idiomatic authentication operations. 2 + 3 + All functions raise [Eio.Io] exceptions on error instead of 4 + returning Result types. *) 5 + 6 + (** Login parameters *) 7 + type login_params = Matrix_client.Auth.login_params = { 8 + device_id : string option; 9 + initial_device_display_name : string option; 10 + } 11 + 12 + let default_login_params = Matrix_client.Auth.default_login_params 13 + 14 + (** Login flow types *) 15 + type login_flow = Matrix_client.Auth.login_flow = 16 + | Password 17 + | Token 18 + | Sso 19 + | Unknown of string 20 + 21 + (** Get available login flows from the server. 22 + @raise Eio.Io on network or protocol error *) 23 + let get_login_flows client = 24 + Error.unwrap (Matrix_client.Auth.get_login_flows (Client.base client)) 25 + 26 + (** Login with username and password. 27 + 28 + Returns the updated client with the session attached. 29 + 30 + @param user The username (localpart or full user ID) 31 + @param password The password 32 + @param params Optional login parameters 33 + @raise Eio.Io on authentication failure or network error *) 34 + let login_password client ~user ~password ?(params = default_login_params) () = 35 + let session = Error.unwrap 36 + (Matrix_client.Auth.login_password (Client.base client) ~user ~password ~params ()) 37 + in 38 + Client.with_session client session 39 + 40 + (** Login with a token. 41 + 42 + Returns the updated client with the session attached. 43 + 44 + @param token The login token 45 + @param params Optional login parameters 46 + @raise Eio.Io on authentication failure or network error *) 47 + let login_token client ~token ?(params = default_login_params) () = 48 + let session = Error.unwrap 49 + (Matrix_client.Auth.login_token (Client.base client) ~token ~params ()) 50 + in 51 + Client.with_session client session 52 + 53 + (** Refresh the access token using a refresh token. 54 + 55 + Returns the new access token and optional new refresh token. 56 + 57 + @param refresh_token The refresh token from login 58 + @raise Eio.Io on failure *) 59 + let refresh_token client ~refresh_token = 60 + Error.unwrap (Matrix_client.Auth.refresh_token (Client.base client) ~refresh_token) 61 + 62 + (** Logout the current session. 63 + @raise Eio.Io on failure *) 64 + let logout client = 65 + Error.unwrap (Matrix_client.Auth.logout (Client.base client)) 66 + 67 + (** Logout all sessions for this user. 68 + @raise Eio.Io on failure *) 69 + let logout_all client = 70 + Error.unwrap (Matrix_client.Auth.logout_all (Client.base client)) 71 + 72 + (** Registration kind *) 73 + type registration_kind = Matrix_client.Auth.registration_kind = 74 + | User 75 + | Guest 76 + 77 + (** Register a new account. 78 + 79 + Returns the updated client with the session attached (unless inhibit_login is true). 80 + 81 + @param kind User or Guest registration 82 + @param username Optional username 83 + @param password Optional password 84 + @param device_id Optional device ID 85 + @param initial_device_display_name Optional display name for this device 86 + @param inhibit_login If true, don't return an access token 87 + @raise Eio.Io on registration failure *) 88 + let register client ?kind ?username ?password ?device_id ?initial_device_display_name ?inhibit_login () = 89 + let session = Error.unwrap 90 + (Matrix_client.Auth.register (Client.base client) ?kind ?username ?password 91 + ?device_id ?initial_device_display_name ?inhibit_login ()) 92 + in 93 + Client.with_session client session 94 + 95 + (** Get information about the current user. 96 + @raise Eio.Io on failure *) 97 + let whoami client = 98 + Error.unwrap (Matrix_client.Auth.whoami (Client.base client))
+132
lib/matrix_eio/backup.ml
··· 1 + (** Eio-idiomatic key backup operations. 2 + 3 + This module provides Eio wrappers for server-side key backup 4 + and recovery operations. *) 5 + 6 + (** Re-export backup types *) 7 + type signature_state = Matrix_client.Backup.signature_state = 8 + | Missing 9 + | Invalid 10 + | Valid_but_not_trusted 11 + | Valid_and_trusted 12 + 13 + type signature_verification = Matrix_client.Backup.signature_verification = { 14 + device_signature : signature_state; 15 + user_identity_signature : signature_state; 16 + other_signatures : (string * signature_state) list; 17 + } 18 + 19 + type backup_state = Matrix_client.Backup.backup_state = 20 + | Disabled 21 + | Creating 22 + | Enabling 23 + | Resuming 24 + | Enabled 25 + | Downloading 26 + | Disabling 27 + 28 + type encrypted_session_data = Matrix_client.Backup.encrypted_session_data = { 29 + ephemeral : string; 30 + ciphertext : string; 31 + mac : string; 32 + } 33 + 34 + type key_backup_data = Matrix_client.Backup.key_backup_data = { 35 + first_message_index : int; 36 + forwarded_count : int; 37 + is_verified : bool; 38 + session_data : encrypted_session_data; 39 + } 40 + 41 + (** {1 Backup Machine} *) 42 + 43 + (** Create a new backup machine *) 44 + let create = Matrix_client.Backup.create 45 + 46 + (** Check if backup is enabled *) 47 + let is_enabled = Matrix_client.Backup.is_enabled 48 + 49 + (** Get backup version *) 50 + let backup_version = Matrix_client.Backup.backup_version 51 + 52 + (** {1 Key Generation} *) 53 + 54 + (** Generate a new backup key pair *) 55 + let generate_backup_key = Matrix_client.Backup.generate_backup_key 56 + 57 + (** Create encryption key from decryption key *) 58 + let encryption_key_of_decryption_key = Matrix_client.Backup.encryption_key_of_decryption_key 59 + 60 + (** Create encryption key from base64 public key *) 61 + let encryption_key_of_base64 public_key = 62 + match Matrix_client.Backup.encryption_key_of_base64 public_key with 63 + | Ok key -> key 64 + | Error msg -> raise (Eio.Exn.create (Error.E (Error.Json msg))) 65 + 66 + (** {1 Backup Setup} *) 67 + 68 + (** Enable backup with a new key *) 69 + let enable_with_new_key = Matrix_client.Backup.enable_with_new_key 70 + 71 + (** Enable backup with an existing decryption key *) 72 + let enable_with_key = Matrix_client.Backup.enable_with_key 73 + 74 + (** Enable backup with only an encryption key (upload-only mode) *) 75 + let enable_upload_only = Matrix_client.Backup.enable_upload_only 76 + 77 + (** Set the backup version after creating *) 78 + let set_backup_version = Matrix_client.Backup.set_backup_version 79 + 80 + (** Disable backup *) 81 + let disable = Matrix_client.Backup.disable 82 + 83 + (** {1 Session Management} *) 84 + 85 + (** Mark a session as needing backup *) 86 + let mark_session_for_backup = Matrix_client.Backup.mark_session_for_backup 87 + 88 + (** Get number of pending sessions *) 89 + let pending_count = Matrix_client.Backup.pending_count 90 + 91 + (** Check if a session has been backed up *) 92 + let is_session_backed_up = Matrix_client.Backup.is_session_backed_up 93 + 94 + (** Mark a session as backed up *) 95 + let mark_session_backed_up = Matrix_client.Backup.mark_session_backed_up 96 + 97 + (** {1 Encryption/Decryption} *) 98 + 99 + (** Encrypt a room key for backup *) 100 + let encrypt_room_key encryption_key ~session_key ~session_id ~room_id ~sender_key = 101 + match Matrix_client.Backup.encrypt_room_key encryption_key 102 + ~session_key ~session_id ~room_id ~sender_key with 103 + | Ok data -> data 104 + | Error msg -> raise (Eio.Exn.create (Error.E (Error.Network msg))) 105 + 106 + (** Decrypt a room key from backup *) 107 + let decrypt_room_key decryption_key session_data = 108 + match Matrix_client.Backup.decrypt_room_key decryption_key session_data with 109 + | Ok data -> data 110 + | Error msg -> raise (Eio.Exn.create (Error.E (Error.Network msg))) 111 + 112 + (** {1 Recovery Key} *) 113 + 114 + (** Encode a backup decryption key as a human-readable recovery key *) 115 + let encode_recovery_key key = 116 + match Matrix_client.Backup.encode_recovery_key key with 117 + | Ok recovery_key -> recovery_key 118 + | Error msg -> raise (Eio.Exn.create (Error.E (Error.Json msg))) 119 + 120 + (** Decode a recovery key to a backup decryption key *) 121 + let decode_recovery_key recovery_key = 122 + match Matrix_client.Backup.decode_recovery_key recovery_key with 123 + | Ok key -> key 124 + | Error msg -> raise (Eio.Exn.create (Error.E (Error.Json msg))) 125 + 126 + (** {1 API Helpers} *) 127 + 128 + (** Create backup version request body for upload *) 129 + let create_version_request_body t = 130 + match Matrix_client.Backup.create_version_request_body t with 131 + | Ok body -> body 132 + | Error msg -> raise (Eio.Exn.create (Error.E (Error.Json msg)))
+69
lib/matrix_eio/client.ml
··· 1 + (** Eio-idiomatic Matrix client. 2 + 3 + This module wraps the base matrix_client with Eio idioms: 4 + - Uses switches for resource management 5 + - Raises Eio.Io exceptions instead of returning Results 6 + - Provides cancellation support via Eio switches 7 + - Supports structured concurrency *) 8 + 9 + type config = { 10 + homeserver : Uri.t; 11 + user_agent : string option; 12 + } 13 + 14 + type t = { 15 + base : Matrix_client.Client.t; 16 + sw : Eio.Switch.t; 17 + env : Eio_unix.Stdenv.base; 18 + } 19 + 20 + (** Create a new Matrix client. 21 + 22 + The client is bound to the provided switch and will be cleaned up 23 + when the switch completes. 24 + 25 + @param sw The Eio switch for resource management 26 + @param env The Eio environment 27 + @param homeserver The Matrix homeserver URL 28 + @param user_agent Optional user agent string *) 29 + let create ~sw ~env ~homeserver ?user_agent () = 30 + let config : Matrix_client.Client.config = { homeserver; user_agent } in 31 + let base = Matrix_client.Client.create ~sw ~config env in 32 + { base; sw; env } 33 + 34 + (** Get the underlying base client *) 35 + let base t = t.base 36 + 37 + (** Get the Eio switch *) 38 + let switch t = t.sw 39 + 40 + (** Get the homeserver URL *) 41 + let homeserver t = Matrix_client.Client.homeserver t.base 42 + 43 + (** Check if the client is logged in *) 44 + let is_logged_in t = Matrix_client.Client.is_logged_in t.base 45 + 46 + (** Get the current user ID (raises if not logged in) *) 47 + let user_id t = 48 + match Matrix_client.Client.user_id t.base with 49 + | Some id -> id 50 + | None -> raise (Eio.Exn.create (Error.E Error.Not_logged_in)) 51 + 52 + (** Get the current device ID (raises if not logged in) *) 53 + let device_id t = 54 + match Matrix_client.Client.device_id t.base with 55 + | Some id -> id 56 + | None -> raise (Eio.Exn.create (Error.E Error.Not_logged_in)) 57 + 58 + (** Get the access token (raises if not logged in) *) 59 + let access_token t = 60 + match Matrix_client.Client.access_token t.base with 61 + | Some token -> token 62 + | None -> raise (Eio.Exn.create (Error.E Error.Not_logged_in)) 63 + 64 + (** Get the session if logged in *) 65 + let session t = Matrix_client.Client.session t.base 66 + 67 + (** Update the client with a new session *) 68 + let with_session t session = 69 + { t with base = Matrix_client.Client.with_session t.base session }
+37
lib/matrix_eio/devices.ml
··· 1 + (** Eio-idiomatic device management operations. *) 2 + 3 + (** Device information *) 4 + type device = Matrix_client.Devices.device = { 5 + device_id : string; 6 + display_name : string option; 7 + last_seen_ip : string option; 8 + last_seen_ts : int64 option; 9 + } 10 + 11 + (** Get all devices for the current user. 12 + @raise Eio.Io on failure *) 13 + let get_devices client = 14 + Error.unwrap (Matrix_client.Devices.get_devices (Client.base client)) 15 + 16 + (** Get information about a specific device. 17 + @raise Eio.Io on failure *) 18 + let get_device client ~device_id = 19 + Error.unwrap (Matrix_client.Devices.get_device (Client.base client) ~device_id) 20 + 21 + (** Update a device's display name. 22 + @raise Eio.Io on failure *) 23 + let update_device client ~device_id ~display_name = 24 + Error.unwrap (Matrix_client.Devices.update_device (Client.base client) 25 + ~device_id ~display_name) 26 + 27 + (** Delete a device. 28 + Note: This may require UIAA (User-Interactive Authentication). 29 + @raise Eio.Io on failure or if UIAA is required *) 30 + let delete_device client ~device_id = 31 + Error.unwrap (Matrix_client.Devices.delete_device (Client.base client) ~device_id) 32 + 33 + (** Delete multiple devices. 34 + Note: This may require UIAA (User-Interactive Authentication). 35 + @raise Eio.Io on failure or if UIAA is required *) 36 + let delete_devices client ~device_ids = 37 + Error.unwrap (Matrix_client.Devices.delete_devices (Client.base client) ~device_ids)
+57
lib/matrix_eio/directory.ml
··· 1 + (** Eio-idiomatic room directory operations. *) 2 + 3 + (** Room alias information *) 4 + type alias_info = Matrix_client.Directory.alias_info = { 5 + room_id : Matrix_proto.Id.Room_id.t; 6 + servers : string list; 7 + } 8 + 9 + (** Resolve a room alias to a room ID. 10 + @raise Eio.Io on failure *) 11 + let resolve_alias client ~alias = 12 + Error.unwrap (Matrix_client.Directory.resolve_alias (Client.base client) ~alias) 13 + 14 + (** Create a room alias. 15 + @raise Eio.Io on failure *) 16 + let create_alias client ~alias ~room_id = 17 + Error.unwrap (Matrix_client.Directory.create_alias (Client.base client) 18 + ~alias ~room_id) 19 + 20 + (** Delete a room alias. 21 + @raise Eio.Io on failure *) 22 + let delete_alias client ~alias = 23 + Error.unwrap (Matrix_client.Directory.delete_alias (Client.base client) ~alias) 24 + 25 + (** Room visibility *) 26 + type visibility = Matrix_client.Directory.visibility 27 + 28 + (** Get room visibility in the directory. 29 + @raise Eio.Io on failure *) 30 + let get_visibility client ~room_id = 31 + Error.unwrap (Matrix_client.Directory.get_visibility (Client.base client) ~room_id) 32 + 33 + (** Set room visibility in the directory. 34 + @raise Eio.Io on failure *) 35 + let set_visibility client ~room_id ~visibility = 36 + Error.unwrap (Matrix_client.Directory.set_visibility (Client.base client) 37 + ~room_id ~visibility) 38 + 39 + (** Search filter *) 40 + type search_filter = Matrix_client.Directory.search_filter = { 41 + generic_search_term : string option; 42 + room_types : string list option; 43 + } 44 + 45 + (** Search result *) 46 + type search_result = Matrix_client.Directory.search_result = { 47 + chunk : Matrix_client.Rooms.public_room list; 48 + next_batch : string option; 49 + prev_batch : string option; 50 + total_room_count_estimate : int option; 51 + } 52 + 53 + (** Search public rooms. 54 + @raise Eio.Io on failure *) 55 + let search client ?server ?limit ?since ?filter () = 56 + Error.unwrap (Matrix_client.Directory.search (Client.base client) 57 + ?server ?limit ?since ?filter ())
+8
lib/matrix_eio/dune
··· 1 + (library 2 + (name matrix_eio) 3 + (public_name matrix_eio) 4 + (libraries 5 + matrix_client 6 + matrix_proto 7 + eio 8 + uri))
+71
lib/matrix_eio/error.ml
··· 1 + (** Eio-idiomatic errors for Matrix operations. 2 + 3 + This module provides error types that follow Eio conventions, 4 + using [Eio.Io] for IO-related errors and exceptions for 5 + exceptional conditions. *) 6 + 7 + (** The source type for Matrix IO errors *) 8 + type err = 9 + | Network of string (** Network-level error *) 10 + | Http of { status : int; body : string } (** HTTP error response *) 11 + | Json of string (** JSON encoding/decoding error *) 12 + | Matrix of { 13 + errcode : Matrix_client.Error.errcode; 14 + error : string; 15 + retry_after_ms : int option; 16 + } (** Matrix protocol error *) 17 + | Not_logged_in (** Operation requires authentication *) 18 + | Cancelled (** Operation was cancelled *) 19 + 20 + type Eio.Exn.err += E of err 21 + 22 + let err e = Eio.Exn.create (E e) 23 + 24 + (** Pretty-print an error source *) 25 + let pp_err fmt = function 26 + | Network msg -> Format.fprintf fmt "Network error: %s" msg 27 + | Http { status; body } -> Format.fprintf fmt "HTTP %d: %s" status body 28 + | Json msg -> Format.fprintf fmt "JSON error: %s" msg 29 + | Matrix { errcode; error; _ } -> 30 + Format.fprintf fmt "Matrix error %s: %s" 31 + (Matrix_client.Error.errcode_to_string errcode) error 32 + | Not_logged_in -> Format.fprintf fmt "Not logged in" 33 + | Cancelled -> Format.fprintf fmt "Operation cancelled" 34 + 35 + let () = Eio.Exn.register_pp (fun fmt -> function 36 + | E e -> pp_err fmt e; true 37 + | _ -> false) 38 + 39 + (** Convert a matrix_client error to an Eio error *) 40 + let of_client_error = function 41 + | Matrix_client.Error.Matrix_error e -> 42 + Matrix { 43 + errcode = e.errcode; 44 + error = e.error; 45 + retry_after_ms = e.retry_after_ms; 46 + } 47 + | Matrix_client.Error.Network_error msg -> Network msg 48 + | Matrix_client.Error.Json_error msg -> Json msg 49 + | Matrix_client.Error.Http_error { status; body } -> Http { status; body } 50 + 51 + (** Raise an Eio.Io exception from a matrix_client error *) 52 + let raise_client_error e = 53 + raise (Eio.Exn.create (E (of_client_error e))) 54 + 55 + (** Convert Result-based client operations to Eio-style exceptions *) 56 + let unwrap = function 57 + | Ok v -> v 58 + | Error e -> raise_client_error e 59 + 60 + (** Check if an error is retryable *) 61 + let is_retryable = function 62 + | Network _ -> true 63 + | Http { status; _ } -> status >= 500 || status = 429 64 + | Matrix { errcode; _ } -> errcode = Matrix_client.Error.M_LIMIT_EXCEEDED 65 + | Json _ | Not_logged_in | Cancelled -> false 66 + 67 + (** Get retry delay in seconds from an error, if available *) 68 + let retry_after = function 69 + | Matrix { retry_after_ms = Some ms; _ } -> Some (float_of_int ms /. 1000.0) 70 + | Http { status = 429; _ } -> Some 1.0 (* Default retry after *) 71 + | _ -> None
+99
lib/matrix_eio/keys.ml
··· 1 + (** Eio-idiomatic E2EE key management operations. *) 2 + 3 + (** Re-export key types from matrix_client *) 4 + type ed25519_keypair = Matrix_client.Keys.ed25519_keypair 5 + type curve25519_keypair = Matrix_client.Keys.curve25519_keypair 6 + type device_keys = Matrix_client.Keys.device_keys 7 + type one_time_key = Matrix_client.Keys.one_time_key 8 + type fallback_key = Matrix_client.Keys.fallback_key 9 + 10 + (** Generate an Ed25519 keypair for signing. *) 11 + let generate_ed25519 = Matrix_client.Keys.generate_ed25519 12 + 13 + (** Generate a Curve25519 keypair for key exchange. *) 14 + let generate_curve25519 = Matrix_client.Keys.generate_curve25519 15 + 16 + (** Generate a batch of one-time keys. 17 + @param count Number of keys to generate 18 + @param sign_with Optional Ed25519 key to sign the one-time keys *) 19 + let generate_one_time_keys = Matrix_client.Keys.generate_one_time_keys 20 + 21 + (** Base64 encoding/decoding for keys *) 22 + let ed25519_pub_to_base64 = Matrix_client.Keys.ed25519_pub_to_base64 23 + let ed25519_priv_to_base64 = Matrix_client.Keys.ed25519_priv_to_base64 24 + let curve25519_pub_to_base64 = Matrix_client.Keys.curve25519_pub_to_base64 25 + let curve25519_secret_to_base64 = Matrix_client.Keys.curve25519_secret_to_base64 26 + 27 + (** Parse keys from base64 *) 28 + let ed25519_pub_of_base64 s = 29 + match Matrix_client.Keys.ed25519_pub_of_base64 s with 30 + | Ok k -> k 31 + | Error msg -> invalid_arg msg 32 + 33 + let ed25519_priv_of_base64 s = 34 + match Matrix_client.Keys.ed25519_priv_of_base64 s with 35 + | Ok k -> k 36 + | Error msg -> invalid_arg msg 37 + 38 + let curve25519_pub_of_base64 s = 39 + match Matrix_client.Keys.curve25519_pub_of_base64 s with 40 + | Ok k -> k 41 + | Error msg -> invalid_arg msg 42 + 43 + let curve25519_secret_of_base64 s = 44 + match Matrix_client.Keys.curve25519_secret_of_base64 s with 45 + | Ok k -> k 46 + | Error msg -> invalid_arg msg 47 + 48 + (** Sign JSON with an Ed25519 key. *) 49 + let sign_json = Matrix_client.Keys.sign_json 50 + 51 + (** Verify a signature on JSON. *) 52 + let verify_signature = Matrix_client.Keys.verify_signature 53 + 54 + (** Perform a Curve25519 key exchange. *) 55 + let key_exchange ~secret ~their_public = 56 + match Matrix_client.Keys.key_exchange ~secret ~their_public with 57 + | Ok shared -> shared 58 + | Error msg -> invalid_arg msg 59 + 60 + (** Create device keys for upload. *) 61 + let create_device_keys ~user_id ~device_id ~ed25519_keypair ~curve25519_keypair = 62 + match Matrix_client.Keys.create_device_keys 63 + ~user_id ~device_id ~ed25519_keypair ~curve25519_keypair with 64 + | Ok keys -> keys 65 + | Error msg -> invalid_arg msg 66 + 67 + (** Upload keys response *) 68 + type upload_keys_response = Matrix_client.Keys.upload_keys_response 69 + 70 + (** Upload device keys and/or one-time keys. 71 + @raise Eio.Io on failure *) 72 + let upload_keys client ?device_keys ?(one_time_keys=[]) ?(fallback_keys=[]) () = 73 + Error.unwrap (Matrix_client.Keys.upload_keys (Client.base client) 74 + ?device_keys ~one_time_keys ~fallback_keys ()) 75 + 76 + (** Query keys response *) 77 + type query_keys_response = Matrix_client.Keys.query_keys_response 78 + type queried_device_keys = Matrix_client.Keys.queried_device_keys 79 + 80 + (** Query device keys for users. 81 + @raise Eio.Io on failure *) 82 + let query_keys client ?timeout ~users () = 83 + Error.unwrap (Matrix_client.Keys.query_keys (Client.base client) ?timeout ~users ()) 84 + 85 + (** Claim keys response *) 86 + type claim_keys_response = Matrix_client.Keys.claim_keys_response 87 + 88 + (** Claim one-time keys for establishing Olm sessions. 89 + @raise Eio.Io on failure *) 90 + let claim_keys client ?timeout ~keys () = 91 + Error.unwrap (Matrix_client.Keys.claim_keys (Client.base client) ?timeout ~keys ()) 92 + 93 + (** Key changes response *) 94 + type key_changes_response = Matrix_client.Keys.key_changes_response 95 + 96 + (** Get key changes since a sync token. 97 + @raise Eio.Io on failure *) 98 + let get_key_changes client ~from ~to_ = 99 + Error.unwrap (Matrix_client.Keys.get_key_changes (Client.base client) ~from ~to_)
+193
lib/matrix_eio/matrix_eio.ml
··· 1 + (** Eio-idiomatic Matrix Client SDK. 2 + 3 + This library provides a Matrix client implementation using Eio idioms: 4 + 5 + {b Structured Concurrency}: All operations respect Eio switches for 6 + proper resource cleanup and cancellation. 7 + 8 + {b Exception-based Errors}: Operations raise [Eio.Io] exceptions instead 9 + of returning Result types, making code more readable and allowing 10 + natural error propagation. 11 + 12 + {b Fibre-based Sync}: The sync loop runs in a dedicated fibre that can 13 + be cancelled via the controlling switch. 14 + 15 + {2 Quick Start} 16 + 17 + {[ 18 + Eio_main.run (fun env -> 19 + Eio.Switch.run (fun sw -> 20 + (* Create client *) 21 + let client = Matrix_eio.Client.create ~sw ~env 22 + ~homeserver:(Uri.of_string "https://matrix.org") () in 23 + 24 + (* Login *) 25 + let client = Matrix_eio.Auth.login_password client 26 + ~user:"username" ~password:"password" () in 27 + 28 + (* Send a message *) 29 + Matrix_eio.Messages.send_text client 30 + ~room_id:(Matrix_proto.Id.Room_id.of_string_exn "!room:server") 31 + ~body:"Hello, Matrix!" (); 32 + 33 + (* Sync with callbacks *) 34 + Matrix_eio.Sync.sync_forever ~sw ~clock:(Eio.Stdenv.clock env) 35 + client 36 + ~on_sync:(fun response -> 37 + (* Handle sync response *) 38 + Matrix_eio.Sync.Continue) 39 + () 40 + )) 41 + ]} 42 + 43 + {2 Error Handling} 44 + 45 + All operations can raise [Eio.Io] exceptions. Use [try...with] or 46 + [Eio.Switch.run] for proper error handling: 47 + 48 + {[ 49 + try 50 + Matrix_eio.Messages.send_text client ~room_id ~body:"test" () 51 + with Eio.Io (Error.E err, _) -> 52 + match err with 53 + | Error.Network msg -> Printf.printf "Network error: %s\n" msg 54 + | Error.Matrix { errcode; error; _ } -> 55 + Printf.printf "Matrix error %s: %s\n" 56 + (Matrix_client.Error.errcode_to_string errcode) error 57 + | _ -> Printf.printf "Other error\n" 58 + ]} 59 + 60 + {2 Cancellation} 61 + 62 + Operations can be cancelled by releasing the switch: 63 + 64 + {[ 65 + let cancelled = ref false in 66 + Eio.Fiber.both 67 + (fun () -> 68 + Eio.Switch.run (fun sw -> 69 + Matrix_eio.Sync.sync_forever ~sw ~clock client 70 + ~on_sync:(fun _ -> Continue) ())) 71 + (fun () -> 72 + Eio.Time.sleep clock 60.0; 73 + (* Sync will be cancelled when the switch releases *) 74 + cancelled := true) 75 + ]} 76 + 77 + {1 Modules} 78 + 79 + {2 Core} 80 + 81 + - {!module:Error} - Error types and handling 82 + - {!module:Client} - Client creation and session management 83 + - {!module:Auth} - Authentication (login, logout, registration) 84 + - {!module:Sync} - Sync loop with Eio patterns 85 + 86 + {2 Room Operations} 87 + 88 + - {!module:Rooms} - Room creation, joining, leaving 89 + - {!module:Messages} - Sending and receiving messages 90 + - {!module:State} - Room state management 91 + - {!module:Relations} - Reactions, edits, threads, replies 92 + 93 + {2 User Features} 94 + 95 + - {!module:Profile} - User profile management 96 + - {!module:Presence} - Online/offline status 97 + - {!module:Typing} - Typing indicators 98 + - {!module:Receipts} - Read receipts 99 + - {!module:Account} - Account settings 100 + 101 + {2 Media & Devices} 102 + 103 + - {!module:Media} - Upload/download media 104 + - {!module:Devices} - Device management 105 + 106 + {2 Discovery} 107 + 108 + - {!module:Directory} - Room directory 109 + 110 + {2 End-to-End Encryption} 111 + 112 + - {!module:Keys} - Key management for E2EE *) 113 + 114 + (** {1 Core} *) 115 + 116 + module Error = Error 117 + module Client = Client 118 + module Auth = Auth 119 + module Sync = Sync 120 + 121 + (** {1 Rooms} *) 122 + 123 + module Rooms = Rooms 124 + module Messages = Messages 125 + module State = State 126 + module Relations = Relations 127 + 128 + (** {1 User Features} *) 129 + 130 + module Profile = Profile 131 + module Typing = Typing 132 + module Receipts = Receipts 133 + module Account = Account 134 + module Presence = Presence 135 + 136 + (** {1 Media & Devices} *) 137 + 138 + module Media = Media 139 + module Devices = Devices 140 + 141 + (** {1 Discovery} *) 142 + 143 + module Directory = Directory 144 + 145 + (** {1 End-to-End Encryption} *) 146 + 147 + module Keys = Keys 148 + module Verification = Verification 149 + module Backup = Backup 150 + 151 + (** {1 Offline Support} *) 152 + 153 + module Send_queue = Send_queue 154 + 155 + (** {1 Convenience Functions} *) 156 + 157 + (** Create a Matrix client connected to a homeserver. 158 + 159 + This is a convenience function that wraps {!Client.create}. 160 + 161 + @param sw The Eio switch for resource management 162 + @param env The Eio environment 163 + @param homeserver The Matrix homeserver URL 164 + @param user_agent Optional user agent string *) 165 + let connect ~sw ~env ~homeserver ?user_agent () = 166 + Client.create ~sw ~env ~homeserver ?user_agent () 167 + 168 + (** Login to a Matrix server with password authentication. 169 + 170 + This is a convenience function that creates a client and logs in. 171 + 172 + @param sw The Eio switch 173 + @param env The Eio environment 174 + @param homeserver The Matrix homeserver URL 175 + @param user The username 176 + @param password The password 177 + @return The logged-in client *) 178 + let login_password ~sw ~env ~homeserver ~user ~password () = 179 + let client = connect ~sw ~env ~homeserver () in 180 + Auth.login_password client ~user ~password () 181 + 182 + (** Run a sync loop with a callback. 183 + 184 + This is a convenience function for common sync patterns. 185 + 186 + @param sw The Eio switch 187 + @param env The Eio environment (provides clock) 188 + @param client The logged-in client 189 + @param on_sync Callback for each sync response 190 + @param on_error Optional error handler *) 191 + let run_sync ~sw ~env client ~on_sync ?on_error () = 192 + let clock = Eio.Stdenv.clock env in 193 + Sync.sync_forever ~sw ~clock client ~on_sync ?on_error ()
+64
lib/matrix_eio/media.ml
··· 1 + (** Eio-idiomatic media operations. 2 + 3 + Note: Many media operations require direct HTTP access for binary data. 4 + These functions may return errors indicating that full implementation 5 + requires using the Requests library directly. *) 6 + 7 + (** Upload media content. 8 + 9 + Note: This requires binary upload which may not be fully implemented. 10 + 11 + @param content_type MIME type of the content 12 + @param filename Optional filename 13 + @param data The media content as string 14 + @return The mxc:// URI for the uploaded content 15 + @raise Eio.Io on failure *) 16 + let upload client ~content_type ~data ?filename () = 17 + Error.unwrap (Matrix_client.Media.upload (Client.base client) 18 + ~content_type ~data ?filename ()) 19 + 20 + (** Download media content. 21 + 22 + Note: This requires binary download which may not be fully implemented. 23 + 24 + @param server_name The server hosting the content 25 + @param media_id The media ID 26 + @return The media content as string 27 + @raise Eio.Io on failure *) 28 + let download client ~server_name ~media_id = 29 + Error.unwrap (Matrix_client.Media.download (Client.base client) 30 + ~server_name ~media_id) 31 + 32 + (** Download a thumbnail of media. 33 + 34 + Note: This requires binary download which may not be fully implemented. 35 + 36 + @param server_name The server hosting the content 37 + @param media_id The media ID 38 + @param width Desired width 39 + @param height Desired height 40 + @param method_ Optional resize method ("crop" or "scale") 41 + @return The thumbnail content as string 42 + @raise Eio.Io on failure *) 43 + let thumbnail client ~server_name ~media_id ~width ~height ?method_ () = 44 + Error.unwrap (Matrix_client.Media.thumbnail (Client.base client) 45 + ~server_name ~media_id ~width ~height ?method_ ()) 46 + 47 + (** Parse an mxc:// URI. 48 + @return (server_name, media_id) tuple option *) 49 + let parse_mxc = Matrix_client.Media.parse_mxc 50 + 51 + (** Convert mxc:// to HTTP URL. 52 + @return HTTP URL option *) 53 + let mxc_to_http client ~mxc ?width ?height () = 54 + Matrix_client.Media.mxc_to_http (Client.base client) ~mxc ?width ?height () 55 + 56 + (** Media configuration from server *) 57 + type config = Matrix_client.Media.config = { 58 + upload_size : int option; 59 + } 60 + 61 + (** Get media upload configuration. 62 + @raise Eio.Io on failure *) 63 + let get_config client = 64 + Error.unwrap (Matrix_client.Media.get_config (Client.base client))
+96
lib/matrix_eio/messages.ml
··· 1 + (** Eio-idiomatic message operations. 2 + 3 + All functions raise [Eio.Io] exceptions on error instead of 4 + returning Result types. *) 5 + 6 + (** Send a text message to a room. 7 + 8 + @param room_id The room to send to 9 + @param body The message text 10 + @param format Optional format (e.g., "org.matrix.custom.html") 11 + @param formatted_body Optional HTML-formatted body 12 + @return The event ID of the sent message 13 + @raise Eio.Io on failure *) 14 + let send_text client ~room_id ~body ?format ?formatted_body () = 15 + Error.unwrap (Matrix_client.Messages.send_text (Client.base client) 16 + ~room_id ~body ?format ?formatted_body ()) 17 + 18 + (** Send an emote message (/me action). 19 + @raise Eio.Io on failure *) 20 + let send_emote client ~room_id ~body () = 21 + Error.unwrap (Matrix_client.Messages.send_emote (Client.base client) 22 + ~room_id ~body ()) 23 + 24 + (** Send a notice message (bot message). 25 + @raise Eio.Io on failure *) 26 + let send_notice client ~room_id ~body () = 27 + Error.unwrap (Matrix_client.Messages.send_notice (Client.base client) 28 + ~room_id ~body ()) 29 + 30 + (** Send an image message. 31 + @raise Eio.Io on failure *) 32 + let send_image client ~room_id ~body ~url ?info () = 33 + Error.unwrap (Matrix_client.Messages.send_image (Client.base client) 34 + ~room_id ~body ~url ?info ()) 35 + 36 + (** Send a file message. 37 + @raise Eio.Io on failure *) 38 + let send_file client ~room_id ~body ~url ?info () = 39 + Error.unwrap (Matrix_client.Messages.send_file (Client.base client) 40 + ~room_id ~body ~url ?info ()) 41 + 42 + (** Redact (delete) an event. 43 + @raise Eio.Io on failure *) 44 + let redact client ~room_id ~event_id ?reason () = 45 + Error.unwrap (Matrix_client.Messages.redact (Client.base client) 46 + ~room_id ~event_id ?reason ()) 47 + 48 + (** Pagination direction *) 49 + type direction = Matrix_client.Messages.direction = Forward | Backward 50 + 51 + (** Messages response from pagination *) 52 + type messages_response = Matrix_client.Messages.messages_response = { 53 + start : string; 54 + end_ : string option; 55 + chunk : Matrix_proto.Event.Raw_event.t list; 56 + state : Matrix_proto.Event.Raw_event.t list; 57 + } 58 + 59 + (** Get messages from a room with pagination. 60 + 61 + @param room_id The room ID 62 + @param from Pagination token to start from 63 + @param dir Direction (Forward or Backward) 64 + @param limit Maximum number of events to return 65 + @param filter Optional filter JSON string 66 + @return Messages response with events and pagination tokens 67 + @raise Eio.Io on failure *) 68 + let get_messages client ~room_id ~from ~dir ?limit ?filter () = 69 + Error.unwrap (Matrix_client.Messages.get_messages (Client.base client) 70 + ~room_id ~from ~dir ?limit ?filter ()) 71 + 72 + (** Event context result *) 73 + type context = Matrix_client.Messages.context = { 74 + start : string; 75 + end_ : string; 76 + event : Matrix_proto.Event.Raw_event.t; 77 + events_before : Matrix_proto.Event.Raw_event.t list; 78 + events_after : Matrix_proto.Event.Raw_event.t list; 79 + state : Matrix_proto.Event.Raw_event.t list; 80 + } 81 + 82 + (** Get context around an event. 83 + 84 + @param room_id The room ID 85 + @param event_id The event to get context for 86 + @param limit Number of events before/after to include 87 + @return Context with events before and after 88 + @raise Eio.Io on failure *) 89 + let get_context client ~room_id ~event_id ?limit () = 90 + Error.unwrap (Matrix_client.Messages.get_context (Client.base client) 91 + ~room_id ~event_id ?limit ()) 92 + 93 + (** Get a single event by ID. 94 + @raise Eio.Io on failure *) 95 + let get_event client ~room_id ~event_id = 96 + Error.unwrap (Matrix_client.Messages.get_event (Client.base client) ~room_id ~event_id)
+41
lib/matrix_eio/presence.ml
··· 1 + (** Eio-idiomatic presence operations. *) 2 + 3 + (** Presence state *) 4 + type presence_state = Matrix_client.Presence.presence_state = 5 + | Online 6 + | Offline 7 + | Unavailable 8 + 9 + (** Presence status *) 10 + type presence = Matrix_client.Presence.presence = { 11 + presence : presence_state; 12 + status_msg : string option; 13 + last_active_ago : int option; 14 + currently_active : bool option; 15 + } 16 + 17 + (** Get a user's presence status. 18 + @raise Eio.Io on failure *) 19 + let get_presence client ~user_id = 20 + Error.unwrap (Matrix_client.Presence.get_presence (Client.base client) ~user_id) 21 + 22 + (** Set the current user's presence status. 23 + @raise Eio.Io on failure *) 24 + let set_presence client ~presence ?status_msg () = 25 + Error.unwrap (Matrix_client.Presence.set_presence (Client.base client) 26 + ~presence ?status_msg ()) 27 + 28 + (** Set the current user as online. 29 + @raise Eio.Io on failure *) 30 + let set_online client ?status_msg () = 31 + set_presence client ~presence:Online ?status_msg () 32 + 33 + (** Set the current user as offline. 34 + @raise Eio.Io on failure *) 35 + let set_offline client () = 36 + set_presence client ~presence:Offline () 37 + 38 + (** Set the current user as unavailable/away. 39 + @raise Eio.Io on failure *) 40 + let set_unavailable client ?status_msg () = 41 + set_presence client ~presence:Unavailable ?status_msg ()
+35
lib/matrix_eio/profile.ml
··· 1 + (** Eio-idiomatic profile operations. 2 + 3 + All functions raise [Eio.Io] exceptions on error instead of 4 + returning Result types. *) 5 + 6 + (** Profile data *) 7 + type profile = Matrix_client.Profile.profile = { 8 + displayname : string option; 9 + avatar_url : string option; 10 + } 11 + 12 + (** Get a user's profile. 13 + @raise Eio.Io on failure *) 14 + let get_profile client ~user_id = 15 + Error.unwrap (Matrix_client.Profile.get_profile (Client.base client) ~user_id) 16 + 17 + (** Get a user's display name. 18 + @raise Eio.Io on failure *) 19 + let get_displayname client ~user_id = 20 + Error.unwrap (Matrix_client.Profile.get_displayname (Client.base client) ~user_id) 21 + 22 + (** Set the current user's display name. 23 + @raise Eio.Io on failure *) 24 + let set_displayname client ~displayname = 25 + Error.unwrap (Matrix_client.Profile.set_displayname (Client.base client) ~displayname) 26 + 27 + (** Get a user's avatar URL. 28 + @raise Eio.Io on failure *) 29 + let get_avatar_url client ~user_id = 30 + Error.unwrap (Matrix_client.Profile.get_avatar_url (Client.base client) ~user_id) 31 + 32 + (** Set the current user's avatar URL. 33 + @raise Eio.Io on failure *) 34 + let set_avatar_url client ~avatar_url = 35 + Error.unwrap (Matrix_client.Profile.set_avatar_url (Client.base client) ~avatar_url)
+33
lib/matrix_eio/receipts.ml
··· 1 + (** Eio-idiomatic receipt operations. *) 2 + 3 + (** Send a read receipt. 4 + 5 + @param room_id The room containing the event 6 + @param event_id The event to mark as read 7 + @param receipt_type Receipt type (default "m.read", also "m.read.private") 8 + @raise Eio.Io on failure *) 9 + let send_receipt client ~room_id ~event_id ?receipt_type () = 10 + Error.unwrap (Matrix_client.Receipts.send_receipt (Client.base client) 11 + ~room_id ~event_id ?receipt_type ()) 12 + 13 + (** Mark a room as read up to an event. 14 + Convenience function that sends a public read receipt. 15 + @raise Eio.Io on failure *) 16 + let mark_read client ~room_id ~event_id = 17 + send_receipt client ~room_id ~event_id ~receipt_type:"m.read" () 18 + 19 + (** Mark a room as read privately. 20 + The read status is not visible to other users. 21 + @raise Eio.Io on failure *) 22 + let mark_read_private client ~room_id ~event_id = 23 + send_receipt client ~room_id ~event_id ~receipt_type:"m.read.private" () 24 + 25 + (** Set the fully read marker for a room. 26 + 27 + @param room_id The room to update 28 + @param fully_read The event to mark as fully read 29 + @param read Optional event to also mark as read 30 + @raise Eio.Io on failure *) 31 + let set_read_marker client ~room_id ~fully_read ?read () = 32 + Error.unwrap (Matrix_client.Receipts.set_read_marker (Client.base client) 33 + ~room_id ~fully_read ?read ())
+85
lib/matrix_eio/relations.ml
··· 1 + (** Eio-idiomatic relation operations (reactions, edits, threads, replies). *) 2 + 3 + (** Send a reaction to an event. 4 + 5 + @param room_id The room containing the event 6 + @param event_id The event to react to 7 + @param key The reaction key (usually an emoji) 8 + @return The reaction event ID 9 + @raise Eio.Io on failure *) 10 + let send_reaction client ~room_id ~event_id ~key = 11 + Error.unwrap (Matrix_client.Relations.send_reaction (Client.base client) 12 + ~room_id ~event_id ~key) 13 + 14 + (** Send an edit to a message. 15 + 16 + @param room_id The room containing the event 17 + @param event_id The event to edit 18 + @param new_body The new message body 19 + @return The edit event ID 20 + @raise Eio.Io on failure *) 21 + let edit_message client ~room_id ~event_id ~new_body = 22 + Error.unwrap (Matrix_client.Relations.edit_message (Client.base client) 23 + ~room_id ~event_id ~new_body) 24 + 25 + (** Send a reply to a message. 26 + 27 + @param room_id The room containing the event 28 + @param event_id The event to reply to 29 + @param body The reply body 30 + @return The reply event ID 31 + @raise Eio.Io on failure *) 32 + let send_reply client ~room_id ~event_id ~body = 33 + Error.unwrap (Matrix_client.Relations.send_reply (Client.base client) 34 + ~room_id ~event_id ~body) 35 + 36 + (** Send a message in a thread. 37 + 38 + @param room_id The room containing the thread 39 + @param thread_root_id The root event of the thread 40 + @param body The message body 41 + @param reply_to_id Optional event to reply to within the thread 42 + @return The message event ID 43 + @raise Eio.Io on failure *) 44 + let send_in_thread client ~room_id ~thread_root_id ?reply_to_id ~body () = 45 + Error.unwrap (Matrix_client.Relations.send_in_thread (Client.base client) 46 + ~room_id ~thread_root_id ?reply_to_id ~body ()) 47 + 48 + (** Relation type *) 49 + type relation_type = Matrix_client.Relations.relation_type = 50 + | Annotation (** m.annotation - reactions *) 51 + | Reference (** m.reference - generic reference *) 52 + | Replace (** m.replace - edits *) 53 + | Thread (** m.thread - threads *) 54 + 55 + (** Aggregation result *) 56 + type aggregation = Matrix_client.Relations.aggregation = { 57 + event_id : Matrix_proto.Id.Event_id.t; 58 + origin_server_ts : int64; 59 + sender : Matrix_proto.Id.User_id.t; 60 + } 61 + 62 + (** Relations response *) 63 + type relations_response = Matrix_client.Relations.relations_response = { 64 + chunk : aggregation list; 65 + next_batch : string option; 66 + prev_batch : string option; 67 + } 68 + 69 + (** Get events related to a given event. 70 + 71 + @param room_id The room containing the event 72 + @param event_id The event to get relations for 73 + @param rel_type Optional relation type filter 74 + @param event_type Optional event type filter 75 + @param limit Maximum number of events 76 + @param from Pagination token 77 + @raise Eio.Io on failure *) 78 + let get_relations client ~room_id ~event_id ?rel_type ?event_type ?limit ?from () = 79 + Error.unwrap (Matrix_client.Relations.get_relations (Client.base client) 80 + ~room_id ~event_id ?rel_type ?event_type ?limit ?from ()) 81 + 82 + (** Get reactions for an event. 83 + @raise Eio.Io on failure *) 84 + let get_reactions client ~room_id ~event_id = 85 + Error.unwrap (Matrix_client.Relations.get_reactions (Client.base client) ~room_id ~event_id)
+140
lib/matrix_eio/rooms.ml
··· 1 + (** Eio-idiomatic room operations. 2 + 3 + All functions raise [Eio.Io] exceptions on error instead of 4 + returning Result types. *) 5 + 6 + (** Room visibility *) 7 + type visibility = Matrix_client.Rooms.visibility 8 + 9 + (** Room creation preset *) 10 + type preset = Matrix_client.Rooms.preset = 11 + | Private_chat 12 + | Public_chat 13 + | Trusted_private_chat 14 + 15 + (** Create a new room. 16 + 17 + @param name Optional room name 18 + @param topic Optional room topic 19 + @param preset Room preset (affects default permissions) 20 + @param is_direct Mark as a direct message room 21 + @param invite List of user IDs to invite 22 + @param room_alias_local_part Local part of a room alias 23 + @param visibility Public or Private 24 + @param room_type Optional room type (e.g., "m.space") 25 + @return The new room ID 26 + @raise Eio.Io on failure *) 27 + let create client ?name ?topic ?visibility ?preset ?room_alias_local_part ?invite ?is_direct ?room_type () = 28 + Error.unwrap (Matrix_client.Rooms.create (Client.base client) 29 + ?name ?topic ?visibility ?preset ?room_alias_local_part ?invite ?is_direct ?room_type ()) 30 + 31 + (** Join a room by ID or alias. 32 + @raise Eio.Io on failure *) 33 + let join client ~room_id_or_alias ?via ?reason () = 34 + Error.unwrap (Matrix_client.Rooms.join (Client.base client) ~room_id_or_alias ?via ?reason ()) 35 + 36 + (** Leave a room. 37 + @raise Eio.Io on failure *) 38 + let leave client ~room_id ?reason () = 39 + Error.unwrap (Matrix_client.Rooms.leave (Client.base client) ~room_id ?reason ()) 40 + 41 + (** Forget a room (remove from room list). 42 + @raise Eio.Io on failure *) 43 + let forget client ~room_id = 44 + Error.unwrap (Matrix_client.Rooms.forget (Client.base client) ~room_id) 45 + 46 + (** Invite a user to a room. 47 + @raise Eio.Io on failure *) 48 + let invite client ~room_id ~user_id ?reason () = 49 + Error.unwrap (Matrix_client.Rooms.invite (Client.base client) ~room_id ~user_id ?reason ()) 50 + 51 + (** Kick a user from a room. 52 + @raise Eio.Io on failure *) 53 + let kick client ~room_id ~user_id ?reason () = 54 + Error.unwrap (Matrix_client.Rooms.kick (Client.base client) ~room_id ~user_id ?reason ()) 55 + 56 + (** Ban a user from a room. 57 + @raise Eio.Io on failure *) 58 + let ban client ~room_id ~user_id ?reason () = 59 + Error.unwrap (Matrix_client.Rooms.ban (Client.base client) ~room_id ~user_id ?reason ()) 60 + 61 + (** Unban a user from a room. 62 + @raise Eio.Io on failure *) 63 + let unban client ~room_id ~user_id ?reason () = 64 + Error.unwrap (Matrix_client.Rooms.unban (Client.base client) ~room_id ~user_id ?reason ()) 65 + 66 + (** Get the list of joined rooms. 67 + @raise Eio.Io on failure *) 68 + let get_joined_rooms client = 69 + Error.unwrap (Matrix_client.Rooms.get_joined_rooms (Client.base client)) 70 + 71 + (** Member info *) 72 + type member = Matrix_client.Rooms.member = { 73 + user_id : Matrix_proto.Id.User_id.t; 74 + display_name : string option; 75 + avatar_url : string option; 76 + membership : string; 77 + } 78 + 79 + (** Get members of a room. 80 + @raise Eio.Io on failure *) 81 + let get_members client ~room_id ?membership ?not_membership () = 82 + Error.unwrap (Matrix_client.Rooms.get_members (Client.base client) ~room_id ?membership ?not_membership ()) 83 + 84 + (** Public room info *) 85 + type public_room = Matrix_client.Rooms.public_room = { 86 + room_id : Matrix_proto.Id.Room_id.t; 87 + name : string option; 88 + topic : string option; 89 + num_joined_members : int; 90 + world_readable : bool; 91 + guest_can_join : bool; 92 + avatar_url : string option; 93 + canonical_alias : string option; 94 + } 95 + 96 + (** Public rooms response *) 97 + type public_rooms_response = Matrix_client.Rooms.public_rooms_response = { 98 + chunk : public_room list; 99 + next_batch : string option; 100 + prev_batch : string option; 101 + total_room_count_estimate : int option; 102 + } 103 + 104 + (** Get public rooms. 105 + @raise Eio.Io on failure *) 106 + let get_public_rooms client ?limit ?since ?server () = 107 + Error.unwrap (Matrix_client.Rooms.get_public_rooms (Client.base client) ?limit ?since ?server ()) 108 + 109 + (** Power levels type *) 110 + type power_levels = Matrix_client.Rooms.power_levels = { 111 + ban : int; 112 + events : (string * int) list; 113 + events_default : int; 114 + invite : int; 115 + kick : int; 116 + redact : int; 117 + state_default : int; 118 + users : (string * int) list; 119 + users_default : int; 120 + notifications : (string * int) list; 121 + } 122 + 123 + (** Get power levels for a room. 124 + @raise Eio.Io on failure *) 125 + let get_power_levels client ~room_id = 126 + Error.unwrap (Matrix_client.Rooms.get_power_levels (Client.base client) ~room_id) 127 + 128 + (** Set power levels for a room. 129 + @raise Eio.Io on failure *) 130 + let set_power_levels client ~room_id ~power_levels = 131 + Error.unwrap (Matrix_client.Rooms.set_power_levels (Client.base client) ~room_id ~power_levels) 132 + 133 + (** Get a user's power level from power levels state. *) 134 + let get_user_power_level = Matrix_client.Rooms.get_user_power_level 135 + 136 + (** Set a user's power level in a room. 137 + @raise Eio.Io on failure *) 138 + let set_user_power_level client ~room_id ~user_id ~level = 139 + Error.unwrap (Matrix_client.Rooms.set_user_power_level (Client.base client) 140 + ~room_id ~user_id ~level)
+242
lib/matrix_eio/send_queue.ml
··· 1 + (** Eio-idiomatic send queue for offline message queueing. 2 + 3 + This module provides Eio wrappers for the send queue with 4 + fibre-based processing support. *) 5 + 6 + (** Re-export send queue types *) 7 + type request_kind = Matrix_client.Send_queue.request_kind = 8 + | Event of { 9 + event_type : string; 10 + content : Jsont.json; 11 + txn_id : string; 12 + } 13 + | MediaUpload of { 14 + content_type : string; 15 + data_size : int; 16 + local_path : string option; 17 + txn_id : string; 18 + } 19 + | Reaction of { 20 + relates_to : Matrix_proto.Id.Event_id.t; 21 + key : string; 22 + txn_id : string; 23 + } 24 + | Redaction of { 25 + event_id : Matrix_proto.Id.Event_id.t; 26 + reason : string option; 27 + txn_id : string; 28 + } 29 + 30 + type request_state = Matrix_client.Send_queue.request_state = 31 + | Pending 32 + | Sending 33 + | Sent 34 + | Failed of string 35 + | Cancelled 36 + 37 + type send_result = Matrix_client.Send_queue.send_result = 38 + | Sent_ok of { event_id : Matrix_proto.Id.Event_id.t option } 39 + | Send_failed of { error : string; retryable : bool } 40 + | Send_cancelled 41 + 42 + type send_handle = Matrix_client.Send_queue.send_handle 43 + type room_send_queue = Matrix_client.Send_queue.room_send_queue 44 + type t = Matrix_client.Send_queue.t 45 + 46 + (** {1 Queue Creation} *) 47 + 48 + (** Create a new global send queue manager *) 49 + let create = Matrix_client.Send_queue.create 50 + 51 + (** Get or create a room queue *) 52 + let get_room_queue = Matrix_client.Send_queue.get_room_queue 53 + 54 + (** Create a new room send queue *) 55 + let create_room_queue = Matrix_client.Send_queue.create_room_queue 56 + 57 + (** {1 Enqueueing Requests} *) 58 + 59 + (** Send a message event *) 60 + let send_message = Matrix_client.Send_queue.send_message 61 + 62 + (** Send a text message *) 63 + let send_text = Matrix_client.Send_queue.send_text 64 + 65 + (** Send a reaction *) 66 + let send_reaction = Matrix_client.Send_queue.send_reaction 67 + 68 + (** Send a redaction *) 69 + let send_redaction = Matrix_client.Send_queue.send_redaction 70 + 71 + (** Send media with dependent event *) 72 + let send_media = Matrix_client.Send_queue.send_media 73 + 74 + (** {1 Handle Operations} *) 75 + 76 + (** Cancel a queued request *) 77 + let cancel = Matrix_client.Send_queue.cancel 78 + 79 + (** Abort a request (cancel and remove) *) 80 + let abort = Matrix_client.Send_queue.abort 81 + 82 + (** Get request by handle *) 83 + let get_request = Matrix_client.Send_queue.get_request 84 + 85 + (** Check if request is still pending *) 86 + let is_pending = Matrix_client.Send_queue.is_pending 87 + 88 + (** Check if request was sent *) 89 + let is_sent = Matrix_client.Send_queue.is_sent 90 + 91 + (** {1 Queue Control} *) 92 + 93 + (** Enable/disable a room queue *) 94 + let set_room_enabled = Matrix_client.Send_queue.set_room_enabled 95 + 96 + (** Enable/disable all queues globally *) 97 + let set_enabled = Matrix_client.Send_queue.set_enabled 98 + 99 + (** Check if globally enabled *) 100 + let is_enabled = Matrix_client.Send_queue.is_enabled 101 + 102 + (** Check if a room queue is enabled *) 103 + let is_room_enabled = Matrix_client.Send_queue.is_room_enabled 104 + 105 + (** {1 Queue Statistics} *) 106 + 107 + (** Count of pending requests in a room queue *) 108 + let pending_count = Matrix_client.Send_queue.pending_count 109 + 110 + (** Count of all pending requests across all rooms *) 111 + let total_pending = Matrix_client.Send_queue.total_pending 112 + 113 + (** Get all pending requests for a room *) 114 + let pending_requests = Matrix_client.Send_queue.pending_requests 115 + 116 + (** Get all failed requests for a room *) 117 + let failed_requests = Matrix_client.Send_queue.failed_requests 118 + 119 + (** {1 Queue Processing} *) 120 + 121 + (** Get next sendable request from queue *) 122 + let next_sendable = Matrix_client.Send_queue.next_sendable 123 + 124 + (** Mark request as being sent *) 125 + let mark_sending = Matrix_client.Send_queue.mark_sending 126 + 127 + (** Mark request as successfully sent *) 128 + let mark_sent = Matrix_client.Send_queue.mark_sent 129 + 130 + (** Mark request as failed with optional retry *) 131 + let mark_failed = Matrix_client.Send_queue.mark_failed 132 + 133 + (** Remove completed/cancelled/failed requests *) 134 + let cleanup_queue = Matrix_client.Send_queue.cleanup_queue 135 + 136 + (** {1 Persistence} *) 137 + 138 + (** Get all pending requests for persistence *) 139 + let requests_to_persist = Matrix_client.Send_queue.requests_to_persist 140 + 141 + (** Restore requests from persistence *) 142 + let restore_requests = Matrix_client.Send_queue.restore_requests 143 + 144 + (** {1 Local Echo} *) 145 + 146 + (** Create a local echo event from a queued request *) 147 + let local_echo_event = Matrix_client.Send_queue.local_echo_event 148 + 149 + (** {1 Dependencies} *) 150 + 151 + (** Add a dependency between requests *) 152 + let add_dependency = Matrix_client.Send_queue.add_dependency 153 + 154 + (** {1 Callbacks} *) 155 + 156 + (** Set callback for state changes *) 157 + let on_state_change = Matrix_client.Send_queue.on_state_change 158 + 159 + (** Set global error callback *) 160 + let on_error = Matrix_client.Send_queue.on_error 161 + 162 + (** {1 Retry Logic} *) 163 + 164 + (** Calculate delay for next retry (exponential backoff) *) 165 + let retry_delay = Matrix_client.Send_queue.retry_delay 166 + 167 + (** Check if a request should be retried *) 168 + let should_retry = Matrix_client.Send_queue.should_retry 169 + 170 + (** {1 Eio-Specific Queue Processing} *) 171 + 172 + (** Process the send queue in a fibre. 173 + 174 + This function runs in a loop, processing queued requests and 175 + sending them to the server. It respects the Eio switch for 176 + cancellation. 177 + 178 + @param sw The Eio switch 179 + @param clock The Eio clock for retry delays 180 + @param send_fn The function to actually send a request *) 181 + let process_queue ~sw ~clock queue ~send_fn = 182 + let rec loop () = 183 + Eio.Switch.check sw; 184 + match next_sendable queue with 185 + | None -> 186 + (* Wait a bit before checking again *) 187 + Eio.Time.sleep clock 0.1; 188 + loop () 189 + | Some request -> 190 + mark_sending queue request; 191 + (try 192 + let result = send_fn request in 193 + (match result with 194 + | Sent_ok _ -> mark_sent queue request 195 + | Send_failed { error; retryable } -> 196 + mark_failed queue request error ~retryable 197 + | Send_cancelled -> 198 + Matrix_client.Send_queue.update_state queue request Cancelled) 199 + with exn -> 200 + let error = Printexc.to_string exn in 201 + mark_failed queue request error ~retryable:true); 202 + (* Small delay between sends *) 203 + Eio.Time.sleep clock 0.05; 204 + loop () 205 + in 206 + loop () 207 + 208 + (** Start processing the send queue in a background fibre. 209 + 210 + Returns immediately after spawning the processing fibre. 211 + 212 + @param sw The Eio switch 213 + @param clock The Eio clock 214 + @param send_fn The function to actually send a request *) 215 + let start_processing ~sw ~clock queue ~send_fn = 216 + Eio.Fiber.fork ~sw (fun () -> 217 + process_queue ~sw ~clock queue ~send_fn) 218 + 219 + (** Process pending requests once (non-blocking). 220 + 221 + Processes all currently sendable requests and returns. 222 + Useful for manual queue processing. *) 223 + let process_pending queue ~send_fn = 224 + let rec process () = 225 + match next_sendable queue with 226 + | None -> () 227 + | Some request -> 228 + mark_sending queue request; 229 + (try 230 + let result = send_fn request in 231 + (match result with 232 + | Sent_ok _ -> mark_sent queue request 233 + | Send_failed { error; retryable } -> 234 + mark_failed queue request error ~retryable 235 + | Send_cancelled -> 236 + Matrix_client.Send_queue.update_state queue request Cancelled) 237 + with exn -> 238 + let error = Printexc.to_string exn in 239 + mark_failed queue request error ~retryable:true); 240 + process () 241 + in 242 + process ()
+63
lib/matrix_eio/state.ml
··· 1 + (** Eio-idiomatic room state operations. 2 + 3 + All functions raise [Eio.Io] exceptions on error instead of 4 + returning Result types. *) 5 + 6 + (** Get a room state event. 7 + 8 + @param room_id The room ID 9 + @param event_type The state event type 10 + @param state_key The state key (empty string for events without a key) 11 + @return The state event content as JSON 12 + @raise Eio.Io on failure *) 13 + let get_state_event client ~room_id ~event_type ?state_key () = 14 + Error.unwrap (Matrix_client.State.get_state_event (Client.base client) 15 + ~room_id ~event_type ?state_key ()) 16 + 17 + (** Set a room state event. 18 + 19 + @param room_id The room ID 20 + @param event_type The state event type 21 + @param state_key The state key (empty string for events without a key) 22 + @param content The event content as JSON 23 + @return The event ID 24 + @raise Eio.Io on failure *) 25 + let set_state client ~room_id ~event_type ?state_key ~content () = 26 + Error.unwrap (Matrix_client.State.set_state (Client.base client) 27 + ~room_id ~event_type ?state_key ~content ()) 28 + 29 + (** Get all state events for a room. 30 + @return List of state events as JSON 31 + @raise Eio.Io on failure *) 32 + let get_state client ~room_id = 33 + Error.unwrap (Matrix_client.State.get_state (Client.base client) ~room_id) 34 + 35 + (** Get the room name. 36 + @raise Eio.Io on failure *) 37 + let get_name client ~room_id = 38 + Error.unwrap (Matrix_client.State.get_name (Client.base client) ~room_id) 39 + 40 + (** Set the room name. 41 + @raise Eio.Io on failure *) 42 + let set_name client ~room_id ~name = 43 + Error.unwrap (Matrix_client.State.set_name (Client.base client) ~room_id ~name) 44 + 45 + (** Get the room topic. 46 + @raise Eio.Io on failure *) 47 + let get_topic client ~room_id = 48 + Error.unwrap (Matrix_client.State.get_topic (Client.base client) ~room_id) 49 + 50 + (** Set the room topic. 51 + @raise Eio.Io on failure *) 52 + let set_topic client ~room_id ~topic = 53 + Error.unwrap (Matrix_client.State.set_topic (Client.base client) ~room_id ~topic) 54 + 55 + (** Get the room avatar URL. 56 + @raise Eio.Io on failure *) 57 + let get_avatar client ~room_id = 58 + Error.unwrap (Matrix_client.State.get_avatar (Client.base client) ~room_id) 59 + 60 + (** Set the room avatar URL. 61 + @raise Eio.Io on failure *) 62 + let set_avatar client ~room_id ~url = 63 + Error.unwrap (Matrix_client.State.set_avatar (Client.base client) ~room_id ~url)
+159
lib/matrix_eio/sync.ml
··· 1 + (** Eio-idiomatic sync operations with structured concurrency. 2 + 3 + This module provides sync functionality using Eio patterns: 4 + - Fibre-based sync loops with cancellation 5 + - Event streams using Eio.Stream 6 + - Proper resource cleanup via switches *) 7 + 8 + (** Sync parameters *) 9 + type params = Matrix_client.Sync.params = { 10 + filter : string option; 11 + since : string option; 12 + full_state : bool; 13 + set_presence : [ `Online | `Offline | `Unavailable ] option; 14 + timeout : int; 15 + } 16 + 17 + let default_params = Matrix_client.Sync.default_params 18 + 19 + (** Sync response type *) 20 + type response = Matrix_proto.Sync.Response.t 21 + 22 + (** Perform a single sync request. 23 + 24 + @param params Sync parameters 25 + @raise Eio.Io on network or protocol error *) 26 + let sync client ?(params = default_params) () = 27 + Error.unwrap (Matrix_client.Sync.sync (Client.base client) ~params ()) 28 + 29 + (** Event handler callback type *) 30 + type 'a handler = response -> 'a 31 + 32 + (** Sync loop control *) 33 + type control = 34 + | Continue (** Continue syncing *) 35 + | Stop (** Stop the sync loop *) 36 + | Retry_after of float (** Retry after given seconds *) 37 + 38 + (** Run a sync loop in a dedicated fibre. 39 + 40 + The sync loop runs until: 41 + - The handler returns [Stop] 42 + - The switch is cancelled 43 + - An unrecoverable error occurs 44 + 45 + @param sw The switch controlling this fibre 46 + @param clock The Eio clock for timing 47 + @param initial_since Optional initial sync token (for resuming) 48 + @param params Sync parameters 49 + @param on_sync Callback for each sync response 50 + @param on_error Callback for errors (returns control action) 51 + @raise Eio.Io on unrecoverable error *) 52 + let sync_forever ~sw ~clock client 53 + ?initial_since 54 + ?(params = default_params) 55 + ~on_sync 56 + ?(on_error = fun _ -> Retry_after 5.0) 57 + () = 58 + let rec loop since = 59 + (* Check if we should stop due to switch cancellation *) 60 + Eio.Fiber.check (); 61 + let params = { params with since } in 62 + match Matrix_client.Sync.sync (Client.base client) ~params () with 63 + | Error e -> 64 + (match on_error (Error.of_client_error e) with 65 + | Continue -> loop since 66 + | Stop -> () 67 + | Retry_after delay -> 68 + Eio.Time.sleep clock delay; 69 + loop since) 70 + | Ok response -> 71 + (match on_sync response with 72 + | Continue -> loop (Some response.next_batch) 73 + | Stop -> () 74 + | Retry_after delay -> 75 + Eio.Time.sleep clock delay; 76 + loop (Some response.next_batch)) 77 + in 78 + Eio.Fiber.fork ~sw (fun () -> loop initial_since) 79 + 80 + (** Run a sync loop that pushes events to a stream. 81 + 82 + This is useful for decoupling sync from event processing. 83 + Events are pushed to the stream as they arrive. 84 + 85 + @param sw The switch controlling this fibre 86 + @param clock The Eio clock for timing 87 + @param stream The stream to push events to 88 + @param initial_since Optional initial sync token 89 + @param params Sync parameters 90 + @param on_error Error handler (returns control action) *) 91 + let sync_to_stream ~sw ~clock client ~stream 92 + ?initial_since 93 + ?(params = default_params) 94 + ?(on_error = fun _ -> Retry_after 5.0) 95 + () = 96 + sync_forever ~sw ~clock client 97 + ?initial_since ~params 98 + ~on_sync:(fun response -> 99 + Eio.Stream.add stream response; 100 + Continue) 101 + ~on_error 102 + () 103 + 104 + (** Create a sync stream and start syncing. 105 + 106 + Returns a stream that yields sync responses. 107 + The sync loop runs in a background fibre. 108 + 109 + @param sw The switch controlling the sync fibre 110 + @param clock The Eio clock 111 + @param capacity Stream buffer capacity 112 + @param initial_since Optional initial sync token 113 + @param params Sync parameters *) 114 + let create_sync_stream ~sw ~clock client 115 + ?(capacity = 10) 116 + ?initial_since 117 + ?(params = default_params) 118 + () = 119 + let stream = Eio.Stream.create capacity in 120 + sync_to_stream ~sw ~clock client ~stream ?initial_since ~params (); 121 + stream 122 + 123 + (** Iterate over sync responses. 124 + 125 + Runs the sync loop and calls [f] for each response. 126 + Stops when [f] returns [Stop] or the switch is cancelled. 127 + 128 + @param sw The switch for cancellation 129 + @param clock The Eio clock 130 + @param initial_since Optional initial sync token 131 + @param params Sync parameters 132 + @param f Callback for each response *) 133 + let iter ~sw ~clock client ?initial_since ?(params = default_params) f = 134 + sync_forever ~sw ~clock client 135 + ?initial_since ~params 136 + ~on_sync:(fun response -> f response; Continue) 137 + () 138 + 139 + (** Filter types and creation *) 140 + type filter = Matrix_client.Sync.filter 141 + type room_filter = Matrix_client.Sync.room_filter 142 + type event_filter = Matrix_client.Sync.event_filter 143 + type room_event_filter = Matrix_client.Sync.room_event_filter 144 + 145 + let default_filter = Matrix_client.Sync.default_filter 146 + let default_room_filter = Matrix_client.Sync.default_room_filter 147 + let default_event_filter = Matrix_client.Sync.default_event_filter 148 + let default_room_event_filter = Matrix_client.Sync.default_room_event_filter 149 + 150 + (** Upload a filter to the server. 151 + @return The filter ID 152 + @raise Eio.Io on failure *) 153 + let create_filter client ~filter = 154 + Error.unwrap (Matrix_client.Sync.create_filter (Client.base client) ~filter) 155 + 156 + (** Get a previously uploaded filter. 157 + @raise Eio.Io on failure *) 158 + let get_filter client ~filter_id = 159 + Error.unwrap (Matrix_client.Sync.get_filter (Client.base client) ~filter_id)
+23
lib/matrix_eio/typing.ml
··· 1 + (** Eio-idiomatic typing indicator operations. *) 2 + 3 + (** Send a typing notification. 4 + 5 + @param room_id The room to send the typing indicator to 6 + @param typing Whether the user is typing 7 + @param timeout Typing timeout in milliseconds (default 30000) 8 + @raise Eio.Io on failure *) 9 + let set_typing client ~room_id ~typing ?timeout () = 10 + Error.unwrap (Matrix_client.Typing.set_typing (Client.base client) 11 + ~room_id ~typing ?timeout ()) 12 + 13 + (** Start typing in a room. 14 + Convenience function that sets typing to true. 15 + @raise Eio.Io on failure *) 16 + let start_typing client ~room_id ?timeout () = 17 + set_typing client ~room_id ~typing:true ?timeout () 18 + 19 + (** Stop typing in a room. 20 + Convenience function that sets typing to false. 21 + @raise Eio.Io on failure *) 22 + let stop_typing client ~room_id = 23 + set_typing client ~room_id ~typing:false ()
+127
lib/matrix_eio/verification.ml
··· 1 + (** Eio-idiomatic device verification operations. 2 + 3 + All verification functions are designed to work with Eio's 4 + structured concurrency and exception-based error handling. *) 5 + 6 + (** Re-export verification types *) 7 + type local_trust = Matrix_client.Verification.local_trust = 8 + | Verified 9 + | BlackListed 10 + | Ignored 11 + | Unset 12 + 13 + type key_usage = Matrix_client.Verification.key_usage = 14 + | Master 15 + | Self_signing 16 + | User_signing 17 + 18 + type sas_state = Matrix_client.Verification.sas_state = 19 + | Sas_created 20 + | Sas_started 21 + | Sas_accepted 22 + | Sas_keys_exchanged 23 + | Sas_confirmed 24 + | Sas_mac_received 25 + | Sas_done 26 + | Sas_cancelled of string 27 + 28 + type sas_output = Matrix_client.Verification.sas_output = 29 + | Decimal of int * int * int 30 + | Emoji of (int * string) list 31 + 32 + type sas_method = Matrix_client.Verification.sas_method = 33 + | Decimal_method 34 + | Emoji_method 35 + 36 + type qr_mode = Matrix_client.Verification.qr_mode = 37 + | Self_verifying_master_key_trusts_device 38 + | Self_verifying_device_trusts_master_key 39 + | Verifying_another_user 40 + 41 + (** {1 Cross-Signing Key Management} *) 42 + 43 + (** Generate new cross-signing keys for the user *) 44 + let generate_cross_signing_keys identity = 45 + Matrix_client.Verification.generate_cross_signing_keys identity 46 + 47 + (** Build upload data for cross-signing keys *) 48 + let build_cross_signing_upload identity = 49 + Matrix_client.Verification.build_cross_signing_upload identity 50 + 51 + (** Create a private cross-signing identity *) 52 + let create_private_identity = Matrix_client.Verification.create_private_identity 53 + 54 + (** {1 Device Trust} *) 55 + 56 + (** Check if a device is verified (locally or via cross-signing) *) 57 + let is_device_verified = Matrix_client.Verification.is_device_verified 58 + 59 + (** Set local trust state for a device *) 60 + let set_device_local_trust = Matrix_client.Verification.set_device_local_trust 61 + 62 + (** Create a verified device record *) 63 + let create_verified_device = Matrix_client.Verification.create_verified_device 64 + 65 + (** {1 User Identity} *) 66 + 67 + (** Check if own identity is verified *) 68 + let is_own_identity_verified = Matrix_client.Verification.is_own_identity_verified 69 + 70 + (** Check if other identity is verified by us *) 71 + let is_other_identity_verified = Matrix_client.Verification.is_other_identity_verified 72 + 73 + (** Check if user identity has changed *) 74 + let has_identity_changed = Matrix_client.Verification.has_identity_changed 75 + 76 + (** Pin the current master key for change detection *) 77 + let pin_master_key = Matrix_client.Verification.pin_master_key 78 + 79 + (** {1 SAS Verification} *) 80 + 81 + (** Create a new SAS verification session *) 82 + let create_sas_session = Matrix_client.Verification.create_sas_session 83 + 84 + (** Get SAS output for display to user *) 85 + let get_sas_output = Matrix_client.Verification.get_sas_output 86 + 87 + (** Confirm SAS match *) 88 + let confirm_sas = Matrix_client.Verification.confirm_sas 89 + 90 + (** Cancel SAS verification *) 91 + let cancel_sas = Matrix_client.Verification.cancel_sas 92 + 93 + (** Check if SAS verification is complete *) 94 + let is_sas_done = Matrix_client.Verification.is_sas_done 95 + 96 + (** {1 QR Code Verification} *) 97 + 98 + (** Create QR verification for self-verification *) 99 + let create_self_qr_verification = Matrix_client.Verification.create_self_qr_verification 100 + 101 + (** Create QR verification for verifying another user *) 102 + let create_user_qr_verification = Matrix_client.Verification.create_user_qr_verification 103 + 104 + (** {1 Verification Requests} *) 105 + 106 + (** Create a verification request *) 107 + let create_verification_request = Matrix_client.Verification.create_verification_request 108 + 109 + (** Accept a verification request *) 110 + let accept_verification_request = Matrix_client.Verification.accept_verification_request 111 + 112 + (** Cancel a verification request *) 113 + let cancel_verification_request = Matrix_client.Verification.cancel_verification_request 114 + 115 + (** {1 Signature Verification} *) 116 + 117 + (** Verify an Ed25519 signature *) 118 + let verify_signature = Matrix_client.Verification.verify_signature 119 + 120 + (** Verify cross-signing signature *) 121 + let verify_cross_signing_signature = Matrix_client.Verification.verify_cross_signing_signature 122 + 123 + (** Sign data with a private key *) 124 + let sign_with_key private_key data = 125 + match Matrix_client.Verification.sign_with_key private_key data with 126 + | Ok sig_ -> sig_ 127 + | Error msg -> raise (Eio.Exn.create (Error.E (Error.Network msg)))
+4
lib/matrix_proto/dune
··· 1 + (library 2 + (name matrix_proto) 3 + (public_name matrix_proto) 4 + (libraries jsont ptime))
+1623
lib/matrix_proto/matrix_event.ml
··· 1 + (** Matrix event types with JSON codecs. 2 + 3 + Events are the fundamental unit of data in Matrix. All communication 4 + in Matrix happens through events, which are JSON objects with a 5 + standardized structure. 6 + 7 + This module provides types and codecs for: 8 + - {b State Events}: Persistent room configuration (create, member, name, etc.) 9 + - {b Message Events}: Transient room messages (text, image, file, etc.) 10 + - {b Ephemeral Events}: Non-persistent data (typing indicators, receipts) 11 + - {b To-Device Events}: Direct device-to-device messages (for E2EE) 12 + - {b Account Data}: Per-user configuration (tags, read markers) 13 + 14 + All types include bidirectional JSON codecs using the [jsont] library, 15 + enabling both parsing and serialization. 16 + 17 + @see <https://spec.matrix.org/v1.11/client-server-api/#events> Events 18 + @see <https://spec.matrix.org/v1.11/client-server-api/#room-events> Room Events 19 + @see <https://spec.matrix.org/v1.11/client-server-api/#state-events> State Events *) 20 + 21 + open Matrix_id 22 + 23 + (** {1 Timestamps} 24 + 25 + Matrix uses millisecond timestamps since the Unix epoch (1970-01-01 00:00:00 UTC). 26 + These are used for event ordering and timing. 27 + 28 + @see <https://spec.matrix.org/v1.11/client-server-api/#events> Event timestamps *) 29 + 30 + module Timestamp = struct 31 + (** Server timestamps in milliseconds since Unix epoch. 32 + 33 + All Matrix events include an [origin_server_ts] field with the 34 + timestamp when the event was received by the originating server. *) 35 + type t = int64 36 + 37 + let of_ptime pt = 38 + let span = Ptime.to_span pt in 39 + let d, ps = Ptime.Span.to_d_ps span in 40 + let days_ms = Int64.mul (Int64.of_int d) 86_400_000L in 41 + let ps_ms = Int64.div ps 1_000_000_000L in 42 + Int64.add days_ms ps_ms 43 + 44 + let to_ptime_opt t = 45 + let days = Int64.div t 86_400_000L |> Int64.to_int in 46 + let rem_ms = Int64.rem t 86_400_000L in 47 + let ps = Int64.mul rem_ms 1_000_000_000L in 48 + Ptime.Span.of_d_ps (days, ps) |> Option.map Ptime.of_span |> Option.join 49 + 50 + let jsont = Jsont.int64 51 + end 52 + 53 + (** {1 Unsigned Event Data} *) 54 + 55 + module Unsigned = struct 56 + (** Unsigned data added by the homeserver. *) 57 + 58 + type t = { 59 + age : int64 option; 60 + prev_content : Jsont.json option; 61 + prev_sender : User_id.t option; 62 + redacted_because : Jsont.json option; 63 + transaction_id : Transaction_id.t option; 64 + } 65 + 66 + let empty = { 67 + age = None; 68 + prev_content = None; 69 + prev_sender = None; 70 + redacted_because = None; 71 + transaction_id = None; 72 + } 73 + 74 + let jsont = 75 + Jsont.Object.( 76 + map (fun age prev_content prev_sender redacted_because transaction_id -> 77 + { age; prev_content; prev_sender; redacted_because; transaction_id }) 78 + |> opt_mem "age" Jsont.int64 ~enc:(fun t -> t.age) 79 + |> opt_mem "prev_content" Jsont.json ~enc:(fun t -> t.prev_content) 80 + |> opt_mem "prev_sender" User_id.jsont ~enc:(fun t -> t.prev_sender) 81 + |> opt_mem "redacted_because" Jsont.json ~enc:(fun t -> t.redacted_because) 82 + |> opt_mem "transaction_id" Transaction_id.jsont ~enc:(fun t -> t.transaction_id) 83 + |> finish) 84 + end 85 + 86 + (** {1 Room Membership} *) 87 + 88 + module Membership = struct 89 + type t = 90 + | Join 91 + | Invite 92 + | Leave 93 + | Ban 94 + | Knock 95 + 96 + let to_string = function 97 + | Join -> "join" 98 + | Invite -> "invite" 99 + | Leave -> "leave" 100 + | Ban -> "ban" 101 + | Knock -> "knock" 102 + 103 + let of_string = function 104 + | "join" -> Ok Join 105 + | "invite" -> Ok Invite 106 + | "leave" -> Ok Leave 107 + | "ban" -> Ok Ban 108 + | "knock" -> Ok Knock 109 + | s -> Error (`Unknown_membership s) 110 + 111 + let jsont = 112 + Jsont.enum [ 113 + ("join", Join); 114 + ("invite", Invite); 115 + ("leave", Leave); 116 + ("ban", Ban); 117 + ("knock", Knock); 118 + ] 119 + end 120 + 121 + (** {1 Join Rules} *) 122 + 123 + module Join_rule = struct 124 + type t = 125 + | Public 126 + | Invite 127 + | Knock 128 + | Restricted 129 + | Knock_restricted 130 + | Private 131 + 132 + let jsont = 133 + Jsont.enum [ 134 + ("public", Public); 135 + ("invite", Invite); 136 + ("knock", Knock); 137 + ("restricted", Restricted); 138 + ("knock_restricted", Knock_restricted); 139 + ("private", Private); 140 + ] 141 + end 142 + 143 + (** {1 History Visibility} *) 144 + 145 + module History_visibility = struct 146 + type t = 147 + | Invited 148 + | Joined 149 + | Shared 150 + | World_readable 151 + 152 + let jsont = 153 + Jsont.enum [ 154 + ("invited", Invited); 155 + ("joined", Joined); 156 + ("shared", Shared); 157 + ("world_readable", World_readable); 158 + ] 159 + end 160 + 161 + (** {1 Room State Event Contents} *) 162 + 163 + module Room_create_content = struct 164 + type t = { 165 + creator : User_id.t option; (* Deprecated in v11, optional *) 166 + room_version : string option; 167 + predecessor : predecessor option; 168 + type_ : string option; (* m.space for spaces *) 169 + } 170 + and predecessor = { 171 + room_id : Room_id.t; 172 + event_id : Event_id.t; 173 + } 174 + 175 + let predecessor_jsont = 176 + Jsont.Object.( 177 + map (fun room_id event_id -> { room_id; event_id }) 178 + |> mem "room_id" Room_id.jsont ~enc:(fun p -> p.room_id) 179 + |> mem "event_id" Event_id.jsont ~enc:(fun p -> p.event_id) 180 + |> finish) 181 + 182 + let jsont = 183 + Jsont.Object.( 184 + map (fun creator room_version predecessor type_ -> 185 + { creator; room_version; predecessor; type_ }) 186 + |> opt_mem "creator" User_id.jsont ~enc:(fun t -> t.creator) 187 + |> opt_mem "room_version" Jsont.string ~enc:(fun t -> t.room_version) 188 + |> opt_mem "predecessor" predecessor_jsont ~enc:(fun t -> t.predecessor) 189 + |> opt_mem "type" Jsont.string ~enc:(fun t -> t.type_) 190 + |> finish) 191 + end 192 + 193 + module Room_name_content = struct 194 + type t = { name : string } 195 + 196 + let jsont = 197 + Jsont.Object.( 198 + map (fun name -> { name }) 199 + |> mem "name" Jsont.string ~enc:(fun t -> t.name) 200 + |> finish) 201 + end 202 + 203 + module Room_topic_content = struct 204 + type t = { topic : string } 205 + 206 + let jsont = 207 + Jsont.Object.( 208 + map (fun topic -> { topic }) 209 + |> mem "topic" Jsont.string ~enc:(fun t -> t.topic) 210 + |> finish) 211 + end 212 + 213 + module Room_avatar_content = struct 214 + type t = { 215 + url : string option; 216 + info : image_info option; 217 + } 218 + and image_info = { 219 + h : int option; 220 + w : int option; 221 + mimetype : string option; 222 + size : int option; 223 + } 224 + 225 + let image_info_jsont = 226 + Jsont.Object.( 227 + map (fun h w mimetype size -> { h; w; mimetype; size }) 228 + |> opt_mem "h" Jsont.int ~enc:(fun t -> t.h) 229 + |> opt_mem "w" Jsont.int ~enc:(fun t -> t.w) 230 + |> opt_mem "mimetype" Jsont.string ~enc:(fun t -> t.mimetype) 231 + |> opt_mem "size" Jsont.int ~enc:(fun t -> t.size) 232 + |> finish) 233 + 234 + let jsont = 235 + Jsont.Object.( 236 + map (fun url info -> { url; info }) 237 + |> opt_mem "url" Jsont.string ~enc:(fun t -> t.url) 238 + |> opt_mem "info" image_info_jsont ~enc:(fun t -> t.info) 239 + |> finish) 240 + end 241 + 242 + module Room_member_content = struct 243 + type t = { 244 + membership : Membership.t; 245 + displayname : string option; 246 + avatar_url : string option; 247 + is_direct : bool option; 248 + reason : string option; 249 + } 250 + 251 + let jsont = 252 + Jsont.Object.( 253 + map (fun membership displayname avatar_url is_direct reason -> 254 + { membership; displayname; avatar_url; is_direct; reason }) 255 + |> mem "membership" Membership.jsont ~enc:(fun t -> t.membership) 256 + |> opt_mem "displayname" Jsont.string ~enc:(fun t -> t.displayname) 257 + |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url) 258 + |> opt_mem "is_direct" Jsont.bool ~enc:(fun t -> t.is_direct) 259 + |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason) 260 + |> finish) 261 + end 262 + 263 + module Room_join_rules_content = struct 264 + type t = { 265 + join_rule : Join_rule.t; 266 + allow : allow_condition list option; 267 + } 268 + and allow_condition = { 269 + type_ : string; 270 + room_id : Room_id.t option; 271 + } 272 + 273 + let allow_condition_jsont = 274 + Jsont.Object.( 275 + map (fun type_ room_id -> { type_; room_id }) 276 + |> mem "type" Jsont.string ~enc:(fun c -> c.type_) 277 + |> opt_mem "room_id" Room_id.jsont ~enc:(fun c -> c.room_id) 278 + |> finish) 279 + 280 + let jsont = 281 + Jsont.Object.( 282 + map (fun join_rule allow -> { join_rule; allow }) 283 + |> mem "join_rule" Join_rule.jsont ~enc:(fun t -> t.join_rule) 284 + |> opt_mem "allow" (Jsont.list allow_condition_jsont) ~enc:(fun t -> t.allow) 285 + |> finish) 286 + end 287 + 288 + module Room_history_visibility_content = struct 289 + type t = { history_visibility : History_visibility.t } 290 + 291 + let jsont = 292 + Jsont.Object.( 293 + map (fun history_visibility -> { history_visibility }) 294 + |> mem "history_visibility" History_visibility.jsont 295 + ~enc:(fun t -> t.history_visibility) 296 + |> finish) 297 + end 298 + 299 + module Room_canonical_alias_content = struct 300 + type t = { 301 + alias : Room_alias.t option; 302 + alt_aliases : Room_alias.t list option; 303 + } 304 + 305 + let jsont = 306 + Jsont.Object.( 307 + map (fun alias alt_aliases -> { alias; alt_aliases }) 308 + |> opt_mem "alias" Room_alias.jsont ~enc:(fun t -> t.alias) 309 + |> opt_mem "alt_aliases" (Jsont.list Room_alias.jsont) ~enc:(fun t -> t.alt_aliases) 310 + |> finish) 311 + end 312 + 313 + module Room_power_levels_content = struct 314 + type t = { 315 + ban : int option; 316 + events : (string * int) list option; 317 + events_default : int option; 318 + invite : int option; 319 + kick : int option; 320 + redact : int option; 321 + state_default : int option; 322 + users : (string * int) list option; 323 + users_default : int option; 324 + } 325 + 326 + (* Helper for string -> int maps encoded as objects *) 327 + module StringMap = Map.Make(String) 328 + 329 + let int_map_jsont = 330 + Jsont.Object.as_string_map Jsont.int 331 + |> Jsont.map 332 + ~dec:(fun m -> StringMap.bindings m) 333 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 334 + 335 + let jsont = 336 + Jsont.Object.( 337 + map (fun ban events events_default invite kick redact 338 + state_default users users_default -> 339 + { ban; events; events_default; invite; kick; redact; 340 + state_default; users; users_default }) 341 + |> opt_mem "ban" Jsont.int ~enc:(fun t -> t.ban) 342 + |> opt_mem "events" int_map_jsont ~enc:(fun t -> t.events) 343 + |> opt_mem "events_default" Jsont.int ~enc:(fun t -> t.events_default) 344 + |> opt_mem "invite" Jsont.int ~enc:(fun t -> t.invite) 345 + |> opt_mem "kick" Jsont.int ~enc:(fun t -> t.kick) 346 + |> opt_mem "redact" Jsont.int ~enc:(fun t -> t.redact) 347 + |> opt_mem "state_default" Jsont.int ~enc:(fun t -> t.state_default) 348 + |> opt_mem "users" int_map_jsont ~enc:(fun t -> t.users) 349 + |> opt_mem "users_default" Jsont.int ~enc:(fun t -> t.users_default) 350 + |> finish) 351 + end 352 + 353 + module Room_encryption_content = struct 354 + type t = { 355 + algorithm : string; 356 + rotation_period_ms : int64 option; 357 + rotation_period_msgs : int option; 358 + } 359 + 360 + let jsont = 361 + Jsont.Object.( 362 + map (fun algorithm rotation_period_ms rotation_period_msgs -> 363 + { algorithm; rotation_period_ms; rotation_period_msgs }) 364 + |> mem "algorithm" Jsont.string ~enc:(fun t -> t.algorithm) 365 + |> opt_mem "rotation_period_ms" Jsont.int64 ~enc:(fun t -> t.rotation_period_ms) 366 + |> opt_mem "rotation_period_msgs" Jsont.int ~enc:(fun t -> t.rotation_period_msgs) 367 + |> finish) 368 + end 369 + 370 + module Room_pinned_events_content = struct 371 + type t = { 372 + pinned : string list; 373 + } 374 + 375 + let jsont = 376 + Jsont.Object.( 377 + map (fun pinned -> { pinned }) 378 + |> mem "pinned" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.pinned) 379 + |> finish) 380 + end 381 + 382 + module Room_server_acl_content = struct 383 + type t = { 384 + allow : string list; 385 + allow_ip_literals : bool; 386 + deny : string list; 387 + } 388 + 389 + let jsont = 390 + Jsont.Object.( 391 + map (fun allow allow_ip_literals deny -> 392 + { allow; allow_ip_literals; deny }) 393 + |> mem "allow" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.allow) 394 + |> mem "allow_ip_literals" Jsont.bool ~dec_absent:true ~enc:(fun t -> t.allow_ip_literals) 395 + |> mem "deny" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.deny) 396 + |> finish) 397 + end 398 + 399 + module Room_tombstone_content = struct 400 + type t = { 401 + body : string; 402 + replacement_room : Room_id.t; 403 + } 404 + 405 + let jsont = 406 + Jsont.Object.( 407 + map (fun body replacement_room -> { body; replacement_room }) 408 + |> mem "body" Jsont.string ~enc:(fun t -> t.body) 409 + |> mem "replacement_room" Room_id.jsont ~enc:(fun t -> t.replacement_room) 410 + |> finish) 411 + end 412 + 413 + module Room_guest_access_content = struct 414 + type access = 415 + | Can_join 416 + | Forbidden 417 + 418 + let access_jsont = 419 + Jsont.enum [ 420 + ("can_join", Can_join); 421 + ("forbidden", Forbidden); 422 + ] 423 + 424 + type t = { 425 + guest_access : access; 426 + } 427 + 428 + let jsont = 429 + Jsont.Object.( 430 + map (fun guest_access -> { guest_access }) 431 + |> mem "guest_access" access_jsont ~enc:(fun t -> t.guest_access) 432 + |> finish) 433 + end 434 + 435 + (** {1 Space Event Contents} *) 436 + 437 + module Space_child_content = struct 438 + type t = { 439 + via : string list option; 440 + order : string option; 441 + suggested : bool option; 442 + } 443 + 444 + let jsont = 445 + Jsont.Object.( 446 + map (fun via order suggested -> { via; order; suggested }) 447 + |> opt_mem "via" (Jsont.list Jsont.string) ~enc:(fun t -> t.via) 448 + |> opt_mem "order" Jsont.string ~enc:(fun t -> t.order) 449 + |> opt_mem "suggested" Jsont.bool ~enc:(fun t -> t.suggested) 450 + |> finish) 451 + end 452 + 453 + module Space_parent_content = struct 454 + type t = { 455 + via : string list option; 456 + canonical : bool option; 457 + } 458 + 459 + let jsont = 460 + Jsont.Object.( 461 + map (fun via canonical -> { via; canonical }) 462 + |> opt_mem "via" (Jsont.list Jsont.string) ~enc:(fun t -> t.via) 463 + |> opt_mem "canonical" Jsont.bool ~enc:(fun t -> t.canonical) 464 + |> finish) 465 + end 466 + 467 + (** {1 Call Event Contents} *) 468 + 469 + module Call_invite_content = struct 470 + type t = { 471 + call_id : string; 472 + party_id : string option; 473 + version : int; 474 + lifetime : int; 475 + offer : sdp_content; 476 + invitee : string option; 477 + } 478 + and sdp_content = { 479 + type_ : string; 480 + sdp : string; 481 + } 482 + 483 + let sdp_content_jsont = 484 + Jsont.Object.( 485 + map (fun type_ sdp -> { type_; sdp }) 486 + |> mem "type" Jsont.string ~enc:(fun t -> t.type_) 487 + |> mem "sdp" Jsont.string ~enc:(fun t -> t.sdp) 488 + |> finish) 489 + 490 + let jsont = 491 + Jsont.Object.( 492 + map (fun call_id party_id version lifetime offer invitee -> 493 + { call_id; party_id; version; lifetime; offer; invitee }) 494 + |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id) 495 + |> opt_mem "party_id" Jsont.string ~enc:(fun t -> t.party_id) 496 + |> mem "version" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.version) 497 + |> mem "lifetime" Jsont.int ~enc:(fun t -> t.lifetime) 498 + |> mem "offer" sdp_content_jsont ~enc:(fun t -> t.offer) 499 + |> opt_mem "invitee" Jsont.string ~enc:(fun t -> t.invitee) 500 + |> finish) 501 + end 502 + 503 + module Call_answer_content = struct 504 + type sdp_content = { 505 + type_ : string; 506 + sdp : string; 507 + } 508 + 509 + type t = { 510 + call_id : string; 511 + party_id : string option; 512 + version : int; 513 + answer : sdp_content; 514 + } 515 + 516 + let sdp_content_jsont = 517 + Jsont.Object.( 518 + map (fun type_ sdp -> { type_; sdp }) 519 + |> mem "type" Jsont.string ~enc:(fun t -> t.type_) 520 + |> mem "sdp" Jsont.string ~enc:(fun t -> t.sdp) 521 + |> finish) 522 + 523 + let jsont = 524 + Jsont.Object.( 525 + map (fun call_id party_id version answer -> 526 + { call_id; party_id; version; answer }) 527 + |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id) 528 + |> opt_mem "party_id" Jsont.string ~enc:(fun t -> t.party_id) 529 + |> mem "version" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.version) 530 + |> mem "answer" sdp_content_jsont ~enc:(fun t -> t.answer) 531 + |> finish) 532 + end 533 + 534 + module Call_hangup_content = struct 535 + type reason = 536 + | Ice_failed 537 + | Invite_timeout 538 + | User_hangup 539 + | User_media_failed 540 + | User_busy 541 + | Unknown_error 542 + 543 + let reason_jsont = 544 + Jsont.enum [ 545 + ("ice_failed", Ice_failed); 546 + ("invite_timeout", Invite_timeout); 547 + ("user_hangup", User_hangup); 548 + ("user_media_failed", User_media_failed); 549 + ("user_busy", User_busy); 550 + ("unknown_error", Unknown_error); 551 + ] 552 + 553 + type t = { 554 + call_id : string; 555 + party_id : string option; 556 + version : int; 557 + reason : reason option; 558 + } 559 + 560 + let jsont = 561 + Jsont.Object.( 562 + map (fun call_id party_id version reason -> 563 + { call_id; party_id; version; reason }) 564 + |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id) 565 + |> opt_mem "party_id" Jsont.string ~enc:(fun t -> t.party_id) 566 + |> mem "version" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.version) 567 + |> opt_mem "reason" reason_jsont ~enc:(fun t -> t.reason) 568 + |> finish) 569 + end 570 + 571 + module Call_candidates_content = struct 572 + type candidate = { 573 + candidate : string; 574 + sdp_mid : string; 575 + sdp_m_line_index : int; 576 + } 577 + 578 + let candidate_jsont = 579 + Jsont.Object.( 580 + map (fun candidate sdp_mid sdp_m_line_index -> 581 + { candidate; sdp_mid; sdp_m_line_index }) 582 + |> mem "candidate" Jsont.string ~enc:(fun t -> t.candidate) 583 + |> mem "sdpMid" Jsont.string ~enc:(fun t -> t.sdp_mid) 584 + |> mem "sdpMLineIndex" Jsont.int ~enc:(fun t -> t.sdp_m_line_index) 585 + |> finish) 586 + 587 + type t = { 588 + call_id : string; 589 + party_id : string option; 590 + version : int; 591 + candidates : candidate list; 592 + } 593 + 594 + let jsont = 595 + Jsont.Object.( 596 + map (fun call_id party_id version candidates -> 597 + { call_id; party_id; version; candidates }) 598 + |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id) 599 + |> opt_mem "party_id" Jsont.string ~enc:(fun t -> t.party_id) 600 + |> mem "version" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.version) 601 + |> mem "candidates" (Jsont.list candidate_jsont) ~dec_absent:[] 602 + ~enc:(fun t -> t.candidates) 603 + |> finish) 604 + end 605 + 606 + (** {1 Call Member Content (m.call.member)} *) 607 + 608 + module Call_member_content = struct 609 + (** Focus type for MatrixRTC *) 610 + type focus = { 611 + type_ : string; 612 + livekit_service_url : string option; 613 + livekit_alias : string option; 614 + } 615 + 616 + let focus_jsont = 617 + Jsont.Object.( 618 + map (fun type_ livekit_service_url livekit_alias -> 619 + { type_; livekit_service_url; livekit_alias }) 620 + |> mem "type" Jsont.string ~enc:(fun t -> t.type_) 621 + |> opt_mem "livekit_service_url" Jsont.string ~enc:(fun t -> t.livekit_service_url) 622 + |> opt_mem "livekit_alias" Jsont.string ~enc:(fun t -> t.livekit_alias) 623 + |> finish) 624 + 625 + type membership = { 626 + call_id : string; 627 + scope : string; 628 + application : string; 629 + device_id : string; 630 + expires : int64; 631 + foci_active : focus list option; 632 + membership_id : string option; 633 + } 634 + 635 + let membership_jsont = 636 + Jsont.Object.( 637 + map (fun call_id scope application device_id expires foci_active membership_id -> 638 + { call_id; scope; application; device_id; expires; foci_active; membership_id }) 639 + |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id) 640 + |> mem "scope" Jsont.string ~dec_absent:"m.room" ~enc:(fun t -> t.scope) 641 + |> mem "application" Jsont.string ~enc:(fun t -> t.application) 642 + |> mem "device_id" Jsont.string ~enc:(fun t -> t.device_id) 643 + |> mem "expires" Jsont.int64 ~enc:(fun t -> t.expires) 644 + |> opt_mem "foci_active" (Jsont.list focus_jsont) ~enc:(fun t -> t.foci_active) 645 + |> opt_mem "membership_id" Jsont.string ~enc:(fun t -> t.membership_id) 646 + |> finish) 647 + 648 + type t = { 649 + memberships : membership list; 650 + } 651 + 652 + let jsont = 653 + Jsont.Object.( 654 + map (fun memberships -> { memberships }) 655 + |> mem "memberships" (Jsont.list membership_jsont) ~dec_absent:[] 656 + ~enc:(fun t -> t.memberships) 657 + |> finish) 658 + end 659 + 660 + (** {1 Key Verification Event Contents} 661 + 662 + Key verification events are used to verify the identity of users 663 + and their devices through interactive verification protocols like 664 + SAS (Short Authentication String) or QR codes. 665 + 666 + @see <https://spec.matrix.org/v1.11/client-server-api/#key-verification-framework> Key Verification Framework 667 + @see <https://spec.matrix.org/v1.11/client-server-api/#short-authentication-string-sas-verification> SAS Verification *) 668 + 669 + module Key_verification_ready_content = struct 670 + (** Content for [m.key.verification.ready] events. 671 + 672 + Sent by a user to indicate they are ready to start verification 673 + and which methods they support. *) 674 + type t = { 675 + from_device : string; 676 + methods : string list; 677 + transaction_id : string option; 678 + } 679 + 680 + let jsont = 681 + Jsont.Object.( 682 + map (fun from_device methods transaction_id -> 683 + { from_device; methods; transaction_id }) 684 + |> mem "from_device" Jsont.string ~enc:(fun t -> t.from_device) 685 + |> mem "methods" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.methods) 686 + |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id) 687 + |> finish) 688 + end 689 + 690 + module Key_verification_start_content = struct 691 + type t = { 692 + from_device : string; 693 + method_ : string; 694 + transaction_id : string option; 695 + key_agreement_protocols : string list option; 696 + hashes : string list option; 697 + message_authentication_codes : string list option; 698 + short_authentication_string : string list option; 699 + } 700 + 701 + let jsont = 702 + Jsont.Object.( 703 + map (fun from_device method_ transaction_id key_agreement_protocols hashes 704 + message_authentication_codes short_authentication_string -> 705 + { from_device; method_; transaction_id; key_agreement_protocols; hashes; 706 + message_authentication_codes; short_authentication_string }) 707 + |> mem "from_device" Jsont.string ~enc:(fun t -> t.from_device) 708 + |> mem "method" Jsont.string ~enc:(fun t -> t.method_) 709 + |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id) 710 + |> opt_mem "key_agreement_protocols" (Jsont.list Jsont.string) 711 + ~enc:(fun t -> t.key_agreement_protocols) 712 + |> opt_mem "hashes" (Jsont.list Jsont.string) ~enc:(fun t -> t.hashes) 713 + |> opt_mem "message_authentication_codes" (Jsont.list Jsont.string) 714 + ~enc:(fun t -> t.message_authentication_codes) 715 + |> opt_mem "short_authentication_string" (Jsont.list Jsont.string) 716 + ~enc:(fun t -> t.short_authentication_string) 717 + |> finish) 718 + end 719 + 720 + module Key_verification_accept_content = struct 721 + type t = { 722 + transaction_id : string option; 723 + method_ : string; 724 + key_agreement_protocol : string; 725 + hash : string; 726 + message_authentication_code : string; 727 + short_authentication_string : string list; 728 + commitment : string; 729 + } 730 + 731 + let jsont = 732 + Jsont.Object.( 733 + map (fun transaction_id method_ key_agreement_protocol hash 734 + message_authentication_code short_authentication_string commitment -> 735 + { transaction_id; method_; key_agreement_protocol; hash; 736 + message_authentication_code; short_authentication_string; commitment }) 737 + |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id) 738 + |> mem "method" Jsont.string ~enc:(fun t -> t.method_) 739 + |> mem "key_agreement_protocol" Jsont.string ~enc:(fun t -> t.key_agreement_protocol) 740 + |> mem "hash" Jsont.string ~enc:(fun t -> t.hash) 741 + |> mem "message_authentication_code" Jsont.string 742 + ~enc:(fun t -> t.message_authentication_code) 743 + |> mem "short_authentication_string" (Jsont.list Jsont.string) 744 + ~enc:(fun t -> t.short_authentication_string) 745 + |> mem "commitment" Jsont.string ~enc:(fun t -> t.commitment) 746 + |> finish) 747 + end 748 + 749 + module Key_verification_key_content = struct 750 + type t = { 751 + transaction_id : string option; 752 + key : string; 753 + } 754 + 755 + let jsont = 756 + Jsont.Object.( 757 + map (fun transaction_id key -> { transaction_id; key }) 758 + |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id) 759 + |> mem "key" Jsont.string ~enc:(fun t -> t.key) 760 + |> finish) 761 + end 762 + 763 + module Key_verification_mac_content = struct 764 + module StringMap = Map.Make(String) 765 + 766 + let string_map_jsont = 767 + Jsont.Object.as_string_map Jsont.string 768 + |> Jsont.map 769 + ~dec:(fun m -> StringMap.bindings m) 770 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 771 + 772 + type t = { 773 + transaction_id : string option; 774 + mac : (string * string) list; 775 + keys : string; 776 + } 777 + 778 + let jsont = 779 + Jsont.Object.( 780 + map (fun transaction_id mac keys -> { transaction_id; mac; keys }) 781 + |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id) 782 + |> mem "mac" string_map_jsont ~enc:(fun t -> t.mac) 783 + |> mem "keys" Jsont.string ~enc:(fun t -> t.keys) 784 + |> finish) 785 + end 786 + 787 + module Key_verification_cancel_content = struct 788 + type t = { 789 + transaction_id : string option; 790 + code : string; 791 + reason : string; 792 + } 793 + 794 + let jsont = 795 + Jsont.Object.( 796 + map (fun transaction_id code reason -> { transaction_id; code; reason }) 797 + |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id) 798 + |> mem "code" Jsont.string ~enc:(fun t -> t.code) 799 + |> mem "reason" Jsont.string ~enc:(fun t -> t.reason) 800 + |> finish) 801 + end 802 + 803 + module Key_verification_done_content = struct 804 + type t = { 805 + transaction_id : string option; 806 + } 807 + 808 + let jsont = 809 + Jsont.Object.( 810 + map (fun transaction_id -> { transaction_id }) 811 + |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id) 812 + |> finish) 813 + end 814 + 815 + (** {1 Policy Rule Event Contents} 816 + 817 + Policy rules allow servers and rooms to define moderation policies. 818 + These events describe entities (users, rooms, servers) that should 819 + be banned or otherwise restricted. 820 + 821 + @see <https://spec.matrix.org/v1.11/client-server-api/#moderation-policy-lists> Moderation Policy Lists *) 822 + 823 + module Policy_rule_content = struct 824 + (** Recommendation for how to handle a policy rule match. 825 + 826 + Currently only [m.ban] is specified, but servers may define 827 + custom recommendations. *) 828 + type recommendation = 829 + | Ban (** The entity should be banned *) 830 + | Unknown of string (** Custom or unrecognized recommendation *) 831 + 832 + let recommendation_to_string = function 833 + | Ban -> "m.ban" 834 + | Unknown s -> s 835 + 836 + let recommendation_of_string = function 837 + | "m.ban" -> Ban 838 + | s -> Unknown s 839 + 840 + let recommendation_jsont = 841 + Jsont.of_of_string ~kind:"recommendation" 842 + ~enc:recommendation_to_string 843 + (fun s -> Ok (recommendation_of_string s)) 844 + 845 + type t = { 846 + entity : string; 847 + reason : string; 848 + recommendation : recommendation; 849 + } 850 + 851 + let jsont = 852 + Jsont.Object.( 853 + map (fun entity reason recommendation -> { entity; reason; recommendation }) 854 + |> mem "entity" Jsont.string ~enc:(fun t -> t.entity) 855 + |> mem "reason" Jsont.string ~enc:(fun t -> t.reason) 856 + |> mem "recommendation" recommendation_jsont ~enc:(fun t -> t.recommendation) 857 + |> finish) 858 + end 859 + 860 + (** {1 Account Data Contents} 861 + 862 + Account data events store per-user, per-room configuration such as 863 + read markers, tags, and notification settings. 864 + 865 + @see <https://spec.matrix.org/v1.11/client-server-api/#client-config> Client Config *) 866 + 867 + module Marked_unread_content = struct 868 + (** Content for [m.marked_unread] room account data. 869 + 870 + Allows users to manually mark a room as unread regardless of 871 + actual read status. *) 872 + type t = { 873 + unread : bool; (** Whether the room should be shown as unread *) 874 + } 875 + 876 + let jsont = 877 + Jsont.Object.( 878 + map (fun unread -> { unread }) 879 + |> mem "unread" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.unread) 880 + |> finish) 881 + end 882 + 883 + (** {1 Encrypted Event Content} *) 884 + 885 + module Encrypted_content = struct 886 + type algorithm = 887 + | Olm_v1_curve25519_aes_sha2_256 888 + | Megolm_v1_aes_sha2_256 889 + | Unknown of string 890 + 891 + let algorithm_to_string = function 892 + | Olm_v1_curve25519_aes_sha2_256 -> "m.olm.v1.curve25519-aes-sha2-256" 893 + | Megolm_v1_aes_sha2_256 -> "m.megolm.v1.aes-sha2-256" 894 + | Unknown s -> s 895 + 896 + let algorithm_of_string = function 897 + | "m.olm.v1.curve25519-aes-sha2-256" -> Olm_v1_curve25519_aes_sha2_256 898 + | "m.megolm.v1.aes-sha2-256" -> Megolm_v1_aes_sha2_256 899 + | s -> Unknown s 900 + 901 + let algorithm_jsont = 902 + Jsont.of_of_string ~kind:"algorithm" 903 + ~enc:algorithm_to_string 904 + (fun s -> Ok (algorithm_of_string s)) 905 + 906 + type t = { 907 + algorithm : algorithm; 908 + sender_key : string; 909 + ciphertext : Jsont.json; (* Can be string or object depending on algorithm *) 910 + session_id : string option; (* Megolm only *) 911 + device_id : string option; 912 + } 913 + 914 + let jsont = 915 + Jsont.Object.( 916 + map (fun algorithm sender_key ciphertext session_id device_id -> 917 + { algorithm; sender_key; ciphertext; session_id; device_id }) 918 + |> mem "algorithm" algorithm_jsont ~enc:(fun t -> t.algorithm) 919 + |> mem "sender_key" Jsont.string ~enc:(fun t -> t.sender_key) 920 + |> mem "ciphertext" Jsont.json ~enc:(fun t -> t.ciphertext) 921 + |> opt_mem "session_id" Jsont.string ~enc:(fun t -> t.session_id) 922 + |> opt_mem "device_id" Jsont.string ~enc:(fun t -> t.device_id) 923 + |> finish) 924 + end 925 + 926 + (** {1 Reaction Content} *) 927 + 928 + module Reaction_content = struct 929 + type relates_to = { 930 + rel_type : string; 931 + event_id : Event_id.t; 932 + key : string; 933 + } 934 + 935 + let relates_to_jsont = 936 + Jsont.Object.( 937 + map (fun rel_type event_id key -> { rel_type; event_id; key }) 938 + |> mem "rel_type" Jsont.string ~enc:(fun t -> t.rel_type) 939 + |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id) 940 + |> mem "key" Jsont.string ~enc:(fun t -> t.key) 941 + |> finish) 942 + 943 + type t = { 944 + relates_to : relates_to; 945 + } 946 + 947 + let jsont = 948 + Jsont.Object.( 949 + map (fun relates_to -> { relates_to }) 950 + |> mem "m.relates_to" relates_to_jsont ~enc:(fun t -> t.relates_to) 951 + |> finish) 952 + end 953 + 954 + (** {1 Beacon/Live Location Content} *) 955 + 956 + module Beacon_info_content = struct 957 + type t = { 958 + description : string option; 959 + live : bool; 960 + timeout : int64; 961 + asset_type : string option; 962 + } 963 + 964 + let jsont = 965 + Jsont.Object.( 966 + map (fun description live timeout asset_type -> 967 + { description; live; timeout; asset_type }) 968 + |> opt_mem "description" Jsont.string ~enc:(fun t -> t.description) 969 + |> mem "live" Jsont.bool ~dec_absent:true ~enc:(fun t -> t.live) 970 + |> mem "timeout" Jsont.int64 ~enc:(fun t -> t.timeout) 971 + |> opt_mem "org.matrix.msc3488.asset" Jsont.string ~enc:(fun t -> t.asset_type) 972 + |> finish) 973 + end 974 + 975 + module Beacon_content = struct 976 + type location = { 977 + uri : string; 978 + description : string option; 979 + } 980 + 981 + let location_jsont = 982 + Jsont.Object.( 983 + map (fun uri description -> { uri; description }) 984 + |> mem "uri" Jsont.string ~enc:(fun t -> t.uri) 985 + |> opt_mem "description" Jsont.string ~enc:(fun t -> t.description) 986 + |> finish) 987 + 988 + type relates_to = { 989 + rel_type : string; 990 + event_id : Event_id.t; 991 + } 992 + 993 + let relates_to_jsont = 994 + Jsont.Object.( 995 + map (fun rel_type event_id -> { rel_type; event_id }) 996 + |> mem "rel_type" Jsont.string ~enc:(fun t -> t.rel_type) 997 + |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id) 998 + |> finish) 999 + 1000 + type t = { 1001 + location : location; 1002 + timestamp : int64; 1003 + relates_to : relates_to; 1004 + } 1005 + 1006 + let jsont = 1007 + Jsont.Object.( 1008 + map (fun location timestamp relates_to -> 1009 + { location; timestamp; relates_to }) 1010 + |> mem "org.matrix.msc3488.location" location_jsont ~enc:(fun t -> t.location) 1011 + |> mem "org.matrix.msc3488.ts" Jsont.int64 ~enc:(fun t -> t.timestamp) 1012 + |> mem "m.relates_to" relates_to_jsont ~enc:(fun t -> t.relates_to) 1013 + |> finish) 1014 + end 1015 + 1016 + (** {1 Poll Event Contents} *) 1017 + 1018 + module Poll_start_content = struct 1019 + type poll_answer = { 1020 + id : string; 1021 + text : string; 1022 + } 1023 + 1024 + let poll_answer_jsont = 1025 + Jsont.Object.( 1026 + map (fun id text -> { id; text }) 1027 + |> mem "id" Jsont.string ~enc:(fun t -> t.id) 1028 + |> mem "org.matrix.msc1767.text" Jsont.string ~enc:(fun t -> t.text) 1029 + |> finish) 1030 + 1031 + type poll_kind = 1032 + | Disclosed 1033 + | Undisclosed 1034 + 1035 + let poll_kind_jsont = 1036 + Jsont.enum [ 1037 + ("org.matrix.msc3381.poll.disclosed", Disclosed); 1038 + ("org.matrix.msc3381.poll.undisclosed", Undisclosed); 1039 + ] 1040 + 1041 + type poll_start = { 1042 + question : string; 1043 + kind : poll_kind; 1044 + max_selections : int; 1045 + answers : poll_answer list; 1046 + } 1047 + 1048 + let poll_start_jsont = 1049 + Jsont.Object.( 1050 + map (fun question kind max_selections answers -> 1051 + { question; kind; max_selections; answers }) 1052 + |> mem "question" Jsont.string ~enc:(fun t -> t.question) 1053 + |> mem "kind" poll_kind_jsont ~dec_absent:Disclosed ~enc:(fun t -> t.kind) 1054 + |> mem "max_selections" Jsont.int ~dec_absent:1 ~enc:(fun t -> t.max_selections) 1055 + |> mem "answers" (Jsont.list poll_answer_jsont) ~enc:(fun t -> t.answers) 1056 + |> finish) 1057 + 1058 + type t = { 1059 + poll_start : poll_start; 1060 + text : string; 1061 + } 1062 + 1063 + let jsont = 1064 + Jsont.Object.( 1065 + map (fun poll_start text -> { poll_start; text }) 1066 + |> mem "org.matrix.msc3381.poll.start" poll_start_jsont ~enc:(fun t -> t.poll_start) 1067 + |> mem "org.matrix.msc1767.text" Jsont.string ~enc:(fun t -> t.text) 1068 + |> finish) 1069 + end 1070 + 1071 + module Poll_response_content = struct 1072 + type relates_to = { 1073 + rel_type : string; 1074 + event_id : Event_id.t; 1075 + } 1076 + 1077 + let relates_to_jsont = 1078 + Jsont.Object.( 1079 + map (fun rel_type event_id -> { rel_type; event_id }) 1080 + |> mem "rel_type" Jsont.string ~enc:(fun t -> t.rel_type) 1081 + |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id) 1082 + |> finish) 1083 + 1084 + type t = { 1085 + relates_to : relates_to; 1086 + answers : string list; 1087 + } 1088 + 1089 + let jsont = 1090 + Jsont.Object.( 1091 + map (fun relates_to answers -> { relates_to; answers }) 1092 + |> mem "m.relates_to" relates_to_jsont ~enc:(fun t -> t.relates_to) 1093 + |> mem "org.matrix.msc3381.poll.response" (Jsont.list Jsont.string) 1094 + ~dec_absent:[] ~enc:(fun t -> t.answers) 1095 + |> finish) 1096 + end 1097 + 1098 + module Poll_end_content = struct 1099 + type relates_to = { 1100 + rel_type : string; 1101 + event_id : Event_id.t; 1102 + } 1103 + 1104 + let relates_to_jsont = 1105 + Jsont.Object.( 1106 + map (fun rel_type event_id -> { rel_type; event_id }) 1107 + |> mem "rel_type" Jsont.string ~enc:(fun t -> t.rel_type) 1108 + |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id) 1109 + |> finish) 1110 + 1111 + type t = { 1112 + relates_to : relates_to; 1113 + text : string; 1114 + } 1115 + 1116 + let jsont = 1117 + Jsont.Object.( 1118 + map (fun relates_to text -> { relates_to; text }) 1119 + |> mem "m.relates_to" relates_to_jsont ~enc:(fun t -> t.relates_to) 1120 + |> mem "org.matrix.msc1767.text" Jsont.string ~enc:(fun t -> t.text) 1121 + |> finish) 1122 + end 1123 + 1124 + (** {1 Message Event Contents} *) 1125 + 1126 + module Msgtype = struct 1127 + type t = 1128 + | Text 1129 + | Emote 1130 + | Notice 1131 + | Image 1132 + | File 1133 + | Audio 1134 + | Video 1135 + | Location 1136 + | Custom of string 1137 + 1138 + let to_string = function 1139 + | Text -> "m.text" 1140 + | Emote -> "m.emote" 1141 + | Notice -> "m.notice" 1142 + | Image -> "m.image" 1143 + | File -> "m.file" 1144 + | Audio -> "m.audio" 1145 + | Video -> "m.video" 1146 + | Location -> "m.location" 1147 + | Custom s -> s 1148 + 1149 + let of_string = function 1150 + | "m.text" -> Text 1151 + | "m.emote" -> Emote 1152 + | "m.notice" -> Notice 1153 + | "m.image" -> Image 1154 + | "m.file" -> File 1155 + | "m.audio" -> Audio 1156 + | "m.video" -> Video 1157 + | "m.location" -> Location 1158 + | s -> Custom s 1159 + 1160 + let jsont = 1161 + Jsont.of_of_string ~kind:"msgtype" 1162 + ~enc:to_string 1163 + (fun s -> Ok (of_string s)) 1164 + end 1165 + 1166 + module Text_message_content = struct 1167 + type t = { 1168 + body : string; 1169 + msgtype : Msgtype.t; 1170 + format : string option; 1171 + formatted_body : string option; 1172 + } 1173 + 1174 + let jsont = 1175 + Jsont.Object.( 1176 + map (fun body msgtype format formatted_body -> 1177 + { body; msgtype; format; formatted_body }) 1178 + |> mem "body" Jsont.string ~enc:(fun t -> t.body) 1179 + |> mem "msgtype" Msgtype.jsont ~enc:(fun t -> t.msgtype) 1180 + |> opt_mem "format" Jsont.string ~enc:(fun t -> t.format) 1181 + |> opt_mem "formatted_body" Jsont.string ~enc:(fun t -> t.formatted_body) 1182 + |> finish) 1183 + 1184 + let make ?(format = "org.matrix.custom.html") ?formatted_body body = 1185 + let msgtype = Msgtype.Text in 1186 + match formatted_body with 1187 + | None -> { body; msgtype; format = None; formatted_body = None } 1188 + | Some fb -> { body; msgtype; format = Some format; formatted_body = Some fb } 1189 + end 1190 + 1191 + module Media_info = struct 1192 + type t = { 1193 + mimetype : string option; 1194 + size : int option; 1195 + duration : int option; 1196 + h : int option; 1197 + w : int option; 1198 + thumbnail_url : string option; 1199 + thumbnail_info : thumbnail_info option; 1200 + } 1201 + and thumbnail_info = { 1202 + mimetype : string option; 1203 + size : int option; 1204 + h : int option; 1205 + w : int option; 1206 + } 1207 + 1208 + let thumbnail_info_jsont : thumbnail_info Jsont.t = 1209 + Jsont.Object.( 1210 + map (fun mimetype size h w -> { mimetype; size; h; w }) 1211 + |> opt_mem "mimetype" Jsont.string ~enc:(fun (t : thumbnail_info) -> t.mimetype) 1212 + |> opt_mem "size" Jsont.int ~enc:(fun (t : thumbnail_info) -> t.size) 1213 + |> opt_mem "h" Jsont.int ~enc:(fun (t : thumbnail_info) -> t.h) 1214 + |> opt_mem "w" Jsont.int ~enc:(fun (t : thumbnail_info) -> t.w) 1215 + |> finish) 1216 + 1217 + let jsont = 1218 + Jsont.Object.( 1219 + map (fun mimetype size duration h w thumbnail_url thumbnail_info -> 1220 + { mimetype; size; duration; h; w; thumbnail_url; thumbnail_info }) 1221 + |> opt_mem "mimetype" Jsont.string ~enc:(fun t -> t.mimetype) 1222 + |> opt_mem "size" Jsont.int ~enc:(fun t -> t.size) 1223 + |> opt_mem "duration" Jsont.int ~enc:(fun t -> t.duration) 1224 + |> opt_mem "h" Jsont.int ~enc:(fun t -> t.h) 1225 + |> opt_mem "w" Jsont.int ~enc:(fun t -> t.w) 1226 + |> opt_mem "thumbnail_url" Jsont.string ~enc:(fun t -> t.thumbnail_url) 1227 + |> opt_mem "thumbnail_info" thumbnail_info_jsont ~enc:(fun t -> t.thumbnail_info) 1228 + |> finish) 1229 + end 1230 + 1231 + module Media_message_content = struct 1232 + type t = { 1233 + body : string; 1234 + msgtype : Msgtype.t; 1235 + url : string option; 1236 + info : Media_info.t option; 1237 + file : encrypted_file option; 1238 + } 1239 + and encrypted_file = { 1240 + url : string; 1241 + key : Jsont.json; (* JWK *) 1242 + iv : string; 1243 + hashes : (string * string) list; 1244 + v : string; 1245 + } 1246 + 1247 + module StringMap = Map.Make(String) 1248 + 1249 + let string_map_jsont = 1250 + Jsont.Object.as_string_map Jsont.string 1251 + |> Jsont.map 1252 + ~dec:(fun m -> StringMap.bindings m) 1253 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 1254 + 1255 + let encrypted_file_jsont : encrypted_file Jsont.t = 1256 + Jsont.Object.( 1257 + map (fun url key iv hashes v -> { url; key; iv; hashes; v }) 1258 + |> mem "url" Jsont.string ~enc:(fun (f : encrypted_file) -> f.url) 1259 + |> mem "key" Jsont.json ~enc:(fun (f : encrypted_file) -> f.key) 1260 + |> mem "iv" Jsont.string ~enc:(fun (f : encrypted_file) -> f.iv) 1261 + |> mem "hashes" string_map_jsont ~enc:(fun (f : encrypted_file) -> f.hashes) 1262 + |> mem "v" Jsont.string ~enc:(fun (f : encrypted_file) -> f.v) 1263 + |> finish) 1264 + 1265 + let jsont = 1266 + Jsont.Object.( 1267 + map (fun body msgtype url info file -> 1268 + { body; msgtype; url; info; file }) 1269 + |> mem "body" Jsont.string ~enc:(fun t -> t.body) 1270 + |> mem "msgtype" Msgtype.jsont ~enc:(fun t -> t.msgtype) 1271 + |> opt_mem "url" Jsont.string ~enc:(fun t -> t.url) 1272 + |> opt_mem "info" Media_info.jsont ~enc:(fun t -> t.info) 1273 + |> opt_mem "file" encrypted_file_jsont ~enc:(fun t -> t.file) 1274 + |> finish) 1275 + end 1276 + 1277 + (** {1 Sticker Content} *) 1278 + 1279 + module Sticker_content = struct 1280 + type t = { 1281 + body : string; 1282 + info : Media_info.t option; 1283 + url : string; 1284 + } 1285 + 1286 + let jsont = 1287 + Jsont.Object.( 1288 + map (fun body info url -> { body; info; url }) 1289 + |> mem "body" Jsont.string ~enc:(fun t -> t.body) 1290 + |> opt_mem "info" Media_info.jsont ~enc:(fun t -> t.info) 1291 + |> mem "url" Jsont.string ~enc:(fun t -> t.url) 1292 + |> finish) 1293 + end 1294 + 1295 + (** {1 Location Content} *) 1296 + 1297 + module Location_message_content = struct 1298 + type location_info = { 1299 + uri : string; 1300 + description : string option; 1301 + } 1302 + 1303 + let location_info_jsont = 1304 + Jsont.Object.( 1305 + map (fun uri description -> { uri; description }) 1306 + |> mem "uri" Jsont.string ~enc:(fun t -> t.uri) 1307 + |> opt_mem "description" Jsont.string ~enc:(fun t -> t.description) 1308 + |> finish) 1309 + 1310 + type t = { 1311 + body : string; 1312 + msgtype : Msgtype.t; 1313 + geo_uri : string; 1314 + info : location_info option; 1315 + } 1316 + 1317 + let jsont = 1318 + Jsont.Object.( 1319 + map (fun body msgtype geo_uri info -> 1320 + { body; msgtype; geo_uri; info }) 1321 + |> mem "body" Jsont.string ~enc:(fun t -> t.body) 1322 + |> mem "msgtype" Msgtype.jsont ~enc:(fun t -> t.msgtype) 1323 + |> mem "geo_uri" Jsont.string ~enc:(fun t -> t.geo_uri) 1324 + |> opt_mem "info" location_info_jsont ~enc:(fun t -> t.info) 1325 + |> finish) 1326 + end 1327 + 1328 + (** {1 Event Types} *) 1329 + 1330 + module Event_type = struct 1331 + type t = 1332 + (* Room state events *) 1333 + | Room_create 1334 + | Room_name 1335 + | Room_topic 1336 + | Room_avatar 1337 + | Room_member 1338 + | Room_join_rules 1339 + | Room_history_visibility 1340 + | Room_canonical_alias 1341 + | Room_power_levels 1342 + | Room_encryption 1343 + | Room_pinned_events 1344 + | Room_server_acl 1345 + | Room_tombstone 1346 + | Room_guest_access 1347 + (* Space events *) 1348 + | Space_child 1349 + | Space_parent 1350 + (* Message events *) 1351 + | Room_message 1352 + | Room_message_encrypted 1353 + | Room_redaction 1354 + | Reaction 1355 + | Sticker 1356 + (* Call events *) 1357 + | Call_invite 1358 + | Call_candidates 1359 + | Call_answer 1360 + | Call_hangup 1361 + | Call_reject 1362 + | Call_select_answer 1363 + | Call_negotiate 1364 + | Call_member 1365 + (* Key verification events *) 1366 + | Key_verification_ready 1367 + | Key_verification_start 1368 + | Key_verification_accept 1369 + | Key_verification_key 1370 + | Key_verification_mac 1371 + | Key_verification_cancel 1372 + | Key_verification_done 1373 + (* Policy rule events *) 1374 + | Policy_rule_room 1375 + | Policy_rule_server 1376 + | Policy_rule_user 1377 + (* To-device events *) 1378 + | Room_key 1379 + | Room_key_request 1380 + | Forwarded_room_key 1381 + | Dummy 1382 + (* Ephemeral *) 1383 + | Typing 1384 + | Receipt 1385 + | Presence 1386 + (* Account data *) 1387 + | Direct 1388 + | Ignored_user_list 1389 + | Fully_read 1390 + | Marked_unread 1391 + | Tag 1392 + | Push_rules 1393 + | Secret_storage_default_key 1394 + | Secret_storage_key 1395 + | Cross_signing_keys 1396 + (* Beacon/location events *) 1397 + | Beacon_info 1398 + | Beacon 1399 + (* Polls *) 1400 + | Poll_start 1401 + | Poll_response 1402 + | Poll_end 1403 + (* Custom *) 1404 + | Custom of string 1405 + 1406 + let to_string = function 1407 + | Room_create -> "m.room.create" 1408 + | Room_name -> "m.room.name" 1409 + | Room_topic -> "m.room.topic" 1410 + | Room_avatar -> "m.room.avatar" 1411 + | Room_member -> "m.room.member" 1412 + | Room_join_rules -> "m.room.join_rules" 1413 + | Room_history_visibility -> "m.room.history_visibility" 1414 + | Room_canonical_alias -> "m.room.canonical_alias" 1415 + | Room_power_levels -> "m.room.power_levels" 1416 + | Room_encryption -> "m.room.encryption" 1417 + | Room_pinned_events -> "m.room.pinned_events" 1418 + | Room_server_acl -> "m.room.server_acl" 1419 + | Room_tombstone -> "m.room.tombstone" 1420 + | Room_guest_access -> "m.room.guest_access" 1421 + | Space_child -> "m.space.child" 1422 + | Space_parent -> "m.space.parent" 1423 + | Room_message -> "m.room.message" 1424 + | Room_message_encrypted -> "m.room.encrypted" 1425 + | Room_redaction -> "m.room.redaction" 1426 + | Reaction -> "m.reaction" 1427 + | Sticker -> "m.sticker" 1428 + | Call_invite -> "m.call.invite" 1429 + | Call_candidates -> "m.call.candidates" 1430 + | Call_answer -> "m.call.answer" 1431 + | Call_hangup -> "m.call.hangup" 1432 + | Call_reject -> "m.call.reject" 1433 + | Call_select_answer -> "m.call.select_answer" 1434 + | Call_negotiate -> "m.call.negotiate" 1435 + | Call_member -> "m.call.member" 1436 + | Key_verification_ready -> "m.key.verification.ready" 1437 + | Key_verification_start -> "m.key.verification.start" 1438 + | Key_verification_accept -> "m.key.verification.accept" 1439 + | Key_verification_key -> "m.key.verification.key" 1440 + | Key_verification_mac -> "m.key.verification.mac" 1441 + | Key_verification_cancel -> "m.key.verification.cancel" 1442 + | Key_verification_done -> "m.key.verification.done" 1443 + | Policy_rule_room -> "m.policy.rule.room" 1444 + | Policy_rule_server -> "m.policy.rule.server" 1445 + | Policy_rule_user -> "m.policy.rule.user" 1446 + | Room_key -> "m.room_key" 1447 + | Room_key_request -> "m.room_key_request" 1448 + | Forwarded_room_key -> "m.forwarded_room_key" 1449 + | Dummy -> "m.dummy" 1450 + | Typing -> "m.typing" 1451 + | Receipt -> "m.receipt" 1452 + | Presence -> "m.presence" 1453 + | Direct -> "m.direct" 1454 + | Ignored_user_list -> "m.ignored_user_list" 1455 + | Fully_read -> "m.fully_read" 1456 + | Marked_unread -> "m.marked_unread" 1457 + | Tag -> "m.tag" 1458 + | Push_rules -> "m.push_rules" 1459 + | Secret_storage_default_key -> "m.secret_storage.default_key" 1460 + | Secret_storage_key -> "m.secret_storage.key" 1461 + | Cross_signing_keys -> "m.cross_signing.keys" 1462 + | Beacon_info -> "org.matrix.msc3672.beacon_info" 1463 + | Beacon -> "org.matrix.msc3672.beacon" 1464 + | Poll_start -> "m.poll.start" 1465 + | Poll_response -> "m.poll.response" 1466 + | Poll_end -> "m.poll.end" 1467 + | Custom s -> s 1468 + 1469 + let of_string = function 1470 + | "m.room.create" -> Room_create 1471 + | "m.room.name" -> Room_name 1472 + | "m.room.topic" -> Room_topic 1473 + | "m.room.avatar" -> Room_avatar 1474 + | "m.room.member" -> Room_member 1475 + | "m.room.join_rules" -> Room_join_rules 1476 + | "m.room.history_visibility" -> Room_history_visibility 1477 + | "m.room.canonical_alias" -> Room_canonical_alias 1478 + | "m.room.power_levels" -> Room_power_levels 1479 + | "m.room.encryption" -> Room_encryption 1480 + | "m.room.pinned_events" -> Room_pinned_events 1481 + | "m.room.server_acl" -> Room_server_acl 1482 + | "m.room.tombstone" -> Room_tombstone 1483 + | "m.room.guest_access" -> Room_guest_access 1484 + | "m.space.child" -> Space_child 1485 + | "m.space.parent" -> Space_parent 1486 + | "m.room.message" -> Room_message 1487 + | "m.room.encrypted" -> Room_message_encrypted 1488 + | "m.room.redaction" -> Room_redaction 1489 + | "m.reaction" -> Reaction 1490 + | "m.sticker" -> Sticker 1491 + | "m.call.invite" -> Call_invite 1492 + | "m.call.candidates" -> Call_candidates 1493 + | "m.call.answer" -> Call_answer 1494 + | "m.call.hangup" -> Call_hangup 1495 + | "m.call.reject" -> Call_reject 1496 + | "m.call.select_answer" -> Call_select_answer 1497 + | "m.call.negotiate" -> Call_negotiate 1498 + | "m.call.member" -> Call_member 1499 + | "m.key.verification.ready" -> Key_verification_ready 1500 + | "m.key.verification.start" -> Key_verification_start 1501 + | "m.key.verification.accept" -> Key_verification_accept 1502 + | "m.key.verification.key" -> Key_verification_key 1503 + | "m.key.verification.mac" -> Key_verification_mac 1504 + | "m.key.verification.cancel" -> Key_verification_cancel 1505 + | "m.key.verification.done" -> Key_verification_done 1506 + | "m.policy.rule.room" -> Policy_rule_room 1507 + | "m.policy.rule.server" -> Policy_rule_server 1508 + | "m.policy.rule.user" -> Policy_rule_user 1509 + | "m.room_key" -> Room_key 1510 + | "m.room_key_request" -> Room_key_request 1511 + | "m.forwarded_room_key" -> Forwarded_room_key 1512 + | "m.dummy" -> Dummy 1513 + | "m.typing" -> Typing 1514 + | "m.receipt" -> Receipt 1515 + | "m.presence" -> Presence 1516 + | "m.direct" -> Direct 1517 + | "m.ignored_user_list" -> Ignored_user_list 1518 + | "m.fully_read" -> Fully_read 1519 + | "m.marked_unread" -> Marked_unread 1520 + | "m.tag" -> Tag 1521 + | "m.push_rules" -> Push_rules 1522 + | "m.secret_storage.default_key" -> Secret_storage_default_key 1523 + | "m.secret_storage.key" -> Secret_storage_key 1524 + | "m.cross_signing.keys" -> Cross_signing_keys 1525 + | "org.matrix.msc3672.beacon_info" -> Beacon_info 1526 + | "org.matrix.msc3672.beacon" -> Beacon 1527 + | "m.poll.start" -> Poll_start 1528 + | "m.poll.response" -> Poll_response 1529 + | "m.poll.end" -> Poll_end 1530 + | s -> Custom s 1531 + 1532 + let jsont = 1533 + Jsont.of_of_string ~kind:"event_type" 1534 + ~enc:to_string 1535 + (fun s -> Ok (of_string s)) 1536 + end 1537 + 1538 + (** {1 State Events} *) 1539 + 1540 + module State_event = struct 1541 + (** A room state event with typed content. *) 1542 + 1543 + type 'content t = { 1544 + event_id : Event_id.t; 1545 + sender : User_id.t; 1546 + origin_server_ts : Timestamp.t; 1547 + state_key : string; 1548 + type_ : Event_type.t; 1549 + content : 'content; 1550 + unsigned : Unsigned.t option; 1551 + } 1552 + 1553 + let make_jsont content_jsont = 1554 + Jsont.Object.( 1555 + map (fun event_id sender origin_server_ts state_key type_ content unsigned -> 1556 + { event_id; sender; origin_server_ts; state_key; type_; content; unsigned }) 1557 + |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id) 1558 + |> mem "sender" User_id.jsont ~enc:(fun t -> t.sender) 1559 + |> mem "origin_server_ts" Timestamp.jsont ~enc:(fun t -> t.origin_server_ts) 1560 + |> mem "state_key" Jsont.string ~enc:(fun t -> t.state_key) 1561 + |> mem "type" Event_type.jsont ~enc:(fun t -> t.type_) 1562 + |> mem "content" content_jsont ~enc:(fun t -> t.content) 1563 + |> opt_mem "unsigned" Unsigned.jsont ~enc:(fun t -> t.unsigned) 1564 + |> finish) 1565 + end 1566 + 1567 + (** {1 Room Events} *) 1568 + 1569 + module Room_event = struct 1570 + (** A room event (timeline event) with typed content. *) 1571 + 1572 + type 'content t = { 1573 + event_id : Event_id.t; 1574 + sender : User_id.t; 1575 + origin_server_ts : Timestamp.t; 1576 + type_ : Event_type.t; 1577 + content : 'content; 1578 + unsigned : Unsigned.t option; 1579 + } 1580 + 1581 + let make_jsont content_jsont = 1582 + Jsont.Object.( 1583 + map (fun event_id sender origin_server_ts type_ content unsigned -> 1584 + { event_id; sender; origin_server_ts; type_; content; unsigned }) 1585 + |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id) 1586 + |> mem "sender" User_id.jsont ~enc:(fun t -> t.sender) 1587 + |> mem "origin_server_ts" Timestamp.jsont ~enc:(fun t -> t.origin_server_ts) 1588 + |> mem "type" Event_type.jsont ~enc:(fun t -> t.type_) 1589 + |> mem "content" content_jsont ~enc:(fun t -> t.content) 1590 + |> opt_mem "unsigned" Unsigned.jsont ~enc:(fun t -> t.unsigned) 1591 + |> finish) 1592 + end 1593 + 1594 + (** {1 Raw/Untyped Events} 1595 + 1596 + For events where we don't know the content type ahead of time. *) 1597 + 1598 + module Raw_event = struct 1599 + type t = { 1600 + event_id : Event_id.t option; 1601 + sender : User_id.t; 1602 + origin_server_ts : Timestamp.t; 1603 + type_ : Event_type.t; 1604 + state_key : string option; 1605 + content : Jsont.json; 1606 + unsigned : Unsigned.t option; 1607 + room_id : Room_id.t option; 1608 + } 1609 + 1610 + let jsont = 1611 + Jsont.Object.( 1612 + map (fun event_id sender origin_server_ts type_ state_key content unsigned room_id -> 1613 + { event_id; sender; origin_server_ts; type_; state_key; content; unsigned; room_id }) 1614 + |> opt_mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id) 1615 + |> mem "sender" User_id.jsont ~enc:(fun t -> t.sender) 1616 + |> mem "origin_server_ts" Timestamp.jsont ~enc:(fun t -> t.origin_server_ts) 1617 + |> mem "type" Event_type.jsont ~enc:(fun t -> t.type_) 1618 + |> opt_mem "state_key" Jsont.string ~enc:(fun t -> t.state_key) 1619 + |> mem "content" Jsont.json ~enc:(fun t -> t.content) 1620 + |> opt_mem "unsigned" Unsigned.jsont ~enc:(fun t -> t.unsigned) 1621 + |> opt_mem "room_id" Room_id.jsont ~enc:(fun t -> t.room_id) 1622 + |> finish) 1623 + end
+611
lib/matrix_proto/matrix_event.mli
··· 1 + (** Matrix event types with JSON codecs. *) 2 + 3 + open Matrix_id 4 + 5 + (** {1 Timestamps} *) 6 + 7 + module Timestamp : sig 8 + type t = int64 9 + val of_ptime : Ptime.t -> t 10 + val to_ptime_opt : t -> Ptime.t option 11 + val jsont : t Jsont.t 12 + end 13 + 14 + (** {1 Unsigned Event Data} *) 15 + 16 + module Unsigned : sig 17 + type t = { 18 + age : int64 option; 19 + prev_content : Jsont.json option; 20 + prev_sender : User_id.t option; 21 + redacted_because : Jsont.json option; 22 + transaction_id : Transaction_id.t option; 23 + } 24 + val empty : t 25 + val jsont : t Jsont.t 26 + end 27 + 28 + (** {1 Room Membership} *) 29 + 30 + module Membership : sig 31 + type t = Join | Invite | Leave | Ban | Knock 32 + val to_string : t -> string 33 + val of_string : string -> (t, [> `Unknown_membership of string ]) result 34 + val jsont : t Jsont.t 35 + end 36 + 37 + (** {1 Join Rules} *) 38 + 39 + module Join_rule : sig 40 + type t = Public | Invite | Knock | Restricted | Knock_restricted | Private 41 + val jsont : t Jsont.t 42 + end 43 + 44 + (** {1 History Visibility} *) 45 + 46 + module History_visibility : sig 47 + type t = Invited | Joined | Shared | World_readable 48 + val jsont : t Jsont.t 49 + end 50 + 51 + (** {1 Room State Event Contents} *) 52 + 53 + module Room_create_content : sig 54 + type t = { 55 + creator : User_id.t option; 56 + room_version : string option; 57 + predecessor : predecessor option; 58 + type_ : string option; 59 + } 60 + and predecessor = { 61 + room_id : Room_id.t; 62 + event_id : Event_id.t; 63 + } 64 + val jsont : t Jsont.t 65 + end 66 + 67 + module Room_name_content : sig 68 + type t = { name : string } 69 + val jsont : t Jsont.t 70 + end 71 + 72 + module Room_topic_content : sig 73 + type t = { topic : string } 74 + val jsont : t Jsont.t 75 + end 76 + 77 + module Room_avatar_content : sig 78 + type t = { 79 + url : string option; 80 + info : image_info option; 81 + } 82 + and image_info = { 83 + h : int option; 84 + w : int option; 85 + mimetype : string option; 86 + size : int option; 87 + } 88 + val jsont : t Jsont.t 89 + end 90 + 91 + module Room_member_content : sig 92 + type t = { 93 + membership : Membership.t; 94 + displayname : string option; 95 + avatar_url : string option; 96 + is_direct : bool option; 97 + reason : string option; 98 + } 99 + val jsont : t Jsont.t 100 + end 101 + 102 + module Room_join_rules_content : sig 103 + type t = { 104 + join_rule : Join_rule.t; 105 + allow : allow_condition list option; 106 + } 107 + and allow_condition = { 108 + type_ : string; 109 + room_id : Room_id.t option; 110 + } 111 + val jsont : t Jsont.t 112 + end 113 + 114 + module Room_history_visibility_content : sig 115 + type t = { history_visibility : History_visibility.t } 116 + val jsont : t Jsont.t 117 + end 118 + 119 + module Room_canonical_alias_content : sig 120 + type t = { 121 + alias : Room_alias.t option; 122 + alt_aliases : Room_alias.t list option; 123 + } 124 + val jsont : t Jsont.t 125 + end 126 + 127 + module Room_power_levels_content : sig 128 + type t = { 129 + ban : int option; 130 + events : (string * int) list option; 131 + events_default : int option; 132 + invite : int option; 133 + kick : int option; 134 + redact : int option; 135 + state_default : int option; 136 + users : (string * int) list option; 137 + users_default : int option; 138 + } 139 + val jsont : t Jsont.t 140 + end 141 + 142 + module Room_encryption_content : sig 143 + type t = { 144 + algorithm : string; 145 + rotation_period_ms : int64 option; 146 + rotation_period_msgs : int option; 147 + } 148 + val jsont : t Jsont.t 149 + end 150 + 151 + module Room_pinned_events_content : sig 152 + type t = { pinned : string list } 153 + val jsont : t Jsont.t 154 + end 155 + 156 + module Room_server_acl_content : sig 157 + type t = { 158 + allow : string list; 159 + allow_ip_literals : bool; 160 + deny : string list; 161 + } 162 + val jsont : t Jsont.t 163 + end 164 + 165 + module Room_tombstone_content : sig 166 + type t = { 167 + body : string; 168 + replacement_room : Room_id.t; 169 + } 170 + val jsont : t Jsont.t 171 + end 172 + 173 + module Room_guest_access_content : sig 174 + type access = Can_join | Forbidden 175 + val access_jsont : access Jsont.t 176 + type t = { guest_access : access } 177 + val jsont : t Jsont.t 178 + end 179 + 180 + (** {1 Space Event Contents} *) 181 + 182 + module Space_child_content : sig 183 + type t = { 184 + via : string list option; 185 + order : string option; 186 + suggested : bool option; 187 + } 188 + val jsont : t Jsont.t 189 + end 190 + 191 + module Space_parent_content : sig 192 + type t = { 193 + via : string list option; 194 + canonical : bool option; 195 + } 196 + val jsont : t Jsont.t 197 + end 198 + 199 + (** {1 Call Event Contents} *) 200 + 201 + module Call_invite_content : sig 202 + type sdp_content = { type_ : string; sdp : string } 203 + type t = { 204 + call_id : string; 205 + party_id : string option; 206 + version : int; 207 + lifetime : int; 208 + offer : sdp_content; 209 + invitee : string option; 210 + } 211 + val jsont : t Jsont.t 212 + end 213 + 214 + module Call_answer_content : sig 215 + type sdp_content = { type_ : string; sdp : string } 216 + type t = { 217 + call_id : string; 218 + party_id : string option; 219 + version : int; 220 + answer : sdp_content; 221 + } 222 + val jsont : t Jsont.t 223 + end 224 + 225 + module Call_hangup_content : sig 226 + type reason = 227 + | Ice_failed | Invite_timeout | User_hangup 228 + | User_media_failed | User_busy | Unknown_error 229 + type t = { 230 + call_id : string; 231 + party_id : string option; 232 + version : int; 233 + reason : reason option; 234 + } 235 + val jsont : t Jsont.t 236 + end 237 + 238 + module Call_candidates_content : sig 239 + type candidate = { 240 + candidate : string; 241 + sdp_mid : string; 242 + sdp_m_line_index : int; 243 + } 244 + type t = { 245 + call_id : string; 246 + party_id : string option; 247 + version : int; 248 + candidates : candidate list; 249 + } 250 + val jsont : t Jsont.t 251 + end 252 + 253 + (** {1 Call Member Content (m.call.member)} *) 254 + 255 + module Call_member_content : sig 256 + type focus = { 257 + type_ : string; 258 + livekit_service_url : string option; 259 + livekit_alias : string option; 260 + } 261 + type membership = { 262 + call_id : string; 263 + scope : string; 264 + application : string; 265 + device_id : string; 266 + expires : int64; 267 + foci_active : focus list option; 268 + membership_id : string option; 269 + } 270 + type t = { memberships : membership list } 271 + val jsont : t Jsont.t 272 + end 273 + 274 + (** {1 Key Verification Event Contents} *) 275 + 276 + module Key_verification_ready_content : sig 277 + type t = { 278 + from_device : string; 279 + methods : string list; 280 + transaction_id : string option; 281 + } 282 + val jsont : t Jsont.t 283 + end 284 + 285 + module Key_verification_start_content : sig 286 + type t = { 287 + from_device : string; 288 + method_ : string; 289 + transaction_id : string option; 290 + key_agreement_protocols : string list option; 291 + hashes : string list option; 292 + message_authentication_codes : string list option; 293 + short_authentication_string : string list option; 294 + } 295 + val jsont : t Jsont.t 296 + end 297 + 298 + module Key_verification_accept_content : sig 299 + type t = { 300 + transaction_id : string option; 301 + method_ : string; 302 + key_agreement_protocol : string; 303 + hash : string; 304 + message_authentication_code : string; 305 + short_authentication_string : string list; 306 + commitment : string; 307 + } 308 + val jsont : t Jsont.t 309 + end 310 + 311 + module Key_verification_key_content : sig 312 + type t = { 313 + transaction_id : string option; 314 + key : string; 315 + } 316 + val jsont : t Jsont.t 317 + end 318 + 319 + module Key_verification_mac_content : sig 320 + type t = { 321 + transaction_id : string option; 322 + mac : (string * string) list; 323 + keys : string; 324 + } 325 + val jsont : t Jsont.t 326 + end 327 + 328 + module Key_verification_cancel_content : sig 329 + type t = { 330 + transaction_id : string option; 331 + code : string; 332 + reason : string; 333 + } 334 + val jsont : t Jsont.t 335 + end 336 + 337 + module Key_verification_done_content : sig 338 + type t = { transaction_id : string option } 339 + val jsont : t Jsont.t 340 + end 341 + 342 + (** {1 Policy Rule Event Contents} *) 343 + 344 + module Policy_rule_content : sig 345 + type recommendation = Ban | Unknown of string 346 + val recommendation_to_string : recommendation -> string 347 + val recommendation_of_string : string -> recommendation 348 + type t = { 349 + entity : string; 350 + reason : string; 351 + recommendation : recommendation; 352 + } 353 + val jsont : t Jsont.t 354 + end 355 + 356 + (** {1 Account Data Contents} *) 357 + 358 + module Marked_unread_content : sig 359 + type t = { unread : bool } 360 + val jsont : t Jsont.t 361 + end 362 + 363 + (** {1 Encrypted Event Content} *) 364 + 365 + module Encrypted_content : sig 366 + type algorithm = 367 + | Olm_v1_curve25519_aes_sha2_256 368 + | Megolm_v1_aes_sha2_256 369 + | Unknown of string 370 + val algorithm_to_string : algorithm -> string 371 + val algorithm_of_string : string -> algorithm 372 + type t = { 373 + algorithm : algorithm; 374 + sender_key : string; 375 + ciphertext : Jsont.json; 376 + session_id : string option; 377 + device_id : string option; 378 + } 379 + val jsont : t Jsont.t 380 + end 381 + 382 + (** {1 Reaction Content} *) 383 + 384 + module Reaction_content : sig 385 + type relates_to = { 386 + rel_type : string; 387 + event_id : Event_id.t; 388 + key : string; 389 + } 390 + type t = { relates_to : relates_to } 391 + val jsont : t Jsont.t 392 + end 393 + 394 + (** {1 Beacon/Live Location Content} *) 395 + 396 + module Beacon_info_content : sig 397 + type t = { 398 + description : string option; 399 + live : bool; 400 + timeout : int64; 401 + asset_type : string option; 402 + } 403 + val jsont : t Jsont.t 404 + end 405 + 406 + module Beacon_content : sig 407 + type location = { uri : string; description : string option } 408 + type relates_to = { rel_type : string; event_id : Event_id.t } 409 + type t = { 410 + location : location; 411 + timestamp : int64; 412 + relates_to : relates_to; 413 + } 414 + val jsont : t Jsont.t 415 + end 416 + 417 + (** {1 Poll Event Contents} *) 418 + 419 + module Poll_start_content : sig 420 + type poll_answer = { id : string; text : string } 421 + type poll_kind = Disclosed | Undisclosed 422 + type poll_start = { 423 + question : string; 424 + kind : poll_kind; 425 + max_selections : int; 426 + answers : poll_answer list; 427 + } 428 + type t = { poll_start : poll_start; text : string } 429 + val jsont : t Jsont.t 430 + end 431 + 432 + module Poll_response_content : sig 433 + type relates_to = { rel_type : string; event_id : Event_id.t } 434 + type t = { relates_to : relates_to; answers : string list } 435 + val jsont : t Jsont.t 436 + end 437 + 438 + module Poll_end_content : sig 439 + type relates_to = { rel_type : string; event_id : Event_id.t } 440 + type t = { relates_to : relates_to; text : string } 441 + val jsont : t Jsont.t 442 + end 443 + 444 + (** {1 Message Event Contents} *) 445 + 446 + module Msgtype : sig 447 + type t = 448 + | Text | Emote | Notice | Image | File | Audio | Video | Location 449 + | Custom of string 450 + val to_string : t -> string 451 + val of_string : string -> t 452 + val jsont : t Jsont.t 453 + end 454 + 455 + module Text_message_content : sig 456 + type t = { 457 + body : string; 458 + msgtype : Msgtype.t; 459 + format : string option; 460 + formatted_body : string option; 461 + } 462 + val jsont : t Jsont.t 463 + val make : ?format:string -> ?formatted_body:string -> string -> t 464 + end 465 + 466 + module Media_info : sig 467 + type t = { 468 + mimetype : string option; 469 + size : int option; 470 + duration : int option; 471 + h : int option; 472 + w : int option; 473 + thumbnail_url : string option; 474 + thumbnail_info : thumbnail_info option; 475 + } 476 + and thumbnail_info = { 477 + mimetype : string option; 478 + size : int option; 479 + h : int option; 480 + w : int option; 481 + } 482 + val jsont : t Jsont.t 483 + end 484 + 485 + module Media_message_content : sig 486 + type t = { 487 + body : string; 488 + msgtype : Msgtype.t; 489 + url : string option; 490 + info : Media_info.t option; 491 + file : encrypted_file option; 492 + } 493 + and encrypted_file = { 494 + url : string; 495 + key : Jsont.json; 496 + iv : string; 497 + hashes : (string * string) list; 498 + v : string; 499 + } 500 + val jsont : t Jsont.t 501 + end 502 + 503 + (** {1 Sticker Content} *) 504 + 505 + module Sticker_content : sig 506 + type t = { 507 + body : string; 508 + info : Media_info.t option; 509 + url : string; 510 + } 511 + val jsont : t Jsont.t 512 + end 513 + 514 + (** {1 Location Content} *) 515 + 516 + module Location_message_content : sig 517 + type location_info = { uri : string; description : string option } 518 + type t = { 519 + body : string; 520 + msgtype : Msgtype.t; 521 + geo_uri : string; 522 + info : location_info option; 523 + } 524 + val jsont : t Jsont.t 525 + end 526 + 527 + (** {1 Event Types} *) 528 + 529 + module Event_type : sig 530 + type t = 531 + (* Room state events *) 532 + | Room_create | Room_name | Room_topic | Room_avatar | Room_member 533 + | Room_join_rules | Room_history_visibility | Room_canonical_alias 534 + | Room_power_levels | Room_encryption | Room_pinned_events 535 + | Room_server_acl | Room_tombstone | Room_guest_access 536 + (* Space events *) 537 + | Space_child | Space_parent 538 + (* Message events *) 539 + | Room_message | Room_message_encrypted | Room_redaction 540 + | Reaction | Sticker 541 + (* Call events *) 542 + | Call_invite | Call_candidates | Call_answer | Call_hangup 543 + | Call_reject | Call_select_answer | Call_negotiate | Call_member 544 + (* Key verification events *) 545 + | Key_verification_ready | Key_verification_start | Key_verification_accept 546 + | Key_verification_key | Key_verification_mac | Key_verification_cancel 547 + | Key_verification_done 548 + (* Policy rule events *) 549 + | Policy_rule_room | Policy_rule_server | Policy_rule_user 550 + (* To-device events *) 551 + | Room_key | Room_key_request | Forwarded_room_key | Dummy 552 + (* Ephemeral *) 553 + | Typing | Receipt | Presence 554 + (* Account data *) 555 + | Direct | Ignored_user_list | Fully_read | Marked_unread | Tag | Push_rules 556 + | Secret_storage_default_key | Secret_storage_key | Cross_signing_keys 557 + (* Beacon/location events *) 558 + | Beacon_info | Beacon 559 + (* Polls *) 560 + | Poll_start | Poll_response | Poll_end 561 + (* Custom *) 562 + | Custom of string 563 + val to_string : t -> string 564 + val of_string : string -> t 565 + val jsont : t Jsont.t 566 + end 567 + 568 + (** {1 State Events} *) 569 + 570 + module State_event : sig 571 + type 'content t = { 572 + event_id : Event_id.t; 573 + sender : User_id.t; 574 + origin_server_ts : Timestamp.t; 575 + state_key : string; 576 + type_ : Event_type.t; 577 + content : 'content; 578 + unsigned : Unsigned.t option; 579 + } 580 + val make_jsont : 'content Jsont.t -> 'content t Jsont.t 581 + end 582 + 583 + (** {1 Room Events} *) 584 + 585 + module Room_event : sig 586 + type 'content t = { 587 + event_id : Event_id.t; 588 + sender : User_id.t; 589 + origin_server_ts : Timestamp.t; 590 + type_ : Event_type.t; 591 + content : 'content; 592 + unsigned : Unsigned.t option; 593 + } 594 + val make_jsont : 'content Jsont.t -> 'content t Jsont.t 595 + end 596 + 597 + (** {1 Raw Events} *) 598 + 599 + module Raw_event : sig 600 + type t = { 601 + event_id : Event_id.t option; 602 + sender : User_id.t; 603 + origin_server_ts : Timestamp.t; 604 + type_ : Event_type.t; 605 + state_key : string option; 606 + content : Jsont.json; 607 + unsigned : Unsigned.t option; 608 + room_id : Room_id.t option; 609 + } 610 + val jsont : t Jsont.t 611 + end
+421
lib/matrix_proto/matrix_id.ml
··· 1 + (** Matrix identifiers with validation and JSON codecs. 2 + 3 + Matrix uses several types of identifiers that follow specific formats 4 + as defined in the Matrix specification: 5 + 6 + - {b User IDs}: [@localpart:server_name] - Identify users uniquely 7 + - {b Room IDs}: [!opaque_id:server_name] - Identify rooms uniquely 8 + - {b Event IDs}: [$opaque_id] or [$opaque_id:server_name] - Identify events 9 + - {b Room Aliases}: [#alias:server_name] - Human-readable room references 10 + - {b Device IDs}: Opaque strings identifying client devices 11 + 12 + All identifiers are case-sensitive. Server names follow DNS syntax 13 + with optional port numbers. 14 + 15 + @see <https://spec.matrix.org/v1.11/appendices/#identifier-grammar> Identifier Grammar 16 + @see <https://spec.matrix.org/v1.11/appendices/#common-identifier-format> Common Identifier Format *) 17 + 18 + (** {1 Server Names} 19 + 20 + Server names identify Matrix homeservers and follow a DNS-like format. 21 + They consist of a hostname and optional port, e.g., [matrix.org] or 22 + [matrix.org:8448]. 23 + 24 + @see <https://spec.matrix.org/v1.11/appendices/#server-name> Server Name *) 25 + 26 + module Server_name = struct 27 + (** A Matrix server name (hostname with optional port). 28 + 29 + Server names are used as the domain part of user IDs, room IDs, and 30 + other identifiers. They follow DNS hostname rules with an optional 31 + port number suffix. *) 32 + type t = string 33 + 34 + (** Check if a character is valid in a server name. *) 35 + let is_valid_char = function 36 + | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '-' | '.' | ':' | '[' | ']' -> true 37 + | _ -> false 38 + 39 + (** Check if a string is a valid server name. *) 40 + let is_valid s = 41 + String.length s > 0 && String.for_all is_valid_char s 42 + 43 + (** Parse a server name from a string. 44 + @return [Ok t] if valid, [Error `Invalid_server_name] otherwise *) 45 + let of_string s = 46 + if is_valid s then Ok s 47 + else Error (`Invalid_server_name s) 48 + 49 + (** Parse a server name, raising [Invalid_argument] on failure. *) 50 + let of_string_exn s = 51 + match of_string s with 52 + | Ok t -> t 53 + | Error (`Invalid_server_name s) -> 54 + invalid_arg (Printf.sprintf "Invalid server name: %s" s) 55 + 56 + (** Convert to string representation. *) 57 + let to_string t = t 58 + 59 + (** JSON codec for server names. *) 60 + let jsont = 61 + Jsont.of_of_string ~kind:"server_name" 62 + ~enc:to_string 63 + (fun s -> Result.map_error (fun (`Invalid_server_name s) -> s) (of_string s)) 64 + end 65 + 66 + (** {1 User IDs} 67 + 68 + User IDs uniquely identify Matrix users. They have the format 69 + [@localpart:server_name], where the localpart consists of lowercase 70 + letters, digits, and certain special characters. 71 + 72 + @see <https://spec.matrix.org/v1.11/appendices/#user-identifiers> User Identifiers *) 73 + 74 + module User_id = struct 75 + (** A Matrix user ID (e.g., [@alice:matrix.org]). 76 + 77 + User IDs consist of a sigil [@], a localpart, a colon, and a server name. 78 + The localpart is case-sensitive and may contain: [a-z0-9._=\-/+] *) 79 + type t = { localpart : string; server_name : Server_name.t } 80 + 81 + (** The user ID sigil character. *) 82 + let sigil = '@' 83 + 84 + (** Check if a character is valid in a user ID localpart. 85 + Valid characters are lowercase letters, digits, and: [._=\-/+] *) 86 + let is_valid_localpart_char = function 87 + | 'a' .. 'z' | '0' .. '9' | '.' | '_' | '=' | '-' | '/' | '+' -> true 88 + | _ -> false 89 + 90 + (** Parse a user ID from its string representation. 91 + @return [Ok t] if valid, [Error `Invalid_user_id reason] otherwise *) 92 + let of_string s = 93 + if String.length s < 2 then 94 + Error (`Invalid_user_id "too short") 95 + else if s.[0] <> sigil then 96 + Error (`Invalid_user_id "must start with @") 97 + else 98 + match String.index_opt s ':' with 99 + | None -> Error (`Invalid_user_id "missing colon separator") 100 + | Some colon_pos -> 101 + let localpart = String.sub s 1 (colon_pos - 1) in 102 + let server_part = String.sub s (colon_pos + 1) (String.length s - colon_pos - 1) in 103 + if String.length localpart = 0 then 104 + Error (`Invalid_user_id "empty localpart") 105 + else if not (String.for_all is_valid_localpart_char localpart) then 106 + Error (`Invalid_user_id "invalid characters in localpart") 107 + else 108 + match Server_name.of_string server_part with 109 + | Error _ -> Error (`Invalid_user_id "invalid server name") 110 + | Ok server_name -> Ok { localpart; server_name } 111 + 112 + (** Parse a user ID, raising [Invalid_argument] on failure. *) 113 + let of_string_exn s = 114 + match of_string s with 115 + | Ok t -> t 116 + | Error (`Invalid_user_id msg) -> 117 + invalid_arg (Printf.sprintf "Invalid user ID '%s': %s" s msg) 118 + 119 + (** Convert to the canonical string representation. *) 120 + let to_string { localpart; server_name } = 121 + Printf.sprintf "%c%s:%s" sigil localpart (Server_name.to_string server_name) 122 + 123 + (** Get the localpart (the part before the colon, without the sigil). *) 124 + let localpart t = t.localpart 125 + 126 + (** Get the server name (the part after the colon). *) 127 + let server_name t = t.server_name 128 + 129 + (** JSON codec for user IDs. *) 130 + let jsont = 131 + Jsont.of_of_string ~kind:"user_id" 132 + ~enc:to_string 133 + (fun s -> Result.map_error (fun (`Invalid_user_id msg) -> msg) (of_string s)) 134 + end 135 + 136 + (** {1 Room IDs} 137 + 138 + Room IDs uniquely identify Matrix rooms. They have the format 139 + [!opaque_id:server_name], where the opaque_id is generated by 140 + the creating server. 141 + 142 + @see <https://spec.matrix.org/v1.11/appendices/#room-ids> Room IDs *) 143 + 144 + module Room_id = struct 145 + (** A Matrix room ID (e.g., [!abcdef:matrix.org]). 146 + 147 + Room IDs are assigned by the server that creates the room and 148 + are globally unique. The opaque_id portion is server-generated. *) 149 + type t = { opaque_id : string; server_name : Server_name.t } 150 + 151 + (** The room ID sigil character. *) 152 + let sigil = '!' 153 + 154 + (** Parse a room ID from its string representation. 155 + @return [Ok t] if valid, [Error `Invalid_room_id reason] otherwise *) 156 + let of_string s = 157 + if String.length s < 2 then 158 + Error (`Invalid_room_id "too short") 159 + else if s.[0] <> sigil then 160 + Error (`Invalid_room_id "must start with !") 161 + else 162 + match String.index_opt s ':' with 163 + | None -> Error (`Invalid_room_id "missing colon separator") 164 + | Some colon_pos -> 165 + let opaque_id = String.sub s 1 (colon_pos - 1) in 166 + let server_part = String.sub s (colon_pos + 1) (String.length s - colon_pos - 1) in 167 + match Server_name.of_string server_part with 168 + | Error _ -> Error (`Invalid_room_id "invalid server name") 169 + | Ok server_name -> Ok { opaque_id; server_name } 170 + 171 + (** Parse a room ID, raising [Invalid_argument] on failure. *) 172 + let of_string_exn s = 173 + match of_string s with 174 + | Ok t -> t 175 + | Error (`Invalid_room_id msg) -> 176 + invalid_arg (Printf.sprintf "Invalid room ID '%s': %s" s msg) 177 + 178 + (** Convert to the canonical string representation. *) 179 + let to_string { opaque_id; server_name } = 180 + Printf.sprintf "%c%s:%s" sigil opaque_id (Server_name.to_string server_name) 181 + 182 + (** Get the opaque ID portion (server-generated, without sigil). *) 183 + let opaque_id t = t.opaque_id 184 + 185 + (** Get the server name of the room's creating server. *) 186 + let server_name t = t.server_name 187 + 188 + (** JSON codec for room IDs. *) 189 + let jsont = 190 + Jsont.of_of_string ~kind:"room_id" 191 + ~enc:to_string 192 + (fun s -> Result.map_error (fun (`Invalid_room_id msg) -> msg) (of_string s)) 193 + end 194 + 195 + (** {1 Event IDs} 196 + 197 + Event IDs uniquely identify events within Matrix. The format varies 198 + by room version: 199 + - Versions 1-3: [$opaque_id:server_name] 200 + - Version 4+: [$base64_opaque_id] (no server name, uses reference hashing) 201 + 202 + @see <https://spec.matrix.org/v1.11/appendices/#event-ids> Event IDs 203 + @see <https://spec.matrix.org/v1.11/rooms/> Room Versions *) 204 + 205 + module Event_id = struct 206 + (** A Matrix event ID. 207 + 208 + Event IDs can be in two formats depending on the room version: 209 + - {b V1}: [$opaque_id:server_name] (room versions 1-3) 210 + - {b V4}: [$base64_opaque_id] (room version 4+, using reference hashing) *) 211 + type t = 212 + | V1 of { opaque_id : string; server_name : Server_name.t } 213 + | V4 of { opaque_id : string } 214 + 215 + (** The event ID sigil character. *) 216 + let sigil = '$' 217 + 218 + (** Parse an event ID from its string representation. 219 + Automatically detects V1 vs V4 format based on presence of colon. 220 + @return [Ok t] if valid, [Error `Invalid_event_id reason] otherwise *) 221 + let of_string s = 222 + if String.length s < 2 then 223 + Error (`Invalid_event_id "too short") 224 + else if s.[0] <> sigil then 225 + Error (`Invalid_event_id "must start with $") 226 + else 227 + let rest = String.sub s 1 (String.length s - 1) in 228 + match String.index_opt rest ':' with 229 + | None -> 230 + (* V4+ format: no server name *) 231 + Ok (V4 { opaque_id = rest }) 232 + | Some colon_pos -> 233 + let opaque_id = String.sub rest 0 colon_pos in 234 + let server_part = String.sub rest (colon_pos + 1) (String.length rest - colon_pos - 1) in 235 + match Server_name.of_string server_part with 236 + | Error _ -> Error (`Invalid_event_id "invalid server name") 237 + | Ok server_name -> Ok (V1 { opaque_id; server_name }) 238 + 239 + (** Parse an event ID, raising [Invalid_argument] on failure. *) 240 + let of_string_exn s = 241 + match of_string s with 242 + | Ok t -> t 243 + | Error (`Invalid_event_id msg) -> 244 + invalid_arg (Printf.sprintf "Invalid event ID '%s': %s" s msg) 245 + 246 + (** Convert to the canonical string representation. *) 247 + let to_string = function 248 + | V1 { opaque_id; server_name } -> 249 + Printf.sprintf "%c%s:%s" sigil opaque_id (Server_name.to_string server_name) 250 + | V4 { opaque_id } -> 251 + Printf.sprintf "%c%s" sigil opaque_id 252 + 253 + (** JSON codec for event IDs. *) 254 + let jsont = 255 + Jsont.of_of_string ~kind:"event_id" 256 + ~enc:to_string 257 + (fun s -> Result.map_error (fun (`Invalid_event_id msg) -> msg) (of_string s)) 258 + end 259 + 260 + (** {1 Room Aliases} 261 + 262 + Room aliases are human-readable identifiers that point to room IDs. 263 + They have the format [#alias:server_name]. Unlike room IDs, aliases 264 + can be created and deleted, and multiple aliases can point to the 265 + same room. 266 + 267 + @see <https://spec.matrix.org/v1.11/appendices/#room-aliases> Room Aliases *) 268 + 269 + module Room_alias = struct 270 + (** A Matrix room alias (e.g., [#general:matrix.org]). 271 + 272 + Room aliases provide human-readable names for rooms. They are 273 + managed by the server in the alias's domain and can be changed 274 + over time. *) 275 + type t = { alias : string; server_name : Server_name.t } 276 + 277 + (** The room alias sigil character. *) 278 + let sigil = '#' 279 + 280 + (** Parse a room alias from its string representation. 281 + @return [Ok t] if valid, [Error `Invalid_room_alias reason] otherwise *) 282 + let of_string s = 283 + if String.length s < 2 then 284 + Error (`Invalid_room_alias "too short") 285 + else if s.[0] <> sigil then 286 + Error (`Invalid_room_alias "must start with #") 287 + else 288 + match String.index_opt s ':' with 289 + | None -> Error (`Invalid_room_alias "missing colon separator") 290 + | Some colon_pos -> 291 + let alias = String.sub s 1 (colon_pos - 1) in 292 + let server_part = String.sub s (colon_pos + 1) (String.length s - colon_pos - 1) in 293 + match Server_name.of_string server_part with 294 + | Error _ -> Error (`Invalid_room_alias "invalid server name") 295 + | Ok server_name -> Ok { alias; server_name } 296 + 297 + (** Parse a room alias, raising [Invalid_argument] on failure. *) 298 + let of_string_exn s = 299 + match of_string s with 300 + | Ok t -> t 301 + | Error (`Invalid_room_alias msg) -> 302 + invalid_arg (Printf.sprintf "Invalid room alias '%s': %s" s msg) 303 + 304 + (** Convert to the canonical string representation. *) 305 + let to_string { alias; server_name } = 306 + Printf.sprintf "%c%s:%s" sigil alias (Server_name.to_string server_name) 307 + 308 + (** Get the alias portion (without the sigil). *) 309 + let alias t = t.alias 310 + 311 + (** Get the server that manages this alias. *) 312 + let server_name t = t.server_name 313 + 314 + (** JSON codec for room aliases. *) 315 + let jsont = 316 + Jsont.of_of_string ~kind:"room_alias" 317 + ~enc:to_string 318 + (fun s -> Result.map_error (fun (`Invalid_room_alias msg) -> msg) (of_string s)) 319 + end 320 + 321 + (** {1 Device IDs} 322 + 323 + Device IDs identify client devices for end-to-end encryption and 324 + session management. They are opaque strings assigned by the server 325 + or provided by the client during login. 326 + 327 + @see <https://spec.matrix.org/v1.11/client-server-api/#device-management> Device Management *) 328 + 329 + module Device_id = struct 330 + (** A Matrix device ID. 331 + 332 + Device IDs are opaque strings that identify a specific client 333 + device. They are used for E2EE key management and session tracking. *) 334 + type t = string 335 + 336 + (** Parse a device ID from a string. Device IDs cannot be empty. *) 337 + let of_string s = 338 + if String.length s = 0 then 339 + Error (`Invalid_device_id "empty") 340 + else 341 + Ok s 342 + 343 + (** Parse a device ID, raising [Invalid_argument] on failure. *) 344 + let of_string_exn s = 345 + match of_string s with 346 + | Ok t -> t 347 + | Error (`Invalid_device_id msg) -> 348 + invalid_arg (Printf.sprintf "Invalid device ID: %s" msg) 349 + 350 + (** Convert to string representation. *) 351 + let to_string t = t 352 + 353 + (** JSON codec for device IDs. *) 354 + let jsont = Jsont.string 355 + end 356 + 357 + (** {1 Session IDs} 358 + 359 + Session IDs identify Megolm encryption sessions used for 360 + encrypted group messaging. 361 + 362 + @see <https://spec.matrix.org/v1.11/client-server-api/#messaging-algorithm-megolm> Megolm *) 363 + 364 + module Session_id = struct 365 + (** A Megolm session ID. 366 + 367 + Session IDs identify outbound Megolm sessions used to encrypt 368 + messages in encrypted rooms. *) 369 + type t = string 370 + 371 + (** Parse a session ID from a string. Session IDs cannot be empty. *) 372 + let of_string s = 373 + if String.length s = 0 then 374 + Error (`Invalid_session_id "empty") 375 + else 376 + Ok s 377 + 378 + (** Convert to string representation. *) 379 + let to_string t = t 380 + 381 + (** JSON codec for session IDs. *) 382 + let jsont = Jsont.string 383 + end 384 + 385 + (** {1 Transaction IDs} 386 + 387 + Transaction IDs are client-generated identifiers used for 388 + idempotent request handling. If a request with the same transaction 389 + ID is sent multiple times, the server will only process it once. 390 + 391 + @see <https://spec.matrix.org/v1.11/client-server-api/#transaction-identifiers> Transaction Identifiers *) 392 + 393 + module Transaction_id = struct 394 + (** A client-generated transaction ID. 395 + 396 + Transaction IDs ensure idempotency for state-changing requests. 397 + The same transaction ID should be used when retrying a request 398 + to prevent duplicate actions. *) 399 + type t = string 400 + 401 + (** Generate a new random transaction ID. 402 + 403 + Uses 16 random bytes encoded as hexadecimal. *) 404 + let generate () = 405 + let random_bytes = Bytes.create 16 in 406 + for i = 0 to 15 do 407 + Bytes.set random_bytes i (Char.chr (Random.int 256)) 408 + done; 409 + let buf = Buffer.create 32 in 410 + Bytes.iter (fun c -> Buffer.add_string buf (Printf.sprintf "%02x" (Char.code c))) random_bytes; 411 + Buffer.contents buf 412 + 413 + (** Create a transaction ID from an existing string. *) 414 + let of_string s = s 415 + 416 + (** Convert to string representation. *) 417 + let to_string t = t 418 + 419 + (** JSON codec for transaction IDs. *) 420 + let jsont = Jsont.string 421 + end
+113
lib/matrix_proto/matrix_id.mli
··· 1 + (** Matrix identifiers with validation and JSON codecs. 2 + 3 + Matrix uses several types of identifiers that follow specific formats. 4 + All identifiers are case-sensitive. *) 5 + 6 + (** {1 Server Names} *) 7 + 8 + module Server_name : sig 9 + type t 10 + 11 + val of_string : string -> (t, [> `Invalid_server_name of string ]) result 12 + val of_string_exn : string -> t 13 + val to_string : t -> string 14 + val jsont : t Jsont.t 15 + end 16 + 17 + (** {1 User IDs} 18 + 19 + User IDs have the format [@localpart:server_name]. *) 20 + 21 + module User_id : sig 22 + type t 23 + 24 + val of_string : string -> (t, [> `Invalid_user_id of string ]) result 25 + val of_string_exn : string -> t 26 + val to_string : t -> string 27 + val localpart : t -> string 28 + val server_name : t -> Server_name.t 29 + val jsont : t Jsont.t 30 + end 31 + 32 + (** {1 Room IDs} 33 + 34 + Room IDs have the format [!opaque_id:server_name]. *) 35 + 36 + module Room_id : sig 37 + type t 38 + 39 + val of_string : string -> (t, [> `Invalid_room_id of string ]) result 40 + val of_string_exn : string -> t 41 + val to_string : t -> string 42 + val opaque_id : t -> string 43 + val server_name : t -> Server_name.t 44 + val jsont : t Jsont.t 45 + end 46 + 47 + (** {1 Event IDs} 48 + 49 + Event IDs can be either: 50 + - Version 1-3: [$opaque_id:server_name] 51 + - Version 4+: [$base64_opaque_id] (no server name) *) 52 + 53 + module Event_id : sig 54 + type t 55 + 56 + val of_string : string -> (t, [> `Invalid_event_id of string ]) result 57 + val of_string_exn : string -> t 58 + val to_string : t -> string 59 + val jsont : t Jsont.t 60 + end 61 + 62 + (** {1 Room Aliases} 63 + 64 + Room aliases have the format [#alias:server_name]. *) 65 + 66 + module Room_alias : sig 67 + type t 68 + 69 + val of_string : string -> (t, [> `Invalid_room_alias of string ]) result 70 + val of_string_exn : string -> t 71 + val to_string : t -> string 72 + val alias : t -> string 73 + val server_name : t -> Server_name.t 74 + val jsont : t Jsont.t 75 + end 76 + 77 + (** {1 Device IDs} 78 + 79 + Device IDs are opaque strings. *) 80 + 81 + module Device_id : sig 82 + type t 83 + 84 + val of_string : string -> (t, [> `Invalid_device_id of string ]) result 85 + val of_string_exn : string -> t 86 + val to_string : t -> string 87 + val jsont : t Jsont.t 88 + end 89 + 90 + (** {1 Session IDs} 91 + 92 + Megolm session IDs. *) 93 + 94 + module Session_id : sig 95 + type t 96 + 97 + val of_string : string -> (t, [> `Invalid_session_id of string ]) result 98 + val to_string : t -> string 99 + val jsont : t Jsont.t 100 + end 101 + 102 + (** {1 Transaction IDs} 103 + 104 + Client-generated transaction IDs for idempotency. *) 105 + 106 + module Transaction_id : sig 107 + type t 108 + 109 + val generate : unit -> t 110 + val of_string : string -> t 111 + val to_string : t -> string 112 + val jsont : t Jsont.t 113 + end
+15
lib/matrix_proto/matrix_proto.ml
··· 1 + (** Matrix protocol types with JSON codecs. 2 + 3 + This library provides comprehensive OCaml types and bidirectional JSON 4 + codecs for the Matrix protocol. All codecs are built using the jsont 5 + library for type-safe encoding and decoding. 6 + 7 + {1 Modules} 8 + 9 + - {!Matrix_id}: Matrix identifiers (User_id, Room_id, Event_id, etc.) 10 + - {!Matrix_event}: Event types and content structures 11 + - {!Matrix_sync}: Sync API response types *) 12 + 13 + module Id = Matrix_id 14 + module Event = Matrix_event 15 + module Sync = Matrix_sync
+312
lib/matrix_proto/matrix_sync.ml
··· 1 + (** Matrix sync API response types with JSON codecs. 2 + 3 + The sync API is the core of Matrix client communication. This module 4 + provides types for the complete sync response structure. *) 5 + 6 + open Matrix_id 7 + open Matrix_event 8 + 9 + (** {1 Timeline} *) 10 + 11 + module Timeline = struct 12 + type t = { 13 + events : Raw_event.t list; 14 + limited : bool option; 15 + prev_batch : string option; 16 + } 17 + 18 + let jsont = 19 + Jsont.Object.( 20 + map (fun events limited prev_batch -> { events; limited; prev_batch }) 21 + |> mem "events" (Jsont.list Raw_event.jsont) ~dec_absent:[] 22 + ~enc:(fun t -> t.events) 23 + |> opt_mem "limited" Jsont.bool ~enc:(fun t -> t.limited) 24 + |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch) 25 + |> finish) 26 + end 27 + 28 + (** {1 Ephemeral Events} *) 29 + 30 + module Ephemeral = struct 31 + type t = { 32 + events : Jsont.json list; 33 + } 34 + 35 + let jsont = 36 + Jsont.Object.( 37 + map (fun events -> { events }) 38 + |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.events) 39 + |> finish) 40 + end 41 + 42 + (** {1 Account Data} *) 43 + 44 + module Account_data = struct 45 + type t = { 46 + events : Jsont.json list; 47 + } 48 + 49 + let jsont = 50 + Jsont.Object.( 51 + map (fun events -> { events }) 52 + |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.events) 53 + |> finish) 54 + end 55 + 56 + (** {1 Room State} *) 57 + 58 + module Room_state = struct 59 + type t = { 60 + events : Raw_event.t list; 61 + } 62 + 63 + let jsont = 64 + Jsont.Object.( 65 + map (fun events -> { events }) 66 + |> mem "events" (Jsont.list Raw_event.jsont) ~dec_absent:[] 67 + ~enc:(fun t -> t.events) 68 + |> finish) 69 + end 70 + 71 + (** {1 Unread Notification Counts} *) 72 + 73 + module Unread_notification_counts = struct 74 + type t = { 75 + highlight_count : int option; 76 + notification_count : int option; 77 + } 78 + 79 + let jsont = 80 + Jsont.Object.( 81 + map (fun highlight_count notification_count -> 82 + { highlight_count; notification_count }) 83 + |> opt_mem "highlight_count" Jsont.int ~enc:(fun t -> t.highlight_count) 84 + |> opt_mem "notification_count" Jsont.int ~enc:(fun t -> t.notification_count) 85 + |> finish) 86 + end 87 + 88 + (** {1 Room Summary} *) 89 + 90 + module Room_summary = struct 91 + type t = { 92 + heroes : User_id.t list option; 93 + joined_member_count : int option; 94 + invited_member_count : int option; 95 + } 96 + 97 + let jsont = 98 + Jsont.Object.( 99 + map (fun heroes joined_member_count invited_member_count -> 100 + { heroes; joined_member_count; invited_member_count }) 101 + |> opt_mem "m.heroes" (Jsont.list User_id.jsont) ~enc:(fun t -> t.heroes) 102 + |> opt_mem "m.joined_member_count" Jsont.int ~enc:(fun t -> t.joined_member_count) 103 + |> opt_mem "m.invited_member_count" Jsont.int ~enc:(fun t -> t.invited_member_count) 104 + |> finish) 105 + end 106 + 107 + (** {1 Joined Room} *) 108 + 109 + module Joined_room = struct 110 + type t = { 111 + summary : Room_summary.t option; 112 + state : Room_state.t option; 113 + timeline : Timeline.t option; 114 + ephemeral : Ephemeral.t option; 115 + account_data : Account_data.t option; 116 + unread_notifications : Unread_notification_counts.t option; 117 + } 118 + 119 + let jsont = 120 + Jsont.Object.( 121 + map (fun summary state timeline ephemeral account_data unread_notifications -> 122 + { summary; state; timeline; ephemeral; account_data; unread_notifications }) 123 + |> opt_mem "summary" Room_summary.jsont ~enc:(fun t -> t.summary) 124 + |> opt_mem "state" Room_state.jsont ~enc:(fun t -> t.state) 125 + |> opt_mem "timeline" Timeline.jsont ~enc:(fun t -> t.timeline) 126 + |> opt_mem "ephemeral" Ephemeral.jsont ~enc:(fun t -> t.ephemeral) 127 + |> opt_mem "account_data" Account_data.jsont ~enc:(fun t -> t.account_data) 128 + |> opt_mem "unread_notifications" Unread_notification_counts.jsont ~enc:(fun t -> t.unread_notifications) 129 + |> finish) 130 + end 131 + 132 + (** {1 Invited Room} *) 133 + 134 + module Invited_room = struct 135 + type t = { 136 + invite_state : invite_state option; 137 + } 138 + and invite_state = { 139 + events : Jsont.json list; 140 + } 141 + 142 + let invite_state_jsont = 143 + Jsont.Object.( 144 + map (fun events -> { events }) 145 + |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun s -> s.events) 146 + |> finish) 147 + 148 + let jsont = 149 + Jsont.Object.( 150 + map (fun invite_state -> { invite_state }) 151 + |> opt_mem "invite_state" invite_state_jsont ~enc:(fun t -> t.invite_state) 152 + |> finish) 153 + end 154 + 155 + (** {1 Left Room} *) 156 + 157 + module Left_room = struct 158 + type t = { 159 + state : Room_state.t option; 160 + timeline : Timeline.t option; 161 + account_data : Account_data.t option; 162 + } 163 + 164 + let jsont = 165 + Jsont.Object.( 166 + map (fun state timeline account_data -> { state; timeline; account_data }) 167 + |> opt_mem "state" Room_state.jsont ~enc:(fun t -> t.state) 168 + |> opt_mem "timeline" Timeline.jsont ~enc:(fun t -> t.timeline) 169 + |> opt_mem "account_data" Account_data.jsont ~enc:(fun t -> t.account_data) 170 + |> finish) 171 + end 172 + 173 + (** {1 Knocked Room} *) 174 + 175 + module Knocked_room = struct 176 + type t = { 177 + knock_state : knock_state option; 178 + } 179 + and knock_state = { 180 + events : Jsont.json list; 181 + } 182 + 183 + let knock_state_jsont = 184 + Jsont.Object.( 185 + map (fun events -> { events }) 186 + |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun s -> s.events) 187 + |> finish) 188 + 189 + let jsont = 190 + Jsont.Object.( 191 + map (fun knock_state -> { knock_state }) 192 + |> opt_mem "knock_state" knock_state_jsont ~enc:(fun t -> t.knock_state) 193 + |> finish) 194 + end 195 + 196 + (** {1 Rooms} *) 197 + 198 + module Rooms = struct 199 + type t = { 200 + join : (string * Joined_room.t) list; 201 + invite : (string * Invited_room.t) list; 202 + leave : (string * Left_room.t) list; 203 + knock : (string * Knocked_room.t) list; 204 + } 205 + 206 + module StringMap = Map.Make(String) 207 + 208 + let room_map_jsont jsont = 209 + Jsont.Object.as_string_map jsont 210 + |> Jsont.map 211 + ~dec:(fun m -> StringMap.bindings m) 212 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 213 + 214 + let jsont = 215 + Jsont.Object.( 216 + map (fun join invite leave knock -> { join; invite; leave; knock }) 217 + |> mem "join" (room_map_jsont Joined_room.jsont) ~dec_absent:[] 218 + ~enc:(fun t -> t.join) 219 + |> mem "invite" (room_map_jsont Invited_room.jsont) ~dec_absent:[] 220 + ~enc:(fun t -> t.invite) 221 + |> mem "leave" (room_map_jsont Left_room.jsont) ~dec_absent:[] 222 + ~enc:(fun t -> t.leave) 223 + |> mem "knock" (room_map_jsont Knocked_room.jsont) ~dec_absent:[] 224 + ~enc:(fun t -> t.knock) 225 + |> finish) 226 + end 227 + 228 + (** {1 Device Lists} *) 229 + 230 + module Device_lists = struct 231 + type t = { 232 + changed : User_id.t list; 233 + left : User_id.t list; 234 + } 235 + 236 + let jsont = 237 + Jsont.Object.( 238 + map (fun changed left -> { changed; left }) 239 + |> mem "changed" (Jsont.list User_id.jsont) ~dec_absent:[] ~enc:(fun t -> t.changed) 240 + |> mem "left" (Jsont.list User_id.jsont) ~dec_absent:[] ~enc:(fun t -> t.left) 241 + |> finish) 242 + end 243 + 244 + (** {1 To-Device Events} *) 245 + 246 + module To_device = struct 247 + type t = { 248 + events : Jsont.json list; 249 + } 250 + 251 + let jsont = 252 + Jsont.Object.( 253 + map (fun events -> { events }) 254 + |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.events) 255 + |> finish) 256 + end 257 + 258 + (** {1 Presence} *) 259 + 260 + module Presence = struct 261 + type t = { 262 + events : Jsont.json list; 263 + } 264 + 265 + let jsont = 266 + Jsont.Object.( 267 + map (fun events -> { events }) 268 + |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.events) 269 + |> finish) 270 + end 271 + 272 + (** {1 Sync Response} *) 273 + 274 + module Response = struct 275 + type t = { 276 + next_batch : string; 277 + rooms : Rooms.t option; 278 + presence : Presence.t option; 279 + account_data : Account_data.t option; 280 + to_device : To_device.t option; 281 + device_lists : Device_lists.t option; 282 + device_one_time_keys_count : (string * int) list; 283 + device_unused_fallback_key_types : string list option; 284 + } 285 + 286 + module StringMap = Map.Make(String) 287 + 288 + let int_map_jsont = 289 + Jsont.Object.as_string_map Jsont.int 290 + |> Jsont.map 291 + ~dec:(fun m -> StringMap.bindings m) 292 + ~enc:(fun l -> List.to_seq l |> StringMap.of_seq) 293 + 294 + let jsont = 295 + Jsont.Object.( 296 + map (fun next_batch rooms presence account_data to_device 297 + device_lists device_one_time_keys_count 298 + device_unused_fallback_key_types -> 299 + { next_batch; rooms; presence; account_data; to_device; 300 + device_lists; device_one_time_keys_count; 301 + device_unused_fallback_key_types }) 302 + |> mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch) 303 + |> opt_mem "rooms" Rooms.jsont ~enc:(fun t -> t.rooms) 304 + |> opt_mem "presence" Presence.jsont ~enc:(fun t -> t.presence) 305 + |> opt_mem "account_data" Account_data.jsont ~enc:(fun t -> t.account_data) 306 + |> opt_mem "to_device" To_device.jsont ~enc:(fun t -> t.to_device) 307 + |> opt_mem "device_lists" Device_lists.jsont ~enc:(fun t -> t.device_lists) 308 + |> mem "device_one_time_keys_count" int_map_jsont ~dec_absent:[] 309 + ~enc:(fun t -> t.device_one_time_keys_count) 310 + |> opt_mem "device_unused_fallback_key_types" (Jsont.list Jsont.string) ~enc:(fun t -> t.device_unused_fallback_key_types) 311 + |> finish) 312 + end
+153
lib/matrix_proto/matrix_sync.mli
··· 1 + (** Matrix sync API response types with JSON codecs. 2 + 3 + The sync API is the core of Matrix client communication. This module 4 + provides types for the complete sync response structure. *) 5 + 6 + open Matrix_id 7 + open Matrix_event 8 + 9 + (** {1 Timeline} *) 10 + 11 + module Timeline : sig 12 + type t = { 13 + events : Raw_event.t list; 14 + limited : bool option; 15 + prev_batch : string option; 16 + } 17 + val jsont : t Jsont.t 18 + end 19 + 20 + (** {1 Ephemeral Events} *) 21 + 22 + module Ephemeral : sig 23 + type t = { events : Jsont.json list } 24 + val jsont : t Jsont.t 25 + end 26 + 27 + (** {1 Account Data} *) 28 + 29 + module Account_data : sig 30 + type t = { events : Jsont.json list } 31 + val jsont : t Jsont.t 32 + end 33 + 34 + (** {1 Room State} *) 35 + 36 + module Room_state : sig 37 + type t = { events : Raw_event.t list } 38 + val jsont : t Jsont.t 39 + end 40 + 41 + (** {1 Unread Notification Counts} *) 42 + 43 + module Unread_notification_counts : sig 44 + type t = { 45 + highlight_count : int option; 46 + notification_count : int option; 47 + } 48 + val jsont : t Jsont.t 49 + end 50 + 51 + (** {1 Room Summary} *) 52 + 53 + module Room_summary : sig 54 + type t = { 55 + heroes : User_id.t list option; 56 + joined_member_count : int option; 57 + invited_member_count : int option; 58 + } 59 + val jsont : t Jsont.t 60 + end 61 + 62 + (** {1 Joined Room} *) 63 + 64 + module Joined_room : sig 65 + type t = { 66 + summary : Room_summary.t option; 67 + state : Room_state.t option; 68 + timeline : Timeline.t option; 69 + ephemeral : Ephemeral.t option; 70 + account_data : Account_data.t option; 71 + unread_notifications : Unread_notification_counts.t option; 72 + } 73 + val jsont : t Jsont.t 74 + end 75 + 76 + (** {1 Invited Room} *) 77 + 78 + module Invited_room : sig 79 + type t = { invite_state : invite_state option } 80 + and invite_state = { events : Jsont.json list } 81 + val jsont : t Jsont.t 82 + end 83 + 84 + (** {1 Left Room} *) 85 + 86 + module Left_room : sig 87 + type t = { 88 + state : Room_state.t option; 89 + timeline : Timeline.t option; 90 + account_data : Account_data.t option; 91 + } 92 + val jsont : t Jsont.t 93 + end 94 + 95 + (** {1 Knocked Room} *) 96 + 97 + module Knocked_room : sig 98 + type t = { knock_state : knock_state option } 99 + and knock_state = { events : Jsont.json list } 100 + val jsont : t Jsont.t 101 + end 102 + 103 + (** {1 Rooms} *) 104 + 105 + module Rooms : sig 106 + type t = { 107 + join : (string * Joined_room.t) list; 108 + invite : (string * Invited_room.t) list; 109 + leave : (string * Left_room.t) list; 110 + knock : (string * Knocked_room.t) list; 111 + } 112 + val jsont : t Jsont.t 113 + end 114 + 115 + (** {1 Device Lists} *) 116 + 117 + module Device_lists : sig 118 + type t = { 119 + changed : User_id.t list; 120 + left : User_id.t list; 121 + } 122 + val jsont : t Jsont.t 123 + end 124 + 125 + (** {1 To-Device Events} *) 126 + 127 + module To_device : sig 128 + type t = { events : Jsont.json list } 129 + val jsont : t Jsont.t 130 + end 131 + 132 + (** {1 Presence} *) 133 + 134 + module Presence : sig 135 + type t = { events : Jsont.json list } 136 + val jsont : t Jsont.t 137 + end 138 + 139 + (** {1 Sync Response} *) 140 + 141 + module Response : sig 142 + type t = { 143 + next_batch : string; 144 + rooms : Rooms.t option; 145 + presence : Presence.t option; 146 + account_data : Account_data.t option; 147 + to_device : To_device.t option; 148 + device_lists : Device_lists.t option; 149 + device_one_time_keys_count : (string * int) list; 150 + device_unused_fallback_key_types : string list option; 151 + } 152 + val jsont : t Jsont.t 153 + end
+37
matrix_client.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Matrix client SDK for OCaml" 4 + description: 5 + "Full Matrix client SDK using requests for HTTP and jsont for JSON" 6 + maintainer: ["dev@matrix.org"] 7 + authors: ["OCaml Matrix Contributors"] 8 + license: "Apache-2.0" 9 + homepage: "https://github.com/matrix-org/ocaml-matrix" 10 + bug-reports: "https://github.com/matrix-org/ocaml-matrix/issues" 11 + depends: [ 12 + "dune" {>= "3.20"} 13 + "ocaml" {>= "5.1"} 14 + "matrix_proto" 15 + "requests" 16 + "jsont" 17 + "uri" 18 + "eio" 19 + "ptime" 20 + "odoc" {with-doc} 21 + ] 22 + build: [ 23 + ["dune" "subst"] {dev} 24 + [ 25 + "dune" 26 + "build" 27 + "-p" 28 + name 29 + "-j" 30 + jobs 31 + "@install" 32 + "@runtest" {with-test} 33 + "@doc" {with-doc} 34 + ] 35 + ] 36 + dev-repo: "git+https://github.com/matrix-org/ocaml-matrix.git" 37 + x-maintenance-intent: ["(latest)"]
+35
matrix_eio.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Eio-idiomatic Matrix client SDK" 4 + description: 5 + "Matrix client SDK using Eio idioms: switches for resource management, Eio.Io for errors, fibres for concurrency" 6 + maintainer: ["dev@matrix.org"] 7 + authors: ["OCaml Matrix Contributors"] 8 + license: "Apache-2.0" 9 + homepage: "https://github.com/matrix-org/ocaml-matrix" 10 + bug-reports: "https://github.com/matrix-org/ocaml-matrix/issues" 11 + depends: [ 12 + "dune" {>= "3.20"} 13 + "ocaml" {>= "5.1"} 14 + "matrix_client" 15 + "matrix_proto" 16 + "eio" 17 + "uri" 18 + "odoc" {with-doc} 19 + ] 20 + build: [ 21 + ["dune" "subst"] {dev} 22 + [ 23 + "dune" 24 + "build" 25 + "-p" 26 + name 27 + "-j" 28 + jobs 29 + "@install" 30 + "@runtest" {with-test} 31 + "@doc" {with-doc} 32 + ] 33 + ] 34 + dev-repo: "git+https://github.com/matrix-org/ocaml-matrix.git" 35 + x-maintenance-intent: ["(latest)"]
+34
matrix_proto.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Matrix protocol types with JSON codecs" 4 + description: 5 + "OCaml types for Matrix protocol with bidirectional JSON encoding/decoding using jsont" 6 + maintainer: ["dev@matrix.org"] 7 + authors: ["OCaml Matrix Contributors"] 8 + license: "Apache-2.0" 9 + homepage: "https://github.com/matrix-org/ocaml-matrix" 10 + bug-reports: "https://github.com/matrix-org/ocaml-matrix/issues" 11 + depends: [ 12 + "dune" {>= "3.20"} 13 + "ocaml" {>= "5.1"} 14 + "jsont" 15 + "ptime" 16 + "alcotest" {with-test} 17 + "odoc" {with-doc} 18 + ] 19 + build: [ 20 + ["dune" "subst"] {dev} 21 + [ 22 + "dune" 23 + "build" 24 + "-p" 25 + name 26 + "-j" 27 + jobs 28 + "@install" 29 + "@runtest" {with-test} 30 + "@doc" {with-doc} 31 + ] 32 + ] 33 + dev-repo: "git+https://github.com/matrix-org/ocaml-matrix.git" 34 + x-maintenance-intent: ["(latest)"]
+31
ocaml-matrix.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Pure OCaml Matrix SDK" 4 + description: "A pure OCaml implementation of the Matrix client SDK" 5 + maintainer: ["dev@matrix.org"] 6 + authors: ["OCaml Matrix Contributors"] 7 + license: "Apache-2.0" 8 + homepage: "https://github.com/matrix-org/ocaml-matrix" 9 + bug-reports: "https://github.com/matrix-org/ocaml-matrix/issues" 10 + depends: [ 11 + "dune" {>= "3.20"} 12 + "ocaml" {>= "5.1"} 13 + "matrix_proto" 14 + "odoc" {with-doc} 15 + ] 16 + build: [ 17 + ["dune" "subst"] {dev} 18 + [ 19 + "dune" 20 + "build" 21 + "-p" 22 + name 23 + "-j" 24 + jobs 25 + "@install" 26 + "@runtest" {with-test} 27 + "@doc" {with-doc} 28 + ] 29 + ] 30 + dev-repo: "git+https://github.com/matrix-org/ocaml-matrix.git" 31 + x-maintenance-intent: ["(latest)"]
+10
test/dune
··· 1 + (executable 2 + (name test_matrix_proto) 3 + (libraries matrix_proto jsont jsont.bytesrw)) 4 + 5 + (rule 6 + (alias runtest) 7 + (deps 8 + (glob_files fixtures/*.json) 9 + test_matrix_proto.exe) 10 + (action (run ./test_matrix_proto.exe)))
+60
test/fixtures/identifiers.json
··· 1 + { 2 + "user_ids": { 3 + "valid": [ 4 + "@alice:example.org", 5 + "@bob:matrix.org", 6 + "@user123:localhost", 7 + "@test.user:server.com", 8 + "@admin:192.168.1.1:8448" 9 + ], 10 + "invalid": [ 11 + "alice:example.org", 12 + "@", 13 + "@alice", 14 + "@@alice:example.org", 15 + "@alice:", 16 + "@:example.org" 17 + ] 18 + }, 19 + "room_ids": { 20 + "valid": [ 21 + "!SVkFJHzfwvuaIEawgC:localhost", 22 + "!opaqueid:matrix.org", 23 + "!abc123:server.example.com" 24 + ], 25 + "invalid": [ 26 + "SVkFJHzfwvuaIEawgC:localhost", 27 + "!", 28 + "!SVkFJHzfwvuaIEawgC", 29 + "!:localhost" 30 + ] 31 + }, 32 + "event_ids": { 33 + "valid_v1": [ 34 + "$152037280074GZeOm:localhost", 35 + "$event123:matrix.org" 36 + ], 37 + "valid_v4": [ 38 + "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg", 39 + "$base64opaqueid" 40 + ], 41 + "invalid": [ 42 + "152037280074GZeOm:localhost", 43 + "$", 44 + "" 45 + ] 46 + }, 47 + "room_aliases": { 48 + "valid": [ 49 + "#test:localhost", 50 + "#general:matrix.org", 51 + "#room-name:server.com" 52 + ], 53 + "invalid": [ 54 + "test:localhost", 55 + "#", 56 + "#test", 57 + "#:localhost" 58 + ] 59 + } 60 + }
+135
test/fixtures/message_events.json
··· 1 + { 2 + "text_message": { 3 + "content": { 4 + "body": "Hello, world!", 5 + "msgtype": "m.text" 6 + }, 7 + "event_id": "$152037280074GZeOm:localhost", 8 + "origin_server_ts": 152037280000000, 9 + "sender": "@example:localhost", 10 + "type": "m.room.message" 11 + }, 12 + "text_message_formatted": { 13 + "content": { 14 + "body": "Hello, **world**!", 15 + "msgtype": "m.text", 16 + "format": "org.matrix.custom.html", 17 + "formatted_body": "Hello, <strong>world</strong>!" 18 + }, 19 + "event_id": "$152037280075AbCdE:localhost", 20 + "origin_server_ts": 152037280100000, 21 + "sender": "@example:localhost", 22 + "type": "m.room.message" 23 + }, 24 + "emote_message": { 25 + "content": { 26 + "body": "waves hello", 27 + "msgtype": "m.emote" 28 + }, 29 + "event_id": "$152037280076FgHiJ:localhost", 30 + "origin_server_ts": 152037280200000, 31 + "sender": "@example:localhost", 32 + "type": "m.room.message" 33 + }, 34 + "notice_message": { 35 + "content": { 36 + "body": "This is a notice", 37 + "msgtype": "m.notice" 38 + }, 39 + "event_id": "$152037280077KlMnO:localhost", 40 + "origin_server_ts": 152037280300000, 41 + "sender": "@bot:localhost", 42 + "type": "m.room.message" 43 + }, 44 + "image_message": { 45 + "content": { 46 + "body": "image.png", 47 + "msgtype": "m.image", 48 + "url": "mxc://localhost/abcdef123456", 49 + "info": { 50 + "mimetype": "image/png", 51 + "size": 12345, 52 + "h": 480, 53 + "w": 640, 54 + "thumbnail_url": "mxc://localhost/thumb123", 55 + "thumbnail_info": { 56 + "mimetype": "image/png", 57 + "size": 1234, 58 + "h": 48, 59 + "w": 64 60 + } 61 + } 62 + }, 63 + "event_id": "$152037280078PqRsT:localhost", 64 + "origin_server_ts": 152037280400000, 65 + "sender": "@example:localhost", 66 + "type": "m.room.message" 67 + }, 68 + "file_message": { 69 + "content": { 70 + "body": "document.pdf", 71 + "msgtype": "m.file", 72 + "url": "mxc://localhost/file789", 73 + "info": { 74 + "mimetype": "application/pdf", 75 + "size": 54321 76 + } 77 + }, 78 + "event_id": "$152037280079UvWxY:localhost", 79 + "origin_server_ts": 152037280500000, 80 + "sender": "@example:localhost", 81 + "type": "m.room.message" 82 + }, 83 + "audio_message": { 84 + "content": { 85 + "body": "audio.mp3", 86 + "msgtype": "m.audio", 87 + "url": "mxc://localhost/audio456", 88 + "info": { 89 + "mimetype": "audio/mpeg", 90 + "size": 98765, 91 + "duration": 180000 92 + } 93 + }, 94 + "event_id": "$152037280080ZaBcD:localhost", 95 + "origin_server_ts": 152037280600000, 96 + "sender": "@example:localhost", 97 + "type": "m.room.message" 98 + }, 99 + "video_message": { 100 + "content": { 101 + "body": "video.mp4", 102 + "msgtype": "m.video", 103 + "url": "mxc://localhost/video789", 104 + "info": { 105 + "mimetype": "video/mp4", 106 + "size": 1234567, 107 + "duration": 60000, 108 + "h": 720, 109 + "w": 1280, 110 + "thumbnail_url": "mxc://localhost/vthumb123", 111 + "thumbnail_info": { 112 + "mimetype": "image/jpeg", 113 + "size": 5678, 114 + "h": 72, 115 + "w": 128 116 + } 117 + } 118 + }, 119 + "event_id": "$152037280081EfGhI:localhost", 120 + "origin_server_ts": 152037280700000, 121 + "sender": "@example:localhost", 122 + "type": "m.room.message" 123 + }, 124 + "location_message": { 125 + "content": { 126 + "body": "My Location", 127 + "msgtype": "m.location", 128 + "geo_uri": "geo:51.5074,-0.1278" 129 + }, 130 + "event_id": "$152037280082JkLmN:localhost", 131 + "origin_server_ts": 152037280800000, 132 + "sender": "@example:localhost", 133 + "type": "m.room.message" 134 + } 135 + }
+124
test/fixtures/state_events.json
··· 1 + { 2 + "room_create": { 3 + "content": { 4 + "creator": "@example:localhost", 5 + "room_version": "10" 6 + }, 7 + "event_id": "$15139375510KUZHi:localhost", 8 + "origin_server_ts": 151393755000000, 9 + "sender": "@example:localhost", 10 + "state_key": "", 11 + "type": "m.room.create" 12 + }, 13 + "room_name": { 14 + "content": { 15 + "name": "Test Room" 16 + }, 17 + "event_id": "$15139375513VdeRF:localhost", 18 + "origin_server_ts": 151393755000000, 19 + "sender": "@example:localhost", 20 + "state_key": "", 21 + "type": "m.room.name" 22 + }, 23 + "room_topic": { 24 + "content": { 25 + "topic": "This is a test room" 26 + }, 27 + "event_id": "$151957878228ssqrJ:localhost", 28 + "origin_server_ts": 151957878000000, 29 + "sender": "@example:localhost", 30 + "state_key": "", 31 + "type": "m.room.topic" 32 + }, 33 + "room_member_join": { 34 + "content": { 35 + "avatar_url": "mxc://localhost/wefuiwegh8742w", 36 + "displayname": "Example User", 37 + "membership": "join" 38 + }, 39 + "event_id": "$151800140517rfvjc:localhost", 40 + "origin_server_ts": 151800140000000, 41 + "sender": "@example:localhost", 42 + "state_key": "@example:localhost", 43 + "type": "m.room.member" 44 + }, 45 + "room_member_invite": { 46 + "content": { 47 + "displayname": "Another User", 48 + "membership": "invite" 49 + }, 50 + "event_id": "$151800140517xyza:localhost", 51 + "origin_server_ts": 151800141000000, 52 + "sender": "@example:localhost", 53 + "state_key": "@another:localhost", 54 + "type": "m.room.member" 55 + }, 56 + "room_join_rules": { 57 + "content": { 58 + "join_rule": "public" 59 + }, 60 + "event_id": "$15139375514WsgmR:localhost", 61 + "origin_server_ts": 151393755000000, 62 + "sender": "@example:localhost", 63 + "state_key": "", 64 + "type": "m.room.join_rules" 65 + }, 66 + "room_history_visibility": { 67 + "content": { 68 + "history_visibility": "shared" 69 + }, 70 + "event_id": "$15139375515VaJEY:localhost", 71 + "origin_server_ts": 151393755000000, 72 + "sender": "@example:localhost", 73 + "state_key": "", 74 + "type": "m.room.history_visibility" 75 + }, 76 + "room_power_levels": { 77 + "content": { 78 + "ban": 50, 79 + "events": { 80 + "m.room.name": 50, 81 + "m.room.power_levels": 100 82 + }, 83 + "events_default": 0, 84 + "invite": 0, 85 + "kick": 50, 86 + "redact": 50, 87 + "state_default": 50, 88 + "users": { 89 + "@example:localhost": 100 90 + }, 91 + "users_default": 0 92 + }, 93 + "event_id": "$15139375517ZvWRR:localhost", 94 + "origin_server_ts": 151393755000000, 95 + "sender": "@example:localhost", 96 + "state_key": "", 97 + "type": "m.room.power_levels" 98 + }, 99 + "room_canonical_alias": { 100 + "content": { 101 + "alias": "#test:localhost", 102 + "alt_aliases": [ 103 + "#other:localhost" 104 + ] 105 + }, 106 + "event_id": "$15139375518AbCdE:localhost", 107 + "origin_server_ts": 151393755000000, 108 + "sender": "@example:localhost", 109 + "state_key": "", 110 + "type": "m.room.canonical_alias" 111 + }, 112 + "room_encryption": { 113 + "content": { 114 + "algorithm": "m.megolm.v1.aes-sha2", 115 + "rotation_period_ms": 604800000, 116 + "rotation_period_msgs": 100 117 + }, 118 + "event_id": "$15139375519FgHiJ:localhost", 119 + "origin_server_ts": 151393755000000, 120 + "sender": "@example:localhost", 121 + "state_key": "", 122 + "type": "m.room.encryption" 123 + } 124 + }
+185
test/fixtures/sync_response.json
··· 1 + { 2 + "device_one_time_keys_count": {}, 3 + "next_batch": "s526_47314_0_7_1_1_1_11444_1", 4 + "device_lists": { 5 + "changed": [ 6 + "@example:example.org" 7 + ], 8 + "left": [] 9 + }, 10 + "account_data": { 11 + "events": [ 12 + { 13 + "content": { 14 + "ignored_users": { 15 + "@someone:example.org": {} 16 + } 17 + }, 18 + "type": "m.ignored_user_list" 19 + } 20 + ] 21 + }, 22 + "rooms": { 23 + "invite": {}, 24 + "join": { 25 + "!SVkFJHzfwvuaIEawgC:localhost": { 26 + "summary": {}, 27 + "account_data": { 28 + "events": [ 29 + { 30 + "content": { 31 + "event_id": "$someplace:example.org" 32 + }, 33 + "room_id": "!roomid:room.com", 34 + "type": "m.fully_read" 35 + } 36 + ] 37 + }, 38 + "ephemeral": { 39 + "events": [ 40 + { 41 + "content": { 42 + "$151680659217152dPKjd:localhost": { 43 + "m.read": { 44 + "@example:localhost": { 45 + "ts": 151680989 46 + } 47 + } 48 + } 49 + }, 50 + "room_id": "!SVkFJHzfwvuaIEawgC:localhost", 51 + "type": "m.receipt" 52 + } 53 + ] 54 + }, 55 + "state": { 56 + "events": [ 57 + { 58 + "content": { 59 + "join_rule": "public" 60 + }, 61 + "event_id": "$15139375514WsgmR:localhost", 62 + "origin_server_ts": 151393755000000, 63 + "sender": "@example:localhost", 64 + "state_key": "", 65 + "type": "m.room.join_rules", 66 + "unsigned": { 67 + "age": 7034220 68 + } 69 + }, 70 + { 71 + "content": { 72 + "avatar_url": null, 73 + "displayname": "example", 74 + "membership": "join" 75 + }, 76 + "event_id": "$151800140517rfvjc:localhost", 77 + "origin_server_ts": 151800140000000, 78 + "sender": "@example:localhost", 79 + "state_key": "@example:localhost", 80 + "type": "m.room.member", 81 + "unsigned": { 82 + "age": 2970366 83 + } 84 + }, 85 + { 86 + "content": { 87 + "history_visibility": "shared" 88 + }, 89 + "event_id": "$15139375515VaJEY:localhost", 90 + "origin_server_ts": 151393755000000, 91 + "sender": "@example:localhost", 92 + "state_key": "", 93 + "type": "m.room.history_visibility", 94 + "unsigned": { 95 + "age": 7034220 96 + } 97 + }, 98 + { 99 + "content": { 100 + "creator": "@example:localhost" 101 + }, 102 + "event_id": "$15139375510KUZHi:localhost", 103 + "origin_server_ts": 151393755000000, 104 + "sender": "@example:localhost", 105 + "state_key": "", 106 + "type": "m.room.create", 107 + "unsigned": { 108 + "age": 703422 109 + } 110 + }, 111 + { 112 + "content": { 113 + "topic": "room topic" 114 + }, 115 + "event_id": "$151957878228ssqrJ:localhost", 116 + "origin_server_ts": 151957878000000, 117 + "sender": "@example:localhost", 118 + "state_key": "", 119 + "type": "m.room.topic", 120 + "unsigned": { 121 + "age": 1392989709 122 + } 123 + }, 124 + { 125 + "content": { 126 + "name": "Test Room" 127 + }, 128 + "event_id": "$15139375513VdeRF:localhost", 129 + "origin_server_ts": 151393755000000, 130 + "sender": "@example:localhost", 131 + "state_key": "", 132 + "type": "m.room.name", 133 + "unsigned": { 134 + "age": 703422 135 + } 136 + } 137 + ] 138 + }, 139 + "timeline": { 140 + "events": [ 141 + { 142 + "content": { 143 + "body": "Hello, world!", 144 + "msgtype": "m.text" 145 + }, 146 + "event_id": "$152037280074GZeOm:localhost", 147 + "origin_server_ts": 152037280000000, 148 + "sender": "@example:localhost", 149 + "type": "m.room.message", 150 + "unsigned": { 151 + "age": 598971425 152 + } 153 + } 154 + ], 155 + "limited": true, 156 + "prev_batch": "t392-516_47314_0_7_1_1_1_11444_1" 157 + }, 158 + "unread_notifications": { 159 + "highlight_count": 0, 160 + "notification_count": 1 161 + } 162 + } 163 + }, 164 + "leave": {}, 165 + "knock": {} 166 + }, 167 + "presence": { 168 + "events": [ 169 + { 170 + "content": { 171 + "avatar_url": "mxc://localhost/wefuiwegh8742w", 172 + "currently_active": false, 173 + "last_active_ago": 2478593, 174 + "presence": "online", 175 + "status_msg": "Making cupcakes" 176 + }, 177 + "sender": "@example:localhost", 178 + "type": "m.presence" 179 + } 180 + ] 181 + }, 182 + "to_device": { 183 + "events": [] 184 + } 185 + }
+551
test/test_matrix_proto.ml
··· 1 + (** Tests for matrix_proto library using roundtrip encoding/decoding. *) 2 + 3 + open Matrix_proto 4 + 5 + (** {1 Test Framework} *) 6 + 7 + let tests_run = ref 0 8 + let tests_passed = ref 0 9 + let tests_failed = ref 0 10 + 11 + let test name f = 12 + incr tests_run; 13 + Printf.printf " %s... " name; 14 + flush stdout; 15 + try 16 + f (); 17 + incr tests_passed; 18 + Printf.printf "OK\n" 19 + with e -> 20 + incr tests_failed; 21 + Printf.printf "FAIL: %s\n" (Printexc.to_string e) 22 + 23 + let check_true msg b = 24 + if not b then failwith msg 25 + 26 + let check_eq pp msg a b = 27 + if a <> b then 28 + failwith (Printf.sprintf "%s: expected %s, got %s" msg (pp a) (pp b)) 29 + 30 + let group name tests = 31 + Printf.printf "\n%s:\n" name; 32 + List.iter (fun (n, f) -> test n f) tests 33 + 34 + (** {1 Helpers} *) 35 + 36 + let read_fixture name = 37 + let path = "fixtures/" ^ name in 38 + In_channel.with_open_bin path In_channel.input_all 39 + 40 + let roundtrip_test jsont json_str = 41 + match Jsont_bytesrw.decode_string jsont json_str with 42 + | Error e -> failwith ("decode: " ^ e) 43 + | Ok value -> 44 + match Jsont_bytesrw.encode_string jsont value with 45 + | Error e -> failwith ("encode: " ^ e) 46 + | Ok encoded -> 47 + match Jsont_bytesrw.decode_string jsont encoded with 48 + | Error e -> failwith ("re-decode: " ^ e) 49 + | Ok _ -> () 50 + 51 + (** {1 Identifier Tests} *) 52 + 53 + let identifier_tests = [ 54 + "user_id valid", (fun () -> 55 + let ids = ["@alice:example.org"; "@bob:matrix.org"; "@user123:localhost"] in 56 + List.iter (fun id -> 57 + match Id.User_id.of_string id with 58 + | Ok uid -> 59 + check_eq Fun.id "roundtrip" id (Id.User_id.to_string uid) 60 + | Error _ -> failwith ("expected valid: " ^ id) 61 + ) ids 62 + ); 63 + 64 + "user_id invalid", (fun () -> 65 + let ids = ["alice:example.org"; "@"; "@alice"; "@:example.org"] in 66 + List.iter (fun id -> 67 + match Id.User_id.of_string id with 68 + | Ok _ -> failwith ("expected invalid: " ^ id) 69 + | Error _ -> () 70 + ) ids 71 + ); 72 + 73 + "room_id valid", (fun () -> 74 + let ids = ["!SVkFJHzfwvuaIEawgC:localhost"; "!opaqueid:matrix.org"] in 75 + List.iter (fun id -> 76 + match Id.Room_id.of_string id with 77 + | Ok rid -> 78 + check_eq Fun.id "roundtrip" id (Id.Room_id.to_string rid) 79 + | Error _ -> failwith ("expected valid: " ^ id) 80 + ) ids 81 + ); 82 + 83 + "event_id v1", (fun () -> 84 + let id = "$152037280074GZeOm:localhost" in 85 + match Id.Event_id.of_string id with 86 + | Ok eid -> 87 + check_eq Fun.id "roundtrip" id (Id.Event_id.to_string eid) 88 + | Error _ -> failwith "expected valid" 89 + ); 90 + 91 + "event_id v4", (fun () -> 92 + let id = "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" in 93 + match Id.Event_id.of_string id with 94 + | Ok eid -> 95 + check_eq Fun.id "roundtrip" id (Id.Event_id.to_string eid) 96 + | Error _ -> failwith "expected valid" 97 + ); 98 + 99 + "room_alias valid", (fun () -> 100 + let ids = ["#test:localhost"; "#general:matrix.org"] in 101 + List.iter (fun id -> 102 + match Id.Room_alias.of_string id with 103 + | Ok alias -> 104 + check_eq Fun.id "roundtrip" id (Id.Room_alias.to_string alias) 105 + | Error _ -> failwith ("expected valid: " ^ id) 106 + ) ids 107 + ); 108 + 109 + "transaction_id unique", (fun () -> 110 + let txn1 = Id.Transaction_id.generate () in 111 + let txn2 = Id.Transaction_id.generate () in 112 + check_true "should be unique" 113 + (Id.Transaction_id.to_string txn1 <> Id.Transaction_id.to_string txn2) 114 + ); 115 + ] 116 + 117 + (** {1 Event Content Tests} *) 118 + 119 + let event_content_tests = [ 120 + "room_create_content", (fun () -> 121 + roundtrip_test Event.Room_create_content.jsont 122 + {|{"creator": "@example:localhost", "room_version": "10"}|} 123 + ); 124 + 125 + "room_name_content", (fun () -> 126 + roundtrip_test Event.Room_name_content.jsont 127 + {|{"name": "Test Room"}|} 128 + ); 129 + 130 + "room_topic_content", (fun () -> 131 + roundtrip_test Event.Room_topic_content.jsont 132 + {|{"topic": "This is a test room"}|} 133 + ); 134 + 135 + "room_avatar_content", (fun () -> 136 + roundtrip_test Event.Room_avatar_content.jsont 137 + {|{"url": "mxc://localhost/abc123", "info": {"h": 480, "w": 640, "mimetype": "image/png", "size": 12345}}|} 138 + ); 139 + 140 + "room_member_content join", (fun () -> 141 + roundtrip_test Event.Room_member_content.jsont 142 + {|{"membership": "join", "displayname": "Example User"}|} 143 + ); 144 + 145 + "room_member_content invite", (fun () -> 146 + roundtrip_test Event.Room_member_content.jsont 147 + {|{"membership": "invite"}|} 148 + ); 149 + 150 + "room_member_content leave", (fun () -> 151 + roundtrip_test Event.Room_member_content.jsont 152 + {|{"membership": "leave", "reason": "Goodbye!"}|} 153 + ); 154 + 155 + "room_member_content ban", (fun () -> 156 + roundtrip_test Event.Room_member_content.jsont 157 + {|{"membership": "ban", "reason": "Violated rules"}|} 158 + ); 159 + 160 + "room_join_rules public", (fun () -> 161 + roundtrip_test Event.Room_join_rules_content.jsont 162 + {|{"join_rule": "public"}|} 163 + ); 164 + 165 + "room_join_rules restricted", (fun () -> 166 + roundtrip_test Event.Room_join_rules_content.jsont 167 + {|{"join_rule": "restricted", "allow": [{"type": "m.room_membership", "room_id": "!other:localhost"}]}|} 168 + ); 169 + 170 + "room_history_visibility", (fun () -> 171 + roundtrip_test Event.Room_history_visibility_content.jsont 172 + {|{"history_visibility": "shared"}|} 173 + ); 174 + 175 + "room_power_levels", (fun () -> 176 + roundtrip_test Event.Room_power_levels_content.jsont 177 + {|{"ban": 50, "events": {"m.room.name": 50}, "users": {"@example:localhost": 100}}|} 178 + ); 179 + 180 + "room_canonical_alias", (fun () -> 181 + roundtrip_test Event.Room_canonical_alias_content.jsont 182 + {|{"alias": "#test:localhost", "alt_aliases": ["#other:localhost"]}|} 183 + ); 184 + 185 + "room_encryption", (fun () -> 186 + roundtrip_test Event.Room_encryption_content.jsont 187 + {|{"algorithm": "m.megolm.v1.aes-sha2"}|} 188 + ); 189 + 190 + "room_encryption with rotation", (fun () -> 191 + roundtrip_test Event.Room_encryption_content.jsont 192 + {|{"algorithm": "m.megolm.v1.aes-sha2", "rotation_period_ms": 604800000, "rotation_period_msgs": 100}|} 193 + ); 194 + 195 + "room_pinned_events", (fun () -> 196 + roundtrip_test Event.Room_pinned_events_content.jsont 197 + {|{"pinned": ["$event1:localhost", "$event2:localhost"]}|} 198 + ); 199 + 200 + "room_server_acl", (fun () -> 201 + roundtrip_test Event.Room_server_acl_content.jsont 202 + {|{"allow": ["*"], "allow_ip_literals": false, "deny": ["evil.server"]}|} 203 + ); 204 + 205 + "room_tombstone", (fun () -> 206 + roundtrip_test Event.Room_tombstone_content.jsont 207 + {|{"body": "Room has been upgraded", "replacement_room": "!newroom:localhost"}|} 208 + ); 209 + 210 + "room_guest_access", (fun () -> 211 + roundtrip_test Event.Room_guest_access_content.jsont 212 + {|{"guest_access": "can_join"}|} 213 + ); 214 + 215 + "text_message simple", (fun () -> 216 + roundtrip_test Event.Text_message_content.jsont 217 + {|{"body": "Hello, world!", "msgtype": "m.text"}|} 218 + ); 219 + 220 + "text_message formatted", (fun () -> 221 + roundtrip_test Event.Text_message_content.jsont 222 + {|{"body": "Hello", "msgtype": "m.text", "format": "org.matrix.custom.html", "formatted_body": "<b>Hello</b>"}|} 223 + ); 224 + 225 + "emote_message", (fun () -> 226 + roundtrip_test Event.Text_message_content.jsont 227 + {|{"body": "waves", "msgtype": "m.emote"}|} 228 + ); 229 + 230 + "notice_message", (fun () -> 231 + roundtrip_test Event.Text_message_content.jsont 232 + {|{"body": "Notice", "msgtype": "m.notice"}|} 233 + ); 234 + 235 + "image_message", (fun () -> 236 + roundtrip_test Event.Media_message_content.jsont 237 + {|{"body": "image.png", "msgtype": "m.image", "url": "mxc://localhost/abc123", "info": {"mimetype": "image/png", "size": 12345, "h": 480, "w": 640}}|} 238 + ); 239 + 240 + "file_message", (fun () -> 241 + roundtrip_test Event.Media_message_content.jsont 242 + {|{"body": "document.pdf", "msgtype": "m.file", "url": "mxc://localhost/file789", "info": {"mimetype": "application/pdf", "size": 54321}}|} 243 + ); 244 + 245 + "audio_message", (fun () -> 246 + roundtrip_test Event.Media_message_content.jsont 247 + {|{"body": "audio.mp3", "msgtype": "m.audio", "url": "mxc://localhost/audio456", "info": {"mimetype": "audio/mpeg", "size": 98765, "duration": 180000}}|} 248 + ); 249 + 250 + "video_message", (fun () -> 251 + roundtrip_test Event.Media_message_content.jsont 252 + {|{"body": "video.mp4", "msgtype": "m.video", "url": "mxc://localhost/video789", "info": {"mimetype": "video/mp4", "size": 1234567, "duration": 60000, "h": 720, "w": 1280}}|} 253 + ); 254 + 255 + "location_message", (fun () -> 256 + roundtrip_test Event.Location_message_content.jsont 257 + {|{"body": "My Location", "msgtype": "m.location", "geo_uri": "geo:51.5074,-0.1278"}|} 258 + ); 259 + 260 + "sticker", (fun () -> 261 + roundtrip_test Event.Sticker_content.jsont 262 + {|{"body": "sticker", "url": "mxc://localhost/sticker123", "info": {"mimetype": "image/png", "size": 1234, "h": 128, "w": 128}}|} 263 + ); 264 + ] 265 + 266 + (** {1 Space Content Tests} *) 267 + 268 + let space_content_tests = [ 269 + "space_child", (fun () -> 270 + roundtrip_test Event.Space_child_content.jsont 271 + {|{"via": ["matrix.org"], "order": "a", "suggested": true}|} 272 + ); 273 + 274 + "space_parent", (fun () -> 275 + roundtrip_test Event.Space_parent_content.jsont 276 + {|{"via": ["matrix.org"], "canonical": true}|} 277 + ); 278 + ] 279 + 280 + (** {1 Call Content Tests} *) 281 + 282 + let call_content_tests = [ 283 + "call_invite", (fun () -> 284 + roundtrip_test Event.Call_invite_content.jsont 285 + {|{"call_id": "12345", "version": 1, "lifetime": 60000, "offer": {"type": "offer", "sdp": "v=0..."}}|} 286 + ); 287 + 288 + "call_answer", (fun () -> 289 + roundtrip_test Event.Call_answer_content.jsont 290 + {|{"call_id": "12345", "version": 1, "answer": {"type": "answer", "sdp": "v=0..."}}|} 291 + ); 292 + 293 + "call_hangup", (fun () -> 294 + roundtrip_test Event.Call_hangup_content.jsont 295 + {|{"call_id": "12345", "version": 1, "reason": "user_hangup"}|} 296 + ); 297 + 298 + "call_candidates", (fun () -> 299 + roundtrip_test Event.Call_candidates_content.jsont 300 + {|{"call_id": "12345", "version": 1, "candidates": [{"candidate": "candidate:...", "sdpMid": "0", "sdpMLineIndex": 0}]}|} 301 + ); 302 + 303 + "call_member", (fun () -> 304 + roundtrip_test Event.Call_member_content.jsont 305 + {|{"memberships": [{"call_id": "call1", "scope": "m.room", "application": "m.call", "device_id": "DEVICE1", "expires": 3600000}]}|} 306 + ); 307 + ] 308 + 309 + (** {1 Key Verification Content Tests} *) 310 + 311 + let key_verification_tests = [ 312 + "key_verification_ready", (fun () -> 313 + roundtrip_test Event.Key_verification_ready_content.jsont 314 + {|{"from_device": "DEVICE1", "methods": ["m.sas.v1", "m.qr_code.show.v1"]}|} 315 + ); 316 + 317 + "key_verification_start", (fun () -> 318 + roundtrip_test Event.Key_verification_start_content.jsont 319 + {|{"from_device": "DEVICE1", "method": "m.sas.v1", "key_agreement_protocols": ["curve25519-hkdf-sha256"], "hashes": ["sha256"], "message_authentication_codes": ["hkdf-hmac-sha256"], "short_authentication_string": ["decimal", "emoji"]}|} 320 + ); 321 + 322 + "key_verification_accept", (fun () -> 323 + roundtrip_test Event.Key_verification_accept_content.jsont 324 + {|{"method": "m.sas.v1", "key_agreement_protocol": "curve25519-hkdf-sha256", "hash": "sha256", "message_authentication_code": "hkdf-hmac-sha256", "short_authentication_string": ["decimal", "emoji"], "commitment": "fQpGIW1Snz+pwLZu6sMy2nF92MYN89TDAhYhPxVvBZc"}|} 325 + ); 326 + 327 + "key_verification_key", (fun () -> 328 + roundtrip_test Event.Key_verification_key_content.jsont 329 + {|{"key": "fQpGIW1Snz+pwLZu6sMy2nF92MYN89TDAhYhPxVvBZc"}|} 330 + ); 331 + 332 + "key_verification_mac", (fun () -> 333 + roundtrip_test Event.Key_verification_mac_content.jsont 334 + {|{"mac": {"ed25519:DEVICEID": "fQpGIW1Snz+pwLZu6sMy2nF92MYN89TDAhYhPxVvBZc"}, "keys": "fQpGIW1Snz+pwLZu6sMy2nF92MYN89TDAhYhPxVvBZc"}|} 335 + ); 336 + 337 + "key_verification_cancel", (fun () -> 338 + roundtrip_test Event.Key_verification_cancel_content.jsont 339 + {|{"code": "m.user", "reason": "User cancelled"}|} 340 + ); 341 + 342 + "key_verification_done", (fun () -> 343 + roundtrip_test Event.Key_verification_done_content.jsont 344 + {|{}|} 345 + ); 346 + ] 347 + 348 + (** {1 Policy Rule Content Tests} *) 349 + 350 + let policy_rule_tests = [ 351 + "policy_rule_ban", (fun () -> 352 + roundtrip_test Event.Policy_rule_content.jsont 353 + {|{"entity": "@spam:*", "reason": "Spamming", "recommendation": "m.ban"}|} 354 + ); 355 + 356 + "policy_rule_unknown", (fun () -> 357 + roundtrip_test Event.Policy_rule_content.jsont 358 + {|{"entity": "evil.server", "reason": "Known bad actor", "recommendation": "org.custom.action"}|} 359 + ); 360 + ] 361 + 362 + (** {1 Account Data Content Tests} *) 363 + 364 + let account_data_tests = [ 365 + "marked_unread", (fun () -> 366 + roundtrip_test Event.Marked_unread_content.jsont 367 + {|{"unread": true}|} 368 + ); 369 + 370 + "marked_unread_false", (fun () -> 371 + roundtrip_test Event.Marked_unread_content.jsont 372 + {|{"unread": false}|} 373 + ); 374 + ] 375 + 376 + (** {1 Encrypted Content Tests} *) 377 + 378 + let encrypted_content_tests = [ 379 + "megolm_encrypted", (fun () -> 380 + roundtrip_test Event.Encrypted_content.jsont 381 + {|{"algorithm": "m.megolm.v1.aes-sha2-256", "sender_key": "abc123", "ciphertext": "encrypted...", "session_id": "session123", "device_id": "DEVICE"}|} 382 + ); 383 + 384 + "olm_encrypted", (fun () -> 385 + roundtrip_test Event.Encrypted_content.jsont 386 + {|{"algorithm": "m.olm.v1.curve25519-aes-sha2-256", "sender_key": "abc123", "ciphertext": {"DEVICE": {"type": 0, "body": "encrypted..."}}}|} 387 + ); 388 + ] 389 + 390 + (** {1 Reaction Content Tests} *) 391 + 392 + let reaction_content_tests = [ 393 + "reaction", (fun () -> 394 + roundtrip_test Event.Reaction_content.jsont 395 + {|{"m.relates_to": {"rel_type": "m.annotation", "event_id": "$event:localhost", "key": "👍"}}|} 396 + ); 397 + ] 398 + 399 + (** {1 Beacon Content Tests} *) 400 + 401 + let beacon_content_tests = [ 402 + "beacon_info", (fun () -> 403 + roundtrip_test Event.Beacon_info_content.jsont 404 + {|{"live": true, "timeout": 300000}|} 405 + ); 406 + 407 + "beacon", (fun () -> 408 + roundtrip_test Event.Beacon_content.jsont 409 + {|{"org.matrix.msc3488.location": {"uri": "geo:51.5074,-0.1278"}, "org.matrix.msc3488.ts": 1234567890000, "m.relates_to": {"rel_type": "m.reference", "event_id": "$beacon:localhost"}}|} 410 + ); 411 + ] 412 + 413 + (** {1 Poll Content Tests} *) 414 + 415 + let poll_content_tests = [ 416 + "poll_start", (fun () -> 417 + roundtrip_test Event.Poll_start_content.jsont 418 + {|{"org.matrix.msc3381.poll.start": {"question": "What is your favorite color?", "kind": "org.matrix.msc3381.poll.disclosed", "max_selections": 1, "answers": [{"id": "1", "org.matrix.msc1767.text": "Red"}, {"id": "2", "org.matrix.msc1767.text": "Blue"}]}, "org.matrix.msc1767.text": "What is your favorite color?"}|} 419 + ); 420 + 421 + "poll_response", (fun () -> 422 + roundtrip_test Event.Poll_response_content.jsont 423 + {|{"m.relates_to": {"rel_type": "m.reference", "event_id": "$poll:localhost"}, "org.matrix.msc3381.poll.response": ["1"]}|} 424 + ); 425 + 426 + "poll_end", (fun () -> 427 + roundtrip_test Event.Poll_end_content.jsont 428 + {|{"m.relates_to": {"rel_type": "m.reference", "event_id": "$poll:localhost"}, "org.matrix.msc1767.text": "Poll ended"}|} 429 + ); 430 + ] 431 + 432 + (** {1 Raw Event Tests} *) 433 + 434 + let raw_event_tests = [ 435 + "raw room message", (fun () -> 436 + roundtrip_test Event.Raw_event.jsont 437 + {|{"content": {"body": "Hello", "msgtype": "m.text"}, "event_id": "$event123:localhost", "origin_server_ts": 152037280000000, "sender": "@example:localhost", "type": "m.room.message"}|} 438 + ); 439 + 440 + "raw state event", (fun () -> 441 + roundtrip_test Event.Raw_event.jsont 442 + {|{"content": {"name": "Test Room"}, "event_id": "$event456:localhost", "origin_server_ts": 151393755000000, "sender": "@example:localhost", "state_key": "", "type": "m.room.name"}|} 443 + ); 444 + 445 + "raw event with unsigned", (fun () -> 446 + roundtrip_test Event.Raw_event.jsont 447 + {|{"content": {}, "event_id": "$event789:localhost", "origin_server_ts": 151393755000000, "sender": "@example:localhost", "type": "m.room.create", "unsigned": {"age": 12345}}|} 448 + ); 449 + ] 450 + 451 + (** {1 Sync Response Tests} *) 452 + 453 + let sync_tests = [ 454 + "timeline", (fun () -> 455 + roundtrip_test Sync.Timeline.jsont 456 + {|{"events": [], "limited": true, "prev_batch": "token123"}|} 457 + ); 458 + 459 + "timeline with events", (fun () -> 460 + roundtrip_test Sync.Timeline.jsont 461 + {|{"events": [{"content": {"body": "Hello", "msgtype": "m.text"}, "event_id": "$event:localhost", "origin_server_ts": 123456, "sender": "@user:localhost", "type": "m.room.message"}]}|} 462 + ); 463 + 464 + "room_state", (fun () -> 465 + roundtrip_test Sync.Room_state.jsont 466 + {|{"events": []}|} 467 + ); 468 + 469 + "ephemeral", (fun () -> 470 + roundtrip_test Sync.Ephemeral.jsont 471 + {|{"events": [{"type": "m.typing", "content": {"user_ids": []}}]}|} 472 + ); 473 + 474 + "room_summary", (fun () -> 475 + roundtrip_test Sync.Room_summary.jsont 476 + {|{"m.heroes": ["@alice:localhost"], "m.joined_member_count": 5, "m.invited_member_count": 1}|} 477 + ); 478 + 479 + "unread_notifications", (fun () -> 480 + roundtrip_test Sync.Unread_notification_counts.jsont 481 + {|{"highlight_count": 0, "notification_count": 2}|} 482 + ); 483 + 484 + "joined_room", (fun () -> 485 + roundtrip_test Sync.Joined_room.jsont 486 + {|{"timeline": {"events": []}, "state": {"events": []}}|} 487 + ); 488 + 489 + "invited_room", (fun () -> 490 + roundtrip_test Sync.Invited_room.jsont 491 + {|{"invite_state": {"events": []}}|} 492 + ); 493 + 494 + "left_room", (fun () -> 495 + roundtrip_test Sync.Left_room.jsont 496 + {|{"timeline": {"events": []}}|} 497 + ); 498 + 499 + "rooms", (fun () -> 500 + roundtrip_test Sync.Rooms.jsont 501 + {|{"join": {"!room:localhost": {"timeline": {"events": []}}}, "invite": {}, "leave": {}, "knock": {}}|} 502 + ); 503 + 504 + "device_lists", (fun () -> 505 + roundtrip_test Sync.Device_lists.jsont 506 + {|{"changed": ["@alice:localhost"], "left": []}|} 507 + ); 508 + 509 + "presence", (fun () -> 510 + roundtrip_test Sync.Presence.jsont 511 + {|{"events": []}|} 512 + ); 513 + 514 + "response minimal", (fun () -> 515 + roundtrip_test Sync.Response.jsont 516 + {|{"next_batch": "s12345"}|} 517 + ); 518 + 519 + "response with rooms", (fun () -> 520 + let json = read_fixture "sync_response.json" in 521 + roundtrip_test Sync.Response.jsont json 522 + ); 523 + ] 524 + 525 + (** {1 Main} *) 526 + 527 + let () = 528 + Printf.printf "Matrix Protocol Tests\n"; 529 + Printf.printf "=====================\n"; 530 + 531 + group "Identifiers" identifier_tests; 532 + group "Event Contents" event_content_tests; 533 + group "Space Contents" space_content_tests; 534 + group "Call Contents" call_content_tests; 535 + group "Key Verification Contents" key_verification_tests; 536 + group "Policy Rule Contents" policy_rule_tests; 537 + group "Account Data Contents" account_data_tests; 538 + group "Encrypted Contents" encrypted_content_tests; 539 + group "Reaction Contents" reaction_content_tests; 540 + group "Beacon Contents" beacon_content_tests; 541 + group "Poll Contents" poll_content_tests; 542 + group "Raw Events" raw_event_tests; 543 + group "Sync" sync_tests; 544 + 545 + Printf.printf "\n=====================\n"; 546 + Printf.printf "Results: %d/%d passed" !tests_passed !tests_run; 547 + if !tests_failed > 0 then 548 + Printf.printf " (%d failed)" !tests_failed; 549 + Printf.printf "\n"; 550 + 551 + if !tests_failed > 0 then exit 1