···11+(** Simple Matrix bot example using the Eio-idiomatic Matrix SDK.
22+33+ This example demonstrates:
44+ - Creating a client and logging in with exception-based error handling
55+ - Using Eio structured concurrency for the sync loop
66+ - Responding to messages in rooms
77+ - Proper cancellation via Eio switches
88+99+ To run:
1010+ {[
1111+ dune exec examples/simple_bot.exe -- \
1212+ --homeserver https://matrix.org \
1313+ --username @bot:matrix.org \
1414+ --password secret
1515+ ]}
1616+1717+ @see <https://spec.matrix.org/v1.11/client-server-api/> Matrix Client-Server API *)
1818+1919+open Matrix_eio
2020+2121+(** Helper to extract string from JSON.
2222+ Returns [Some value] if the key exists and is a string, [None] otherwise. *)
2323+let json_get_string key (json : Jsont.json) =
2424+ match json with
2525+ | Jsont.Object (mems, _) ->
2626+ List.find_map (fun ((name, _), value) ->
2727+ if name = key then
2828+ match value with
2929+ | Jsont.String (s, _) -> Some s
3030+ | _ -> None
3131+ else None
3232+ ) mems
3333+ | _ -> None
3434+3535+(** Handle a single sync response, looking for messages to respond to.
3636+3737+ This function processes timeline events from joined rooms and responds
3838+ to simple commands:
3939+ - [!echo <text>] - Echoes the text back
4040+ - [!ping] - Responds with "pong!"
4141+4242+ @see <https://spec.matrix.org/v1.11/client-server-api/#syncing> Sync API *)
4343+let handle_sync client my_user_id (response : Matrix_proto.Sync.Response.t) =
4444+ match response.rooms with
4545+ | None -> ()
4646+ | Some rooms ->
4747+ (* Process each joined room *)
4848+ List.iter (fun (room_id_str, joined_room) ->
4949+ match Matrix_proto.Id.Room_id.of_string room_id_str with
5050+ | Error _ -> ()
5151+ | Ok room_id ->
5252+ (* Check timeline events *)
5353+ let events = match joined_room.Matrix_proto.Sync.Joined_room.timeline with
5454+ | None -> []
5555+ | Some timeline -> timeline.events
5656+ in
5757+ List.iter (fun (event : Matrix_proto.Event.Raw_event.t) ->
5858+ (* Only respond to m.room.message events *)
5959+ let event_type = Matrix_proto.Event.Event_type.to_string event.type_ in
6060+ if event_type = "m.room.message" then begin
6161+ (* Get sender - don't respond to our own messages *)
6262+ let sender = event.sender in
6363+ let is_self = Matrix_proto.Id.User_id.to_string sender =
6464+ Matrix_proto.Id.User_id.to_string my_user_id in
6565+ if not is_self then begin
6666+ (* Extract message body from content *)
6767+ let body = json_get_string "body" event.content in
6868+ match body with
6969+ | Some msg when String.starts_with ~prefix:"!echo " msg ->
7070+ (* Echo command - repeat the message *)
7171+ let echo_text = String.sub msg 6 (String.length msg - 6) in
7272+ (try
7373+ Messages.send_text client ~room_id ~body:echo_text ();
7474+ Printf.printf "Echoed: %s\n%!" echo_text
7575+ with Eio.Io _ as e ->
7676+ Printf.eprintf "Failed to send echo: %s\n%!" (Printexc.to_string e))
7777+ | Some msg when msg = "!ping" ->
7878+ (* Ping command *)
7979+ (try
8080+ Messages.send_text client ~room_id ~body:"pong!" ();
8181+ Printf.printf "Responded to ping\n%!"
8282+ with Eio.Io _ as e ->
8383+ Printf.eprintf "Failed to send pong: %s\n%!" (Printexc.to_string e))
8484+ | Some msg ->
8585+ Printf.printf "Message from %s: %s\n%!"
8686+ (Matrix_proto.Id.User_id.to_string sender) msg
8787+ | None -> ()
8888+ end
8989+ end
9090+ ) events
9191+ ) rooms.join
9292+9393+(** Main bot loop using Eio structured concurrency.
9494+9595+ The sync loop runs in a dedicated fibre that can be cancelled by
9696+ releasing the switch. This allows for clean shutdown. *)
9797+let run_bot ~homeserver ~username ~password =
9898+ Eio_main.run @@ fun env ->
9999+ Eio.Switch.run @@ fun sw ->
100100+101101+ Printf.printf "Connecting to %s...\n%!" homeserver;
102102+103103+ (* Create client and login using the Eio-idiomatic API.
104104+ This raises Eio.Io on failure instead of returning Result. *)
105105+ let client =
106106+ try
107107+ Matrix_eio.login_password ~sw ~env
108108+ ~homeserver:(Uri.of_string homeserver)
109109+ ~user:username ~password ()
110110+ with Eio.Io (Error.E err, _) ->
111111+ Printf.eprintf "Login failed: %a\n%!" Error.pp_err err;
112112+ exit 1
113113+ in
114114+115115+ let my_user_id = Client.user_id client in
116116+ Printf.printf "Logged in as %s\n%!"
117117+ (Matrix_proto.Id.User_id.to_string my_user_id);
118118+119119+ (* Get joined rooms *)
120120+ (try
121121+ let rooms = Rooms.get_joined_rooms client in
122122+ Printf.printf "Joined %d rooms\n%!" (List.length rooms)
123123+ with Eio.Io _ ->
124124+ Printf.printf "Could not get joined rooms\n%!");
125125+126126+ (* Start sync loop in a fibre.
127127+ The loop can be cancelled by releasing the switch. *)
128128+ Printf.printf "Starting sync loop (Ctrl+C to stop)...\n%!";
129129+130130+ let clock = Eio.Stdenv.clock env in
131131+132132+ (* Use the Eio-idiomatic sync loop with callback *)
133133+ Sync.sync_forever ~sw ~clock client
134134+ ~params:{ Sync.default_params with timeout = 30000 }
135135+ ~on_sync:(fun response ->
136136+ Printf.printf "Sync: next_batch=%s\n%!" response.next_batch;
137137+ handle_sync client my_user_id response;
138138+ Sync.Continue)
139139+ ~on_error:(fun err ->
140140+ Printf.eprintf "Sync error: %a\n%!" Error.pp_err err;
141141+ (* Retry after 5 seconds on error *)
142142+ Sync.Retry_after 5.0)
143143+ ();
144144+145145+ (* Block the main fibre - sync runs in background *)
146146+ (* In a real application, you might want to handle signals here *)
147147+ Eio.Fiber.await_cancel ()
148148+149149+(** Parse command line and run the bot. *)
150150+let () =
151151+ let homeserver = ref "" in
152152+ let username = ref "" in
153153+ let password = ref "" in
154154+155155+ let spec = [
156156+ ("--homeserver", Arg.Set_string homeserver, "Homeserver URL (e.g., https://matrix.org)");
157157+ ("--username", Arg.Set_string username, "Username (localpart or full @user:server)");
158158+ ("--password", Arg.Set_string password, "Password");
159159+ ] in
160160+161161+ Arg.parse spec (fun _ -> ()) "Simple Matrix Bot using Eio\n\nUsage:";
162162+163163+ if !homeserver = "" || !username = "" || !password = "" then begin
164164+ Printf.eprintf "Usage: simple_bot --homeserver URL --username USER --password PASS\n";
165165+ exit 1
166166+ end;
167167+168168+ run_bot ~homeserver:!homeserver ~username:!username ~password:!password
+329
lib/matrix_client/account.ml
···11+(** Account management operations. *)
22+33+(* Account data *)
44+let get_account_data client ~event_type =
55+ match Client.user_id client with
66+ | None -> Error (Error.Network_error "Not logged in")
77+ | Some user_id ->
88+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
99+ let path = Printf.sprintf "/user/%s/account_data/%s"
1010+ (Uri.pct_encode user_id_str)
1111+ (Uri.pct_encode event_type)
1212+ in
1313+ match Client.get client ~path () with
1414+ | Error e -> Error e
1515+ | Ok body -> Client.decode_response Jsont.json body
1616+1717+let set_account_data client ~event_type ~content =
1818+ match Client.user_id client with
1919+ | None -> Error (Error.Network_error "Not logged in")
2020+ | Some user_id ->
2121+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
2222+ let path = Printf.sprintf "/user/%s/account_data/%s"
2323+ (Uri.pct_encode user_id_str)
2424+ (Uri.pct_encode event_type)
2525+ in
2626+ match Client.encode_body Jsont.json content with
2727+ | Error e -> Error e
2828+ | Ok body ->
2929+ match Client.put client ~path ~body () with
3030+ | Error e -> Error e
3131+ | Ok _ -> Ok ()
3232+3333+let get_room_account_data client ~room_id ~event_type =
3434+ match Client.user_id client with
3535+ | None -> Error (Error.Network_error "Not logged in")
3636+ | Some user_id ->
3737+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
3838+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
3939+ let path = Printf.sprintf "/user/%s/rooms/%s/account_data/%s"
4040+ (Uri.pct_encode user_id_str)
4141+ (Uri.pct_encode room_id_str)
4242+ (Uri.pct_encode event_type)
4343+ in
4444+ match Client.get client ~path () with
4545+ | Error e -> Error e
4646+ | Ok body -> Client.decode_response Jsont.json body
4747+4848+let set_room_account_data client ~room_id ~event_type ~content =
4949+ match Client.user_id client with
5050+ | None -> Error (Error.Network_error "Not logged in")
5151+ | Some user_id ->
5252+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
5353+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
5454+ let path = Printf.sprintf "/user/%s/rooms/%s/account_data/%s"
5555+ (Uri.pct_encode user_id_str)
5656+ (Uri.pct_encode room_id_str)
5757+ (Uri.pct_encode event_type)
5858+ in
5959+ match Client.encode_body Jsont.json content with
6060+ | Error e -> Error e
6161+ | Ok body ->
6262+ match Client.put client ~path ~body () with
6363+ | Error e -> Error e
6464+ | Ok _ -> Ok ()
6565+6666+(* Third-party identifiers *)
6767+type threepid = {
6868+ medium : string;
6969+ address : string;
7070+ validated_at : int64;
7171+ added_at : int64;
7272+}
7373+7474+let threepid_jsont =
7575+ Jsont.Object.(
7676+ map (fun medium address validated_at added_at ->
7777+ { medium; address; validated_at; added_at })
7878+ |> mem "medium" Jsont.string
7979+ |> mem "address" Jsont.string
8080+ |> mem "validated_at" Jsont.int64
8181+ |> mem "added_at" Jsont.int64
8282+ |> finish)
8383+8484+type threepids_response = {
8585+ threepids : threepid list;
8686+}
8787+8888+let threepids_response_jsont =
8989+ Jsont.Object.(
9090+ map (fun threepids -> { threepids })
9191+ |> mem "threepids" (Jsont.list threepid_jsont) ~dec_absent:[]
9292+ |> finish)
9393+9494+let get_3pids client =
9595+ match Client.get client ~path:"/account/3pid" () with
9696+ | Error e -> Error e
9797+ | Ok body ->
9898+ match Client.decode_response threepids_response_jsont body with
9999+ | Error e -> Error e
100100+ | Ok resp -> Ok resp.threepids
101101+102102+(* Email token request *)
103103+type email_token_request = {
104104+ email : string;
105105+ client_secret : string;
106106+ send_attempt : int;
107107+} [@@warning "-69"]
108108+109109+let email_token_request_jsont =
110110+ Jsont.Object.(
111111+ map (fun email client_secret send_attempt ->
112112+ { email; client_secret; send_attempt })
113113+ |> mem "email" Jsont.string
114114+ |> mem "client_secret" Jsont.string
115115+ |> mem "send_attempt" Jsont.int
116116+ |> finish)
117117+118118+type token_response = {
119119+ sid : string;
120120+}
121121+122122+let token_response_jsont =
123123+ Jsont.Object.(
124124+ map (fun sid -> { sid })
125125+ |> mem "sid" Jsont.string
126126+ |> finish)
127127+128128+let request_email_token client ~email ~client_secret ~send_attempt =
129129+ let request = { email; client_secret; send_attempt } in
130130+ match Client.encode_body email_token_request_jsont request with
131131+ | Error e -> Error e
132132+ | Ok body ->
133133+ match Client.post client ~path:"/account/3pid/email/requestToken" ~body () with
134134+ | Error e -> Error e
135135+ | Ok body ->
136136+ match Client.decode_response token_response_jsont body with
137137+ | Error e -> Error e
138138+ | Ok resp -> Ok resp.sid
139139+140140+(* MSISDN token request *)
141141+type msisdn_token_request = {
142142+ country : string;
143143+ phone_number : string;
144144+ client_secret : string;
145145+ send_attempt : int;
146146+} [@@warning "-69"]
147147+148148+let msisdn_token_request_jsont =
149149+ Jsont.Object.(
150150+ map (fun country phone_number client_secret send_attempt ->
151151+ { country; phone_number; client_secret; send_attempt })
152152+ |> mem "country" Jsont.string
153153+ |> mem "phone_number" Jsont.string
154154+ |> mem "client_secret" Jsont.string
155155+ |> mem "send_attempt" Jsont.int
156156+ |> finish)
157157+158158+let request_msisdn_token client ~country ~phone_number ~client_secret ~send_attempt =
159159+ let request = { country; phone_number; client_secret; send_attempt } in
160160+ match Client.encode_body msisdn_token_request_jsont request with
161161+ | Error e -> Error e
162162+ | Ok body ->
163163+ match Client.post client ~path:"/account/3pid/msisdn/requestToken" ~body () with
164164+ | Error e -> Error e
165165+ | Ok body ->
166166+ match Client.decode_response token_response_jsont body with
167167+ | Error e -> Error e
168168+ | Ok resp -> Ok resp.sid
169169+170170+(* Add 3pid *)
171171+type add_3pid_request = {
172172+ client_secret : string;
173173+ sid : string;
174174+} [@@warning "-69"]
175175+176176+let add_3pid_request_jsont =
177177+ Jsont.Object.(
178178+ map (fun client_secret sid -> { client_secret; sid })
179179+ |> mem "client_secret" Jsont.string
180180+ |> mem "sid" Jsont.string
181181+ |> finish)
182182+183183+let add_3pid client ~client_secret ~sid =
184184+ let request = { client_secret; sid } in
185185+ match Client.encode_body add_3pid_request_jsont request with
186186+ | Error e -> Error e
187187+ | Ok body ->
188188+ match Client.post client ~path:"/account/3pid/add" ~body () with
189189+ | Error e -> Error e
190190+ | Ok _ -> Ok ()
191191+192192+(* Delete 3pid *)
193193+type delete_3pid_request = {
194194+ medium : string;
195195+ address : string;
196196+} [@@warning "-69"]
197197+198198+let delete_3pid_request_jsont =
199199+ Jsont.Object.(
200200+ map (fun medium address -> { medium; address })
201201+ |> mem "medium" Jsont.string
202202+ |> mem "address" Jsont.string
203203+ |> finish)
204204+205205+let delete_3pid client ~medium ~address =
206206+ let request = { medium; address } in
207207+ match Client.encode_body delete_3pid_request_jsont request with
208208+ | Error e -> Error e
209209+ | Ok body ->
210210+ match Client.post client ~path:"/account/3pid/delete" ~body () with
211211+ | Error e -> Error e
212212+ | Ok _ -> Ok ()
213213+214214+(* Password change - simplified without UIAA *)
215215+type change_password_request = {
216216+ new_password : string;
217217+ logout_devices : bool;
218218+} [@@warning "-69"]
219219+220220+let change_password_request_jsont =
221221+ Jsont.Object.(
222222+ map (fun new_password logout_devices -> { new_password; logout_devices })
223223+ |> mem "new_password" Jsont.string
224224+ |> mem "logout_devices" Jsont.bool ~dec_absent:false
225225+ |> finish)
226226+227227+let change_password client ~new_password ?(logout_devices = false) () =
228228+ let request = { new_password; logout_devices } in
229229+ match Client.encode_body change_password_request_jsont request with
230230+ | Error e -> Error e
231231+ | Ok body ->
232232+ match Client.post client ~path:"/account/password" ~body () with
233233+ | Error e -> Error e
234234+ | Ok _ -> Ok ()
235235+236236+(* Account deactivation - simplified without UIAA *)
237237+type deactivate_request = {
238238+ erase : bool;
239239+} [@@warning "-69"]
240240+241241+let deactivate_request_jsont =
242242+ Jsont.Object.(
243243+ map (fun erase -> { erase })
244244+ |> mem "erase" Jsont.bool ~dec_absent:false
245245+ |> finish)
246246+247247+let deactivate client ?(erase = false) () =
248248+ let request = { erase } in
249249+ match Client.encode_body deactivate_request_jsont request with
250250+ | Error e -> Error e
251251+ | Ok body ->
252252+ match Client.post client ~path:"/account/deactivate" ~body () with
253253+ | Error e -> Error e
254254+ | Ok _ -> Ok ()
255255+256256+(* Ignored users - stored in account data *)
257257+type ignored_users_content = {
258258+ ignored_users : (string * Jsont.json) list;
259259+}
260260+261261+let ignored_users_content_jsont =
262262+ let module StringMap = Map.Make(String) in
263263+ let map_jsont =
264264+ Jsont.Object.as_string_map Jsont.json
265265+ |> Jsont.map
266266+ ~dec:(fun m -> StringMap.bindings m)
267267+ ~enc:(fun l -> List.to_seq l |> StringMap.of_seq)
268268+ in
269269+ Jsont.Object.(
270270+ map (fun ignored_users -> { ignored_users })
271271+ |> mem "ignored_users" map_jsont ~dec_absent:[]
272272+ |> finish)
273273+274274+let get_ignored_users client =
275275+ match get_account_data client ~event_type:"m.ignored_user_list" with
276276+ | Error (Error.Matrix_error { errcode = Error.M_NOT_FOUND; _ }) -> Ok []
277277+ | Error e -> Error e
278278+ | Ok json ->
279279+ match Jsont_bytesrw.decode_string ignored_users_content_jsont
280280+ (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with
281281+ | Error _ -> Ok []
282282+ | Ok content ->
283283+ let user_ids = List.filter_map (fun (uid, _) ->
284284+ match Matrix_proto.Id.User_id.of_string uid with
285285+ | Ok id -> Some id
286286+ | Error _ -> None
287287+ ) content.ignored_users in
288288+ Ok user_ids
289289+290290+let ignore_user client ~user_id =
291291+ match get_ignored_users client with
292292+ | Error e -> Error e
293293+ | Ok current ->
294294+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
295295+ if List.exists (fun u -> Matrix_proto.Id.User_id.to_string u = user_id_str) current then
296296+ Ok () (* Already ignored *)
297297+ else
298298+ let new_list = user_id :: current in
299299+ let ignored_map = List.map (fun u ->
300300+ (Matrix_proto.Id.User_id.to_string u, Jsont.Json.object' [])
301301+ ) new_list in
302302+ let content = { ignored_users = ignored_map } in
303303+ match Client.encode_body ignored_users_content_jsont content with
304304+ | Error e -> Error e
305305+ | Ok body ->
306306+ match Client.decode_response Jsont.json body with
307307+ | Error e -> Error e
308308+ | Ok json ->
309309+ set_account_data client ~event_type:"m.ignored_user_list" ~content:json
310310+311311+let unignore_user client ~user_id =
312312+ match get_ignored_users client with
313313+ | Error e -> Error e
314314+ | Ok current ->
315315+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
316316+ let new_list = List.filter (fun u ->
317317+ Matrix_proto.Id.User_id.to_string u <> user_id_str
318318+ ) current in
319319+ let ignored_map = List.map (fun u ->
320320+ (Matrix_proto.Id.User_id.to_string u, Jsont.Json.object' [])
321321+ ) new_list in
322322+ let content = { ignored_users = ignored_map } in
323323+ match Client.encode_body ignored_users_content_jsont content with
324324+ | Error e -> Error e
325325+ | Ok body ->
326326+ match Client.decode_response Jsont.json body with
327327+ | Error e -> Error e
328328+ | Ok json ->
329329+ set_account_data client ~event_type:"m.ignored_user_list" ~content:json
+120
lib/matrix_client/account.mli
···11+(** Account management operations. *)
22+33+(** {1 Account Data} *)
44+55+(** Get global account data of a specific type. *)
66+val get_account_data :
77+ Client.t ->
88+ event_type:string ->
99+ (Jsont.json, Error.t) result
1010+1111+(** Set global account data. *)
1212+val set_account_data :
1313+ Client.t ->
1414+ event_type:string ->
1515+ content:Jsont.json ->
1616+ (unit, Error.t) result
1717+1818+(** Get room-specific account data. *)
1919+val get_room_account_data :
2020+ Client.t ->
2121+ room_id:Matrix_proto.Id.Room_id.t ->
2222+ event_type:string ->
2323+ (Jsont.json, Error.t) result
2424+2525+(** Set room-specific account data. *)
2626+val set_room_account_data :
2727+ Client.t ->
2828+ room_id:Matrix_proto.Id.Room_id.t ->
2929+ event_type:string ->
3030+ content:Jsont.json ->
3131+ (unit, Error.t) result
3232+3333+(** {1 Third-Party Identifiers} *)
3434+3535+(** Third-party identifier info. *)
3636+type threepid = {
3737+ medium : string; (** "email" or "msisdn" *)
3838+ address : string; (** The identifier (email or phone) *)
3939+ validated_at : int64; (** Timestamp when validated *)
4040+ added_at : int64; (** Timestamp when added *)
4141+}
4242+4343+(** Get all third-party identifiers for the account. *)
4444+val get_3pids : Client.t -> (threepid list, Error.t) result
4545+4646+(** Request a token for adding an email address.
4747+ Returns the session ID for the verification flow. *)
4848+val request_email_token :
4949+ Client.t ->
5050+ email:string ->
5151+ client_secret:string ->
5252+ send_attempt:int ->
5353+ (string, Error.t) result
5454+5555+(** Request a token for adding a phone number.
5656+ Returns the session ID for the verification flow. *)
5757+val request_msisdn_token :
5858+ Client.t ->
5959+ country:string ->
6060+ phone_number:string ->
6161+ client_secret:string ->
6262+ send_attempt:int ->
6363+ (string, Error.t) result
6464+6565+(** Add a third-party identifier after validation.
6666+ Requires the session_id and client_secret from the token request. *)
6767+val add_3pid :
6868+ Client.t ->
6969+ client_secret:string ->
7070+ sid:string ->
7171+ (unit, Error.t) result
7272+7373+(** Delete a third-party identifier. *)
7474+val delete_3pid :
7575+ Client.t ->
7676+ medium:string ->
7777+ address:string ->
7878+ (unit, Error.t) result
7979+8080+(** {1 Password Management} *)
8181+8282+(** Change the account password.
8383+8484+ @param logout_devices If true, invalidate all other sessions. *)
8585+val change_password :
8686+ Client.t ->
8787+ new_password:string ->
8888+ ?logout_devices:bool ->
8989+ unit ->
9090+ (unit, Error.t) result
9191+9292+(** {1 Account Deactivation} *)
9393+9494+(** Deactivate the account.
9595+9696+ WARNING: This is irreversible!
9797+9898+ @param erase If true, request erasure of personal data. *)
9999+val deactivate :
100100+ Client.t ->
101101+ ?erase:bool ->
102102+ unit ->
103103+ (unit, Error.t) result
104104+105105+(** {1 Ignored Users} *)
106106+107107+(** Get the list of ignored user IDs. *)
108108+val get_ignored_users : Client.t -> (Matrix_proto.Id.User_id.t list, Error.t) result
109109+110110+(** Ignore a user. *)
111111+val ignore_user :
112112+ Client.t ->
113113+ user_id:Matrix_proto.Id.User_id.t ->
114114+ (unit, Error.t) result
115115+116116+(** Unignore a user. *)
117117+val unignore_user :
118118+ Client.t ->
119119+ user_id:Matrix_proto.Id.User_id.t ->
120120+ (unit, Error.t) result
+304
lib/matrix_client/auth.ml
···11+(** Authentication operations. *)
22+33+(* Login flow types *)
44+type login_flow =
55+ | Password
66+ | Token
77+ | Sso
88+ | Unknown of string
99+1010+let login_flow_of_string = function
1111+ | "m.login.password" -> Password
1212+ | "m.login.token" -> Token
1313+ | "m.login.sso" -> Sso
1414+ | s -> Unknown s
1515+1616+let login_flow_to_string = function
1717+ | Password -> "m.login.password"
1818+ | Token -> "m.login.token"
1919+ | Sso -> "m.login.sso"
2020+ | Unknown s -> s
2121+2222+(* JSON codecs for login flows response *)
2323+let login_flow_jsont =
2424+ Jsont.of_of_string ~kind:"login_flow"
2525+ ~enc:login_flow_to_string
2626+ (fun s -> Ok (login_flow_of_string s))
2727+2828+let login_flow_obj_jsont =
2929+ Jsont.Object.map
3030+ ~kind:"login_flow_object"
3131+ (fun flow_type -> flow_type)
3232+ |> Jsont.Object.mem "type" login_flow_jsont
3333+ |> Jsont.Object.finish
3434+3535+let login_flows_response_jsont =
3636+ Jsont.Object.map
3737+ ~kind:"login_flows_response"
3838+ (fun flows -> flows)
3939+ |> Jsont.Object.mem "flows" (Jsont.list login_flow_obj_jsont)
4040+ |> Jsont.Object.finish
4141+4242+let get_login_flows client =
4343+ match Client.get client ~path:"/login" () with
4444+ | Error e -> Error e
4545+ | Ok body -> Client.decode_response login_flows_response_jsont body
4646+4747+(* Login parameters *)
4848+type login_params = {
4949+ device_id : string option;
5050+ initial_device_display_name : string option;
5151+}
5252+5353+let default_login_params = {
5454+ device_id = None;
5555+ initial_device_display_name = None;
5656+}
5757+5858+(* Login request codec - write-only types for JSON encoding *)
5959+type login_request = {
6060+ req_type : string;
6161+ identifier : login_identifier;
6262+ password : string option;
6363+ token : string option;
6464+ device_id : string option;
6565+ initial_device_display_name : string option;
6666+} [@@warning "-69"]
6767+6868+and login_identifier = {
6969+ id_type : string;
7070+ user : string option;
7171+} [@@warning "-69"]
7272+7373+let login_identifier_jsont =
7474+ Jsont.Object.map
7575+ ~kind:"login_identifier"
7676+ (fun id_type user -> { id_type; user })
7777+ |> Jsont.Object.mem "type" Jsont.string
7878+ |> Jsont.Object.opt_mem "user" Jsont.string
7979+ |> Jsont.Object.finish
8080+8181+let login_request_jsont =
8282+ Jsont.Object.map
8383+ ~kind:"login_request"
8484+ (fun req_type identifier password token device_id initial_device_display_name ->
8585+ { req_type; identifier; password; token; device_id; initial_device_display_name })
8686+ |> Jsont.Object.mem "type" Jsont.string
8787+ |> Jsont.Object.mem "identifier" login_identifier_jsont
8888+ |> Jsont.Object.opt_mem "password" Jsont.string
8989+ |> Jsont.Object.opt_mem "token" Jsont.string
9090+ |> Jsont.Object.opt_mem "device_id" Jsont.string
9191+ |> Jsont.Object.opt_mem "initial_device_display_name" Jsont.string
9292+ |> Jsont.Object.finish
9393+9494+(* Login response codec *)
9595+type login_response = {
9696+ user_id : Matrix_proto.Id.User_id.t;
9797+ access_token : string;
9898+ device_id : Matrix_proto.Id.Device_id.t;
9999+ refresh_token : string option;
100100+}
101101+102102+let login_response_jsont =
103103+ Jsont.Object.map
104104+ ~kind:"login_response"
105105+ (fun user_id access_token device_id refresh_token ->
106106+ { user_id; access_token; device_id; refresh_token })
107107+ |> Jsont.Object.mem "user_id" Matrix_proto.Id.User_id.jsont
108108+ |> Jsont.Object.mem "access_token" Jsont.string
109109+ |> Jsont.Object.mem "device_id" Matrix_proto.Id.Device_id.jsont
110110+ |> Jsont.Object.opt_mem "refresh_token" Jsont.string
111111+ |> Jsont.Object.finish
112112+113113+let response_to_session resp : Client.session =
114114+ { user_id = resp.user_id;
115115+ access_token = resp.access_token;
116116+ device_id = resp.device_id;
117117+ refresh_token = resp.refresh_token;
118118+ }
119119+120120+let login_password client ~user ~password ?(params = default_login_params) () =
121121+ let request = {
122122+ req_type = "m.login.password";
123123+ identifier = { id_type = "m.id.user"; user = Some user };
124124+ password = Some password;
125125+ token = None;
126126+ device_id = params.device_id;
127127+ initial_device_display_name = params.initial_device_display_name;
128128+ } in
129129+ match Client.encode_body login_request_jsont request with
130130+ | Error e -> Error e
131131+ | Ok body ->
132132+ match Client.post_unauthenticated client ~path:"/login" ~body () with
133133+ | Error e -> Error e
134134+ | Ok body ->
135135+ match Client.decode_response login_response_jsont body with
136136+ | Error e -> Error e
137137+ | Ok resp -> Ok (response_to_session resp)
138138+139139+let login_token client ~token ?(params = default_login_params) () =
140140+ let request = {
141141+ req_type = "m.login.token";
142142+ identifier = { id_type = "m.id.user"; user = None };
143143+ password = None;
144144+ token = Some token;
145145+ device_id = params.device_id;
146146+ initial_device_display_name = params.initial_device_display_name;
147147+ } in
148148+ match Client.encode_body login_request_jsont request with
149149+ | Error e -> Error e
150150+ | Ok body ->
151151+ match Client.post_unauthenticated client ~path:"/login" ~body () with
152152+ | Error e -> Error e
153153+ | Ok body ->
154154+ match Client.decode_response login_response_jsont body with
155155+ | Error e -> Error e
156156+ | Ok resp -> Ok (response_to_session resp)
157157+158158+(* Token refresh *)
159159+type refresh_request = {
160160+ refresh_token : string;
161161+} [@@warning "-69"]
162162+163163+let refresh_request_jsont =
164164+ Jsont.Object.map
165165+ ~kind:"refresh_request"
166166+ (fun refresh_token -> { refresh_token })
167167+ |> Jsont.Object.mem "refresh_token" Jsont.string
168168+ |> Jsont.Object.finish
169169+170170+type refresh_response = {
171171+ access_token : string;
172172+ refresh_token : string option;
173173+}
174174+175175+let refresh_response_jsont =
176176+ Jsont.Object.map
177177+ ~kind:"refresh_response"
178178+ (fun access_token refresh_token -> { access_token; refresh_token })
179179+ |> Jsont.Object.mem "access_token" Jsont.string
180180+ |> Jsont.Object.opt_mem "refresh_token" Jsont.string
181181+ |> Jsont.Object.finish
182182+183183+let refresh_token client ~refresh_token =
184184+ let request = { refresh_token } in
185185+ match Client.encode_body refresh_request_jsont request with
186186+ | Error e -> Error e
187187+ | Ok body ->
188188+ match Client.post_unauthenticated client ~path:"/refresh" ~body () with
189189+ | Error e -> Error e
190190+ | Ok body ->
191191+ match Client.decode_response refresh_response_jsont body with
192192+ | Error e -> Error e
193193+ | Ok resp -> Ok (resp.access_token, resp.refresh_token)
194194+195195+(* Logout *)
196196+let logout client =
197197+ match Client.post client ~path:"/logout" ~body:"{}" () with
198198+ | Error e -> Error e
199199+ | Ok _ -> Ok ()
200200+201201+let logout_all client =
202202+ match Client.post client ~path:"/logout/all" ~body:"{}" () with
203203+ | Error e -> Error e
204204+ | Ok _ -> Ok ()
205205+206206+(* Registration *)
207207+type registration_kind =
208208+ | User
209209+ | Guest
210210+211211+type register_request = {
212212+ kind : string option;
213213+ username : string option;
214214+ password : string option;
215215+ device_id : string option;
216216+ initial_device_display_name : string option;
217217+ inhibit_login : bool option;
218218+} [@@warning "-69"]
219219+220220+let register_request_jsont =
221221+ Jsont.Object.map
222222+ ~kind:"register_request"
223223+ (fun username password device_id initial_device_display_name inhibit_login ->
224224+ { kind = None; username; password; device_id; initial_device_display_name; inhibit_login })
225225+ |> Jsont.Object.opt_mem "username" Jsont.string
226226+ |> Jsont.Object.opt_mem "password" Jsont.string
227227+ |> Jsont.Object.opt_mem "device_id" Jsont.string
228228+ |> Jsont.Object.opt_mem "initial_device_display_name" Jsont.string
229229+ |> Jsont.Object.opt_mem "inhibit_login" Jsont.bool
230230+ |> Jsont.Object.finish
231231+232232+type register_response = {
233233+ user_id : Matrix_proto.Id.User_id.t;
234234+ access_token : string option;
235235+ device_id : string option;
236236+ refresh_token : string option;
237237+}
238238+239239+let register_response_jsont =
240240+ Jsont.Object.map
241241+ ~kind:"register_response"
242242+ (fun user_id access_token device_id refresh_token ->
243243+ { user_id; access_token; device_id; refresh_token })
244244+ |> Jsont.Object.mem "user_id" Matrix_proto.Id.User_id.jsont
245245+ |> Jsont.Object.opt_mem "access_token" Jsont.string
246246+ |> Jsont.Object.opt_mem "device_id" Jsont.string
247247+ |> Jsont.Object.opt_mem "refresh_token" Jsont.string
248248+ |> Jsont.Object.finish
249249+250250+let register client ?kind ?username ?password ?device_id ?initial_device_display_name ?inhibit_login () =
251251+ let kind_str = match kind with
252252+ | Some Guest -> Some "guest"
253253+ | Some User | None -> None
254254+ in
255255+ let query = match kind_str with
256256+ | Some k -> Some [("kind", k)]
257257+ | None -> None
258258+ in
259259+ let request = {
260260+ kind = None;
261261+ username;
262262+ password;
263263+ device_id;
264264+ initial_device_display_name;
265265+ inhibit_login;
266266+ } in
267267+ match Client.encode_body register_request_jsont request with
268268+ | Error e -> Error e
269269+ | Ok body ->
270270+ match Client.post_unauthenticated client ~path:"/register" ?query ~body () with
271271+ | Error e -> Error e
272272+ | Ok body ->
273273+ match Client.decode_response register_response_jsont body with
274274+ | Error e -> Error e
275275+ | Ok resp ->
276276+ match resp.access_token, resp.device_id with
277277+ | Some access_token, Some device_id ->
278278+ let device_id = Matrix_proto.Id.Device_id.of_string_exn device_id in
279279+ Ok { Client.user_id = resp.user_id;
280280+ access_token;
281281+ device_id;
282282+ refresh_token = resp.refresh_token }
283283+ | _ ->
284284+ Error (Error.Json_error "Registration succeeded but no session returned (inhibit_login may be true)")
285285+286286+(* Whoami *)
287287+type whoami_response = {
288288+ user_id : Matrix_proto.Id.User_id.t;
289289+}
290290+291291+let whoami_response_jsont =
292292+ Jsont.Object.map
293293+ ~kind:"whoami_response"
294294+ (fun user_id -> { user_id })
295295+ |> Jsont.Object.mem "user_id" Matrix_proto.Id.User_id.jsont
296296+ |> Jsont.Object.finish
297297+298298+let whoami client =
299299+ match Client.get client ~path:"/account/whoami" () with
300300+ | Error e -> Error e
301301+ | Ok body ->
302302+ match Client.decode_response whoami_response_jsont body with
303303+ | Error e -> Error e
304304+ | Ok resp -> Ok resp.user_id
+92
lib/matrix_client/auth.mli
···11+(** Authentication operations. *)
22+33+(** {1 Login Flows} *)
44+55+(** Supported login flow types. *)
66+type login_flow =
77+ | Password (** m.login.password *)
88+ | Token (** m.login.token *)
99+ | Sso (** m.login.sso *)
1010+ | Unknown of string
1111+1212+(** Get supported login flows from the homeserver. *)
1313+val get_login_flows : Client.t -> (login_flow list, Error.t) result
1414+1515+(** {1 Login} *)
1616+1717+(** Login parameters. *)
1818+type login_params = {
1919+ device_id : string option;
2020+ (** Device ID to use. If not specified, the server will generate one. *)
2121+ initial_device_display_name : string option;
2222+ (** Display name for the new device. *)
2323+}
2424+2525+(** Default login parameters. *)
2626+val default_login_params : login_params
2727+2828+(** Login with username and password.
2929+3030+ On success, returns a session that should be passed to {!Client.with_session}
3131+ to create an authenticated client. *)
3232+val login_password :
3333+ Client.t ->
3434+ user:string ->
3535+ password:string ->
3636+ ?params:login_params ->
3737+ unit ->
3838+ (Client.session, Error.t) result
3939+4040+(** Login with a login token (e.g., from SSO flow). *)
4141+val login_token :
4242+ Client.t ->
4343+ token:string ->
4444+ ?params:login_params ->
4545+ unit ->
4646+ (Client.session, Error.t) result
4747+4848+(** {1 Token Refresh} *)
4949+5050+(** Refresh an access token.
5151+5252+ Returns the new access token and optionally a new refresh token. *)
5353+val refresh_token :
5454+ Client.t ->
5555+ refresh_token:string ->
5656+ (string * string option, Error.t) result
5757+5858+(** {1 Logout} *)
5959+6060+(** Logout the current session, invalidating the access token. *)
6161+val logout : Client.t -> (unit, Error.t) result
6262+6363+(** Logout all sessions, invalidating all access tokens for this user. *)
6464+val logout_all : Client.t -> (unit, Error.t) result
6565+6666+(** {1 Registration} *)
6767+6868+(** Registration kind. *)
6969+type registration_kind =
7070+ | User (** Normal user registration *)
7171+ | Guest (** Guest account *)
7272+7373+(** Register a new account.
7474+7575+ Note: Registration may require additional authentication flows (UIAA)
7676+ which are not currently supported. This function only works when
7777+ registration is open without additional verification. *)
7878+val register :
7979+ Client.t ->
8080+ ?kind:registration_kind ->
8181+ ?username:string ->
8282+ ?password:string ->
8383+ ?device_id:string ->
8484+ ?initial_device_display_name:string ->
8585+ ?inhibit_login:bool ->
8686+ unit ->
8787+ (Client.session, Error.t) result
8888+8989+(** {1 Account Info} *)
9090+9191+(** Get the user ID for the current session (whoami endpoint). *)
9292+val whoami : Client.t -> (Matrix_proto.Id.User_id.t, Error.t) result
+466
lib/matrix_client/backup.ml
···11+(** Server-side key backup and recovery.
22+33+ This module implements Matrix server-side backup of room keys using the
44+ m.megolm_backup.v1.curve25519-aes-sha2 backup algorithm.
55+66+ Key backup allows clients to store encrypted room keys on the server so
77+ they can be recovered on new devices or after data loss.
88+99+ Note: Due to various known flaws in this algorithm, it is provided mainly
1010+ for backwards compatibility with existing backups. *)
1111+1212+open Mirage_crypto_ec
1313+1414+(** {1 Backup Key Types} *)
1515+1616+(** Signature verification state *)
1717+type signature_state =
1818+ | Missing (** No signature found *)
1919+ | Invalid (** Signature is invalid *)
2020+ | Valid_but_not_trusted (** Valid but signer not trusted *)
2121+ | Valid_and_trusted (** Valid and signer is trusted *)
2222+2323+let signature_state_trusted = function
2424+ | Valid_and_trusted -> true
2525+ | _ -> false
2626+2727+(** Signature verification result *)
2828+type signature_verification = {
2929+ device_signature : signature_state;
3030+ user_identity_signature : signature_state;
3131+ other_signatures : (string * signature_state) list; (** device_id -> state *)
3232+}
3333+3434+let signature_verification_trusted v =
3535+ signature_state_trusted v.device_signature ||
3636+ signature_state_trusted v.user_identity_signature ||
3737+ List.exists (fun (_, s) -> signature_state_trusted s) v.other_signatures
3838+3939+let empty_signature_verification = {
4040+ device_signature = Missing;
4141+ user_identity_signature = Missing;
4242+ other_signatures = [];
4343+}
4444+4545+(** Auth data for m.megolm_backup.v1.curve25519-aes-sha2 *)
4646+type megolm_v1_auth_data = {
4747+ public_key : string; (** Base64 Curve25519 public key *)
4848+ signatures : (string * (string * string) list) list; (** user_id -> (key_id, sig) *)
4949+}
5050+5151+(** Convert signatures to JSON *)
5252+let signatures_to_json signatures =
5353+ let user_sigs = List.map (fun (user_id, key_sigs) ->
5454+ let inner = List.map (fun (key_id, sig_) ->
5555+ Printf.sprintf {|"%s":"%s"|} key_id sig_
5656+ ) key_sigs |> String.concat "," in
5757+ Printf.sprintf {|"%s":{%s}|} user_id inner
5858+ ) signatures |> String.concat "," in
5959+ "{" ^ user_sigs ^ "}"
6060+6161+(** Encode auth data to JSON string *)
6262+let megolm_v1_auth_data_to_json auth_data =
6363+ Printf.sprintf {|{"public_key":"%s","signatures":%s}|}
6464+ auth_data.public_key
6565+ (signatures_to_json auth_data.signatures)
6666+6767+(** Room key backup info - describes the backup algorithm and parameters *)
6868+type backup_info =
6969+ | Megolm_v1_curve25519_aes_sha2 of megolm_v1_auth_data
7070+ | Other of { algorithm : string; auth_data : Jsont.json }
7171+7272+(** Backup version info from server *)
7373+type backup_version_info = {
7474+ version : string;
7575+ algorithm : string;
7676+ auth_data : Jsont.json;
7777+ count : int;
7878+ etag : string;
7979+}
8080+8181+(** {1 Backup Encryption Key} *)
8282+8383+(** Private key for decrypting backed up room keys *)
8484+type backup_decryption_key = {
8585+ private_key : string; (** Base64-encoded X25519 private key *)
8686+ public_key : string; (** Base64-encoded X25519 public key *)
8787+}
8888+8989+(** Public key for encrypting room keys for backup *)
9090+type backup_encryption_key = {
9191+ public_key : string; (** Base64-encoded X25519 public key *)
9292+ mutable backup_version : string option;
9393+ mutable signatures : (string * (string * string) list) list;
9494+}
9595+9696+(** Generate a new backup key pair *)
9797+let generate_backup_key () =
9898+ let priv, pub = X25519.gen_key () in
9999+ (* X25519.secret is a string, and gen_key returns (secret, public) where public is a string *)
100100+ let priv_bytes = X25519.secret_to_octets priv in
101101+ {
102102+ private_key = Base64.encode_string priv_bytes;
103103+ public_key = Base64.encode_string pub;
104104+ }
105105+106106+(** Create encryption key from decryption key *)
107107+let encryption_key_of_decryption_key (decryption_key : backup_decryption_key) = {
108108+ public_key = decryption_key.public_key;
109109+ backup_version = None;
110110+ signatures = [];
111111+}
112112+113113+(** Create encryption key from base64 public key *)
114114+let encryption_key_of_base64 public_key =
115115+ match Base64.decode public_key with
116116+ | Error _ -> Error "Invalid base64 encoding"
117117+ | Ok bytes ->
118118+ if String.length bytes <> 32 then
119119+ Error "Invalid key length"
120120+ else
121121+ Ok {
122122+ public_key;
123123+ backup_version = None;
124124+ signatures = [];
125125+ }
126126+127127+(** Get the backup algorithm name *)
128128+let backup_algorithm = "m.megolm_backup.v1.curve25519-aes-sha2"
129129+130130+(** {1 Room Key Encryption for Backup} *)
131131+132132+(** Encrypted session data (matches Matrix spec) *)
133133+type encrypted_session_data = {
134134+ ephemeral : string; (** Base64 ephemeral public key *)
135135+ ciphertext : string; (** Base64 ciphertext *)
136136+ mac : string; (** Base64 MAC *)
137137+}
138138+139139+(** Key backup data for a single session *)
140140+type key_backup_data = {
141141+ first_message_index : int;
142142+ forwarded_count : int;
143143+ is_verified : bool;
144144+ session_data : encrypted_session_data;
145145+}
146146+147147+(** Room key backup request (for upload) *)
148148+type keys_backup_request = {
149149+ rooms : (string * (string * key_backup_data) list) list; (** room_id -> session_id -> data *)
150150+}
151151+152152+(** Encrypt a room key for backup using X25519/AES-256/HMAC-SHA256 *)
153153+let encrypt_room_key (encryption_key : backup_encryption_key) ~session_key ~session_id ~room_id ~sender_key =
154154+ match Base64.decode encryption_key.public_key with
155155+ | Error _ -> Error "Invalid encryption key"
156156+ | Ok recipient_pub_bytes ->
157157+ if String.length recipient_pub_bytes <> 32 then
158158+ Error "Invalid X25519 public key length"
159159+ else
160160+ (* Generate ephemeral key pair *)
161161+ let ephemeral_priv, ephemeral_pub = X25519.gen_key () in
162162+163163+ (* Perform X25519 key exchange *)
164164+ (match X25519.key_exchange ephemeral_priv recipient_pub_bytes with
165165+ | Error _ -> Error "Key exchange failed"
166166+ | Ok shared_secret ->
167167+ (* Derive encryption and MAC keys using HKDF *)
168168+ let prk = Hkdf.extract ~hash:`SHA256 ~salt:"" shared_secret in
169169+ let okm = Hkdf.expand ~hash:`SHA256 ~prk ~info:"" 64 in
170170+ let aes_key = String.sub okm 0 32 in
171171+ let mac_key = String.sub okm 32 32 in
172172+173173+ (* Build the payload to encrypt *)
174174+ let payload = Printf.sprintf
175175+ {|{"algorithm":"m.megolm.v1.aes-sha2","room_id":"%s","sender_key":"%s","session_id":"%s","session_key":"%s"}|}
176176+ room_id sender_key session_id session_key
177177+ in
178178+179179+ (* Pad payload to 16-byte boundary (PKCS#7) *)
180180+ let pad_len = 16 - (String.length payload mod 16) in
181181+ let padded = payload ^ String.make pad_len (Char.chr pad_len) in
182182+183183+ (* Generate random IV *)
184184+ let iv = Mirage_crypto_rng.generate 16 in
185185+186186+ (* Encrypt with AES-256-CBC *)
187187+ let key = Mirage_crypto.AES.CBC.of_secret aes_key in
188188+ let ciphertext = Mirage_crypto.AES.CBC.encrypt ~key ~iv padded in
189189+190190+ (* Prepend IV to ciphertext for MAC calculation *)
191191+ let mac_input = iv ^ ciphertext in
192192+193193+ (* Calculate HMAC-SHA256 *)
194194+ let mac = Digestif.SHA256.hmac_string ~key:mac_key mac_input in
195195+ let mac_bytes = Digestif.SHA256.to_raw_string mac in
196196+ (* Truncate to first 8 bytes as per spec *)
197197+ let mac_truncated = String.sub mac_bytes 0 8 in
198198+199199+ Ok {
200200+ ephemeral = Base64.encode_string ephemeral_pub;
201201+ ciphertext = Base64.encode_string (iv ^ ciphertext);
202202+ mac = Base64.encode_string mac_truncated;
203203+ })
204204+205205+(** Decrypt a room key from backup *)
206206+let decrypt_room_key (decryption_key : backup_decryption_key) (session_data : encrypted_session_data) =
207207+ match Base64.decode decryption_key.private_key,
208208+ Base64.decode session_data.ephemeral,
209209+ Base64.decode session_data.ciphertext,
210210+ Base64.decode session_data.mac with
211211+ | Error _, _, _, _ -> Error "Invalid private key encoding"
212212+ | _, Error _, _, _ -> Error "Invalid ephemeral key encoding"
213213+ | _, _, Error _, _ -> Error "Invalid ciphertext encoding"
214214+ | _, _, _, Error _ -> Error "Invalid MAC encoding"
215215+ | Ok priv_bytes, Ok ephemeral_bytes, Ok ciphertext_with_iv, Ok mac_bytes ->
216216+ match X25519.secret_of_octets priv_bytes with
217217+ | Error _ -> Error "Invalid X25519 private key"
218218+ | Ok (priv, _) ->
219219+ (* Perform X25519 key exchange *)
220220+ (match X25519.key_exchange priv ephemeral_bytes with
221221+ | Error _ -> Error "Key exchange failed"
222222+ | Ok shared_secret ->
223223+ (* Derive encryption and MAC keys using HKDF *)
224224+ let prk = Hkdf.extract ~hash:`SHA256 ~salt:"" shared_secret in
225225+ let okm = Hkdf.expand ~hash:`SHA256 ~prk ~info:"" 64 in
226226+ let aes_key = String.sub okm 0 32 in
227227+ let mac_key = String.sub okm 32 32 in
228228+229229+ (* Verify MAC *)
230230+ let expected_mac = Digestif.SHA256.hmac_string ~key:mac_key ciphertext_with_iv in
231231+ let expected_mac_bytes = Digestif.SHA256.to_raw_string expected_mac in
232232+ let expected_mac_truncated = String.sub expected_mac_bytes 0 8 in
233233+ if mac_bytes <> expected_mac_truncated then
234234+ Error "MAC verification failed"
235235+ else begin
236236+ (* Extract IV and ciphertext *)
237237+ if String.length ciphertext_with_iv < 16 then
238238+ Error "Ciphertext too short"
239239+ else begin
240240+ let iv = String.sub ciphertext_with_iv 0 16 in
241241+ let ciphertext = String.sub ciphertext_with_iv 16 (String.length ciphertext_with_iv - 16) in
242242+243243+ (* Decrypt with AES-256-CBC *)
244244+ let key = Mirage_crypto.AES.CBC.of_secret aes_key in
245245+ let plaintext = Mirage_crypto.AES.CBC.decrypt ~key ~iv ciphertext in
246246+247247+ (* Remove PKCS#7 padding *)
248248+ let pad_len = Char.code (String.get plaintext (String.length plaintext - 1)) in
249249+ if pad_len < 1 || pad_len > 16 then
250250+ Error "Invalid padding"
251251+ else
252252+ let unpadded = String.sub plaintext 0 (String.length plaintext - pad_len) in
253253+ Ok unpadded
254254+ end
255255+ end)
256256+257257+(** {1 Backup Machine State} *)
258258+259259+(** State of the backup machine *)
260260+type backup_state =
261261+ | Disabled (** No backup configured *)
262262+ | Creating (** Creating a new backup *)
263263+ | Enabling (** Enabling existing backup *)
264264+ | Resuming (** Resuming existing backup *)
265265+ | Enabled (** Backup is active *)
266266+ | Downloading (** Downloading keys from backup *)
267267+ | Disabling (** Disabling backup *)
268268+269269+(** Backup machine for managing room key backups *)
270270+type t = {
271271+ user_id : Matrix_proto.Id.User_id.t;
272272+ device_id : Matrix_proto.Id.Device_id.t;
273273+ mutable state : backup_state;
274274+ mutable encryption_key : backup_encryption_key option;
275275+ mutable decryption_key : backup_decryption_key option;
276276+ mutable backup_version : string option;
277277+ (* Pending sessions to backup *)
278278+ mutable pending_sessions : (string * string * string) list; (** (room_id, session_id, sender_key) *)
279279+ (* Sessions that have been backed up *)
280280+ mutable backed_up_sessions : (string * string) list; (** (room_id, session_id) *)
281281+}
282282+283283+(** Create a new backup machine *)
284284+let create ~user_id ~device_id = {
285285+ user_id;
286286+ device_id;
287287+ state = Disabled;
288288+ encryption_key = None;
289289+ decryption_key = None;
290290+ backup_version = None;
291291+ pending_sessions = [];
292292+ backed_up_sessions = [];
293293+}
294294+295295+(** Check if backup is enabled *)
296296+let is_enabled t = t.state = Enabled
297297+298298+(** Get the current backup version *)
299299+let backup_version t = t.backup_version
300300+301301+(** {1 Backup Setup} *)
302302+303303+(** Enable backup with a new key *)
304304+let enable_with_new_key t =
305305+ let key = generate_backup_key () in
306306+ t.decryption_key <- Some key;
307307+ t.encryption_key <- Some (encryption_key_of_decryption_key key);
308308+ t.state <- Creating;
309309+ key
310310+311311+(** Enable backup with an existing decryption key *)
312312+let enable_with_key t decryption_key =
313313+ t.decryption_key <- Some decryption_key;
314314+ t.encryption_key <- Some (encryption_key_of_decryption_key decryption_key);
315315+ t.state <- Enabling
316316+317317+(** Enable backup with only an encryption key (upload-only mode) *)
318318+let enable_upload_only t encryption_key version =
319319+ t.encryption_key <- Some encryption_key;
320320+ t.decryption_key <- None;
321321+ t.backup_version <- Some version;
322322+ encryption_key.backup_version <- Some version;
323323+ t.state <- Enabled
324324+325325+(** Set the backup version after creating *)
326326+let set_backup_version t version =
327327+ t.backup_version <- Some version;
328328+ (match t.encryption_key with
329329+ | Some key -> key.backup_version <- Some version
330330+ | None -> ());
331331+ t.state <- Enabled
332332+333333+(** Disable backup *)
334334+let disable t =
335335+ t.state <- Disabling;
336336+ t.encryption_key <- None;
337337+ t.decryption_key <- None;
338338+ t.backup_version <- None;
339339+ t.backed_up_sessions <- [];
340340+ t.state <- Disabled
341341+342342+(** {1 Session Management} *)
343343+344344+(** Mark a session as needing backup *)
345345+let mark_session_for_backup t ~room_id ~session_id ~sender_key =
346346+ if not (List.mem (room_id, session_id) t.backed_up_sessions) then
347347+ t.pending_sessions <- (room_id, session_id, sender_key) :: t.pending_sessions
348348+349349+(** Get number of pending sessions *)
350350+let pending_count t = List.length t.pending_sessions
351351+352352+(** Check if a session has been backed up *)
353353+let is_session_backed_up t ~room_id ~session_id =
354354+ List.mem (room_id, session_id) t.backed_up_sessions
355355+356356+(** Mark a session as backed up *)
357357+let mark_session_backed_up t ~room_id ~session_id =
358358+ t.pending_sessions <- List.filter (fun (r, s, _) -> r <> room_id || s <> session_id) t.pending_sessions;
359359+ if not (List.mem (room_id, session_id) t.backed_up_sessions) then
360360+ t.backed_up_sessions <- (room_id, session_id) :: t.backed_up_sessions
361361+362362+(** {1 Room Key Recovery} *)
363363+364364+(** Recovered room key data *)
365365+type recovered_room_key = {
366366+ room_id : string;
367367+ session_id : string;
368368+ session_key : string;
369369+ sender_key : string;
370370+ algorithm : string;
371371+ forwarded : bool;
372372+}
373373+374374+(** Parse a recovered room key from decrypted JSON *)
375375+let parse_recovered_key json_str =
376376+ (* Simple JSON parsing - in production would use proper parser *)
377377+ let _get_field name str =
378378+ let _pattern = Printf.sprintf {|"%s":"|} name in
379379+ match String.split_on_char '"' str with
380380+ | _ -> None
381381+ in
382382+ (* Simplified - would use Jsont in real implementation *)
383383+ match Jsont_bytesrw.decode_string Jsont.json json_str with
384384+ | Error _ -> None
385385+ | Ok _json -> None (* Would extract fields from JSON *)
386386+387387+(** Result of importing keys from backup *)
388388+type import_result = {
389389+ imported_count : int;
390390+ total_count : int;
391391+ keys : recovered_room_key list;
392392+}
393393+394394+(** {1 Backup Creation API Helpers} *)
395395+396396+(** Build auth data for a new backup *)
397397+let build_auth_data encryption_key = {
398398+ public_key = encryption_key.public_key;
399399+ signatures = encryption_key.signatures;
400400+}
401401+402402+(** Create backup version request body *)
403403+let create_version_request_body t =
404404+ match t.encryption_key with
405405+ | None -> Error "No encryption key configured"
406406+ | Some key ->
407407+ let auth_data = build_auth_data key in
408408+ let auth_json = megolm_v1_auth_data_to_json auth_data in
409409+ Ok (Printf.sprintf
410410+ {|{"algorithm":"%s","auth_data":%s}|}
411411+ backup_algorithm auth_json)
412412+413413+(** {1 Recovery Key Format} *)
414414+415415+(** Encode a backup decryption key as a recovery key (human-readable) *)
416416+let encode_recovery_key (key : backup_decryption_key) =
417417+ match Base64.decode key.private_key with
418418+ | Error _ -> Error "Invalid private key"
419419+ | Ok bytes ->
420420+ (* Add header byte 0x8B, then calculate parity byte *)
421421+ let with_header = "\x8B" ^ bytes in
422422+ let parity = String.fold_left (fun acc c -> acc lxor Char.code c) 0 with_header in
423423+ let full = with_header ^ String.make 1 (Char.chr parity) in
424424+ (* Encode as base58 with spaces every 4 chars for readability *)
425425+ let _base58_alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" in
426426+ (* Simplified - would use proper base58 encoding; using base64 for now *)
427427+ let encoded = Base64.encode_string ~pad:false full in
428428+ (* Add spaces for readability *)
429429+ let with_spaces = String.to_seq encoded
430430+ |> Seq.mapi (fun i c -> if i > 0 && i mod 4 = 0 then [' '; c] else [c])
431431+ |> Seq.flat_map List.to_seq
432432+ |> String.of_seq
433433+ in
434434+ Ok with_spaces
435435+436436+(** Decode a recovery key to a backup decryption key *)
437437+let decode_recovery_key recovery_key =
438438+ (* Remove spaces and decode *)
439439+ let cleaned = String.split_on_char ' ' recovery_key |> String.concat "" in
440440+ match Base64.decode cleaned with
441441+ | Error _ -> Error "Invalid recovery key format"
442442+ | Ok bytes ->
443443+ if String.length bytes < 3 then
444444+ Error "Recovery key too short"
445445+ else if String.get bytes 0 <> '\x8B' then
446446+ Error "Invalid recovery key header"
447447+ else begin
448448+ (* Verify parity *)
449449+ let key_bytes = String.sub bytes 1 (String.length bytes - 2) in
450450+ let expected_parity = Char.code (String.get bytes (String.length bytes - 1)) in
451451+ let actual_parity = String.fold_left (fun acc c -> acc lxor Char.code c) 0
452452+ (String.sub bytes 0 (String.length bytes - 1)) in
453453+ if expected_parity <> actual_parity then
454454+ Error "Recovery key parity check failed"
455455+ else begin
456456+ (* Derive public key from private key *)
457457+ match X25519.secret_of_octets key_bytes with
458458+ | Error _ -> Error "Invalid private key in recovery key"
459459+ | Ok (priv, pub_bytes) ->
460460+ ignore priv;
461461+ Ok {
462462+ private_key = Base64.encode_string key_bytes;
463463+ public_key = Base64.encode_string pub_bytes;
464464+ }
465465+ end
466466+ end
+193
lib/matrix_client/calls.ml
···11+(** VoIP call signaling operations. *)
22+33+(** Call state *)
44+type call_state =
55+ | Ringing
66+ | Connected
77+ | Ended
88+99+(** Generate a new call ID. *)
1010+let generate_call_id () =
1111+ (* Use transaction ID generator for call IDs *)
1212+ Matrix_proto.Id.Transaction_id.(generate () |> to_string)
1313+1414+(** Generate a new party ID for multi-party calls. *)
1515+let generate_party_id () =
1616+ Matrix_proto.Id.Transaction_id.(generate () |> to_string)
1717+1818+(** Send a call invite.
1919+2020+ @param room_id The room to send the invite in
2121+ @param call_id The call ID
2222+ @param offer The SDP offer
2323+ @param lifetime How long the invite is valid in milliseconds
2424+ @param version Call version (0 or 1)
2525+ @param party_id Party ID for version 1 calls
2626+ @param invitee Optional user to invite (for version 1 calls) *)
2727+let send_invite client ~room_id ~call_id ~offer ~lifetime
2828+ ?(version = 1) ?party_id ?invitee () =
2929+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
3030+ let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in
3131+ let path = Printf.sprintf "/rooms/%s/send/m.call.invite/%s"
3232+ (Uri.pct_encode room_id_str)
3333+ (Uri.pct_encode txn_id)
3434+ in
3535+ let content : Matrix_proto.Event.Call_invite_content.t = {
3636+ call_id;
3737+ party_id;
3838+ version;
3939+ lifetime;
4040+ offer;
4141+ invitee;
4242+ } in
4343+ match Client.encode_body Matrix_proto.Event.Call_invite_content.jsont content with
4444+ | Error e -> Error e
4545+ | Ok body ->
4646+ match Client.put client ~path ~body () with
4747+ | Error e -> Error e
4848+ | Ok resp_body ->
4949+ match Client.decode_response Messages.send_response_jsont resp_body with
5050+ | Error e -> Error e
5151+ | Ok resp -> Ok resp.event_id
5252+5353+(** Send call candidates (ICE candidates).
5454+5555+ @param room_id The room
5656+ @param call_id The call ID
5757+ @param candidates List of ICE candidates
5858+ @param version Call version
5959+ @param party_id Party ID for version 1 calls *)
6060+let send_candidates client ~room_id ~call_id ~candidates
6161+ ?(version = 1) ?party_id () =
6262+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
6363+ let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in
6464+ let path = Printf.sprintf "/rooms/%s/send/m.call.candidates/%s"
6565+ (Uri.pct_encode room_id_str)
6666+ (Uri.pct_encode txn_id)
6767+ in
6868+ let content : Matrix_proto.Event.Call_candidates_content.t = {
6969+ call_id;
7070+ party_id;
7171+ version;
7272+ candidates;
7373+ } in
7474+ match Client.encode_body Matrix_proto.Event.Call_candidates_content.jsont content with
7575+ | Error e -> Error e
7676+ | Ok body ->
7777+ match Client.put client ~path ~body () with
7878+ | Error e -> Error e
7979+ | Ok resp_body ->
8080+ match Client.decode_response Messages.send_response_jsont resp_body with
8181+ | Error e -> Error e
8282+ | Ok resp -> Ok resp.event_id
8383+8484+(** Send a call answer.
8585+8686+ @param room_id The room
8787+ @param call_id The call ID
8888+ @param answer The SDP answer
8989+ @param version Call version
9090+ @param party_id Party ID for version 1 calls *)
9191+let send_answer client ~room_id ~call_id ~answer
9292+ ?(version = 1) ?party_id () =
9393+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
9494+ let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in
9595+ let path = Printf.sprintf "/rooms/%s/send/m.call.answer/%s"
9696+ (Uri.pct_encode room_id_str)
9797+ (Uri.pct_encode txn_id)
9898+ in
9999+ let content : Matrix_proto.Event.Call_answer_content.t = {
100100+ call_id;
101101+ party_id;
102102+ version;
103103+ answer;
104104+ } in
105105+ match Client.encode_body Matrix_proto.Event.Call_answer_content.jsont content with
106106+ | Error e -> Error e
107107+ | Ok body ->
108108+ match Client.put client ~path ~body () with
109109+ | Error e -> Error e
110110+ | Ok resp_body ->
111111+ match Client.decode_response Messages.send_response_jsont resp_body with
112112+ | Error e -> Error e
113113+ | Ok resp -> Ok resp.event_id
114114+115115+(** Hang up a call.
116116+117117+ @param room_id The room
118118+ @param call_id The call ID
119119+ @param reason Optional hangup reason
120120+ @param version Call version
121121+ @param party_id Party ID for version 1 calls *)
122122+let send_hangup client ~room_id ~call_id ?reason
123123+ ?(version = 1) ?party_id () =
124124+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
125125+ let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in
126126+ let path = Printf.sprintf "/rooms/%s/send/m.call.hangup/%s"
127127+ (Uri.pct_encode room_id_str)
128128+ (Uri.pct_encode txn_id)
129129+ in
130130+ let content : Matrix_proto.Event.Call_hangup_content.t = {
131131+ call_id;
132132+ party_id;
133133+ version;
134134+ reason;
135135+ } in
136136+ match Client.encode_body Matrix_proto.Event.Call_hangup_content.jsont content with
137137+ | Error e -> Error e
138138+ | Ok body ->
139139+ match Client.put client ~path ~body () with
140140+ | Error e -> Error e
141141+ | Ok resp_body ->
142142+ match Client.decode_response Messages.send_response_jsont resp_body with
143143+ | Error e -> Error e
144144+ | Ok resp -> Ok resp.event_id
145145+146146+(** Reject an incoming call (same as hangup but with different semantics). *)
147147+let reject_call client ~room_id ~call_id ?(version = 1) ?party_id () =
148148+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
149149+ let txn_id = Matrix_proto.Id.Transaction_id.(generate () |> to_string) in
150150+ let path = Printf.sprintf "/rooms/%s/send/m.call.reject/%s"
151151+ (Uri.pct_encode room_id_str)
152152+ (Uri.pct_encode txn_id)
153153+ in
154154+ (* m.call.reject has the same structure as m.call.hangup *)
155155+ let content : Matrix_proto.Event.Call_hangup_content.t = {
156156+ call_id;
157157+ party_id;
158158+ version;
159159+ reason = None;
160160+ } in
161161+ match Client.encode_body Matrix_proto.Event.Call_hangup_content.jsont content with
162162+ | Error e -> Error e
163163+ | Ok body ->
164164+ match Client.put client ~path ~body () with
165165+ | Error e -> Error e
166166+ | Ok resp_body ->
167167+ match Client.decode_response Messages.send_response_jsont resp_body with
168168+ | Error e -> Error e
169169+ | Ok resp -> Ok resp.event_id
170170+171171+(** TURN server credentials *)
172172+type turn_server = {
173173+ username : string;
174174+ password : string;
175175+ uris : string list;
176176+ ttl : int;
177177+}
178178+179179+let turn_server_jsont =
180180+ Jsont.Object.(
181181+ map (fun username password uris ttl ->
182182+ { username; password; uris; ttl })
183183+ |> mem "username" Jsont.string ~enc:(fun t -> t.username)
184184+ |> mem "password" Jsont.string ~enc:(fun t -> t.password)
185185+ |> mem "uris" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.uris)
186186+ |> mem "ttl" Jsont.int ~enc:(fun t -> t.ttl)
187187+ |> finish)
188188+189189+(** Get TURN server credentials from the homeserver. *)
190190+let get_turn_server client =
191191+ match Client.get client ~path:"/voip/turnServer" () with
192192+ | Error e -> Error e
193193+ | Ok body -> Client.decode_response turn_server_jsont body
+137
lib/matrix_client/client.ml
···11+type config = {
22+ homeserver : Uri.t;
33+ user_agent : string option;
44+}
55+66+type session = {
77+ user_id : Matrix_proto.Id.User_id.t;
88+ access_token : string;
99+ device_id : Matrix_proto.Id.Device_id.t;
1010+ refresh_token : string option;
1111+}
1212+1313+type t = {
1414+ http : Requests.t;
1515+ config : config;
1616+ session : session option;
1717+}
1818+1919+let create ~sw ~config env =
2020+ let http = Requests.create ~sw env in
2121+ { http; config; session = None }
2222+2323+let with_session t session =
2424+ { t with session = Some session }
2525+2626+let session t = t.session
2727+let homeserver t = t.config.homeserver
2828+let is_logged_in t = Option.is_some t.session
2929+let access_token t = Option.map (fun s -> s.access_token) t.session
3030+let user_id t = Option.map (fun s -> s.user_id) t.session
3131+let device_id t = Option.map (fun s -> s.device_id) t.session
3232+3333+(* Matrix API base path *)
3434+let api_base = "/_matrix/client/v3"
3535+3636+let make_url t path query =
3737+ let base = t.config.homeserver in
3838+ let path = api_base ^ path in
3939+ let uri = Uri.with_path base path in
4040+ match query with
4141+ | None | Some [] -> uri
4242+ | Some q -> Uri.with_query' uri q
4343+4444+let auth_headers t =
4545+ match t.session with
4646+ | Some s ->
4747+ Requests.Headers.(empty |> set "Authorization" ("Bearer " ^ s.access_token))
4848+ | None ->
4949+ Requests.Headers.empty
5050+5151+let add_user_agent t headers =
5252+ match t.config.user_agent with
5353+ | Some ua -> Requests.Headers.set "User-Agent" ua headers
5454+ | None -> headers
5555+5656+let json_content_type headers =
5757+ Requests.Headers.set "Content-Type" "application/json" headers
5858+5959+let handle_response response =
6060+ let status = Requests.Response.status_code response in
6161+ let body = Requests.Response.text response in
6262+ if status >= 200 && status < 300 then
6363+ Ok body
6464+ else
6565+ (* Try to parse as Matrix error *)
6666+ match Jsont_bytesrw.decode_string Error.matrix_error_jsont body with
6767+ | Ok matrix_err -> Error (Error.Matrix_error matrix_err)
6868+ | Error _ -> Error (Error.Http_error { status; body })
6969+7070+let get t ~path ?query () =
7171+ try
7272+ let url = make_url t path query |> Uri.to_string in
7373+ let headers = auth_headers t |> add_user_agent t in
7474+ let response = Requests.get t.http ~headers url in
7575+ handle_response response
7676+ with
7777+ | exn -> Error (Error.Network_error (Printexc.to_string exn))
7878+7979+let post t ~path ?query ~body () =
8080+ try
8181+ let url = make_url t path query |> Uri.to_string in
8282+ let headers = auth_headers t |> add_user_agent t |> json_content_type in
8383+ let body = Requests.Body.of_string Requests.Mime.json body in
8484+ let response = Requests.post t.http ~headers ~body url in
8585+ handle_response response
8686+ with
8787+ | exn -> Error (Error.Network_error (Printexc.to_string exn))
8888+8989+let put t ~path ?query ~body () =
9090+ try
9191+ let url = make_url t path query |> Uri.to_string in
9292+ let headers = auth_headers t |> add_user_agent t |> json_content_type in
9393+ let body = Requests.Body.of_string Requests.Mime.json body in
9494+ let response = Requests.put t.http ~headers ~body url in
9595+ handle_response response
9696+ with
9797+ | exn -> Error (Error.Network_error (Printexc.to_string exn))
9898+9999+let delete t ~path ?query ?body () =
100100+ try
101101+ let url = make_url t path query |> Uri.to_string in
102102+ let headers = auth_headers t |> add_user_agent t in
103103+ let headers, body =
104104+ match body with
105105+ | Some b ->
106106+ (json_content_type headers,
107107+ Some (Requests.Body.of_string Requests.Mime.json b))
108108+ | None -> (headers, None)
109109+ in
110110+ let response = Requests.request t.http ~headers ?body ~method_:`DELETE url in
111111+ handle_response response
112112+ with
113113+ | exn -> Error (Error.Network_error (Printexc.to_string exn))
114114+115115+let post_unauthenticated t ~path ?query ~body () =
116116+ try
117117+ let url = make_url t path query |> Uri.to_string in
118118+ let headers =
119119+ Requests.Headers.empty
120120+ |> add_user_agent t
121121+ |> json_content_type
122122+ in
123123+ let body = Requests.Body.of_string Requests.Mime.json body in
124124+ let response = Requests.post t.http ~headers ~body url in
125125+ handle_response response
126126+ with
127127+ | exn -> Error (Error.Network_error (Printexc.to_string exn))
128128+129129+let decode_response jsont body =
130130+ match Jsont_bytesrw.decode_string jsont body with
131131+ | Ok v -> Ok v
132132+ | Error e -> Error (Error.Json_error e)
133133+134134+let encode_body jsont value =
135135+ match Jsont_bytesrw.encode_string jsont value with
136136+ | Ok s -> Ok s
137137+ | Error e -> Error (Error.Json_error e)
+110
lib/matrix_client/client.mli
···11+(** Matrix client type and lifecycle. *)
22+33+(** Client configuration. *)
44+type config = {
55+ homeserver : Uri.t;
66+ (** Homeserver URL (e.g., https://matrix.org) *)
77+ user_agent : string option;
88+ (** Optional custom User-Agent header *)
99+}
1010+1111+(** Session information after login. *)
1212+type session = {
1313+ user_id : Matrix_proto.Id.User_id.t;
1414+ access_token : string;
1515+ device_id : Matrix_proto.Id.Device_id.t;
1616+ refresh_token : string option;
1717+}
1818+1919+(** Matrix client. *)
2020+type t
2121+2222+(** Create a new client.
2323+2424+ The client is not logged in initially. Use {!Auth.login_password} or
2525+ {!restore_session} to authenticate.
2626+2727+ The environment must provide network and clock capabilities. *)
2828+val create :
2929+ sw:Eio.Switch.t ->
3030+ config:config ->
3131+ < net : _ Eio.Net.t ; clock : _ Eio.Time.clock ; fs : Eio.Fs.dir_ty Eio.Path.t ; .. > ->
3232+ t
3333+3434+(** Restore a client from a saved session.
3535+3636+ Returns a new client with the session set. The original client is unchanged. *)
3737+val with_session : t -> session -> t
3838+3939+(** Get current session if logged in. *)
4040+val session : t -> session option
4141+4242+(** Get the homeserver URL. *)
4343+val homeserver : t -> Uri.t
4444+4545+(** Check if client is logged in. *)
4646+val is_logged_in : t -> bool
4747+4848+(** Get the access token if logged in. *)
4949+val access_token : t -> string option
5050+5151+(** Get the user ID if logged in. *)
5252+val user_id : t -> Matrix_proto.Id.User_id.t option
5353+5454+(** Get the device ID if logged in. *)
5555+val device_id : t -> Matrix_proto.Id.Device_id.t option
5656+5757+(** {1 Internal HTTP helpers}
5858+5959+ These are used by the API modules. *)
6060+6161+(** Make a GET request. *)
6262+val get :
6363+ t ->
6464+ path:string ->
6565+ ?query:(string * string) list ->
6666+ unit ->
6767+ (string, Error.t) result
6868+6969+(** Make a POST request with JSON body. *)
7070+val post :
7171+ t ->
7272+ path:string ->
7373+ ?query:(string * string) list ->
7474+ body:string ->
7575+ unit ->
7676+ (string, Error.t) result
7777+7878+(** Make a PUT request with JSON body. *)
7979+val put :
8080+ t ->
8181+ path:string ->
8282+ ?query:(string * string) list ->
8383+ body:string ->
8484+ unit ->
8585+ (string, Error.t) result
8686+8787+(** Make a DELETE request. *)
8888+val delete :
8989+ t ->
9090+ path:string ->
9191+ ?query:(string * string) list ->
9292+ ?body:string ->
9393+ unit ->
9494+ (string, Error.t) result
9595+9696+(** Make a POST request without authentication (for login/register). *)
9797+val post_unauthenticated :
9898+ t ->
9999+ path:string ->
100100+ ?query:(string * string) list ->
101101+ body:string ->
102102+ unit ->
103103+ (string, Error.t) result
104104+105105+(** Decode a JSON response using a jsont codec. *)
106106+val decode_response : 'a Jsont.t -> string -> ('a, Error.t) result
107107+108108+(** Encode a value to JSON string using a jsont codec.
109109+ Returns Error if encoding fails. *)
110110+val encode_body : 'a Jsont.t -> 'a -> (string, Error.t) result
+89
lib/matrix_client/devices.ml
···11+(** Device management operations. *)
22+33+type device = {
44+ device_id : string;
55+ display_name : string option;
66+ last_seen_ip : string option;
77+ last_seen_ts : int64 option;
88+}
99+1010+let device_jsont =
1111+ Jsont.Object.(
1212+ map (fun device_id display_name last_seen_ip last_seen_ts ->
1313+ { device_id; display_name; last_seen_ip; last_seen_ts })
1414+ |> mem "device_id" Jsont.string
1515+ |> opt_mem "display_name" Jsont.string ~enc:(fun t -> t.display_name)
1616+ |> opt_mem "last_seen_ip" Jsont.string ~enc:(fun t -> t.last_seen_ip)
1717+ |> opt_mem "last_seen_ts" Jsont.int64 ~enc:(fun t -> t.last_seen_ts)
1818+ |> finish)
1919+2020+type devices_response = {
2121+ devices : device list;
2222+}
2323+2424+let devices_response_jsont =
2525+ Jsont.Object.(
2626+ map (fun devices -> { devices })
2727+ |> mem "devices" (Jsont.list device_jsont) ~dec_absent:[]
2828+ |> finish)
2929+3030+let get_devices client =
3131+ match Client.get client ~path:"/devices" () with
3232+ | Error e -> Error e
3333+ | Ok body ->
3434+ match Client.decode_response devices_response_jsont body with
3535+ | Error e -> Error e
3636+ | Ok resp -> Ok resp.devices
3737+3838+let get_device client ~device_id =
3939+ let path = Printf.sprintf "/devices/%s" (Uri.pct_encode device_id) in
4040+ match Client.get client ~path () with
4141+ | Error e -> Error e
4242+ | Ok body -> Client.decode_response device_jsont body
4343+4444+type update_device_request = {
4545+ display_name : string;
4646+} [@@warning "-69"]
4747+4848+let update_device_request_jsont =
4949+ Jsont.Object.(
5050+ map (fun display_name -> { display_name })
5151+ |> mem "display_name" Jsont.string
5252+ |> finish)
5353+5454+let update_device client ~device_id ~display_name =
5555+ let path = Printf.sprintf "/devices/%s" (Uri.pct_encode device_id) in
5656+ let request = { display_name } in
5757+ match Client.encode_body update_device_request_jsont request with
5858+ | Error e -> Error e
5959+ | Ok body ->
6060+ match Client.put client ~path ~body () with
6161+ | Error e -> Error e
6262+ | Ok _ -> Ok ()
6363+6464+(* Delete device - simplified without UIAA *)
6565+let delete_device client ~device_id =
6666+ let path = Printf.sprintf "/devices/%s" (Uri.pct_encode device_id) in
6767+ match Client.delete client ~path () with
6868+ | Error e -> Error e
6969+ | Ok _ -> Ok ()
7070+7171+(* Delete multiple devices - simplified without UIAA *)
7272+type delete_devices_request = {
7373+ devices : string list;
7474+} [@@warning "-69"]
7575+7676+let delete_devices_request_jsont =
7777+ Jsont.Object.(
7878+ map (fun devices -> { devices })
7979+ |> mem "devices" (Jsont.list Jsont.string)
8080+ |> finish)
8181+8282+let delete_devices client ~device_ids =
8383+ let request = { devices = device_ids } in
8484+ match Client.encode_body delete_devices_request_jsont request with
8585+ | Error e -> Error e
8686+ | Ok body ->
8787+ match Client.post client ~path:"/delete_devices" ~body () with
8888+ | Error e -> Error e
8989+ | Ok _ -> Ok ()
+43
lib/matrix_client/devices.mli
···11+(** Device management operations. *)
22+33+(** Device information. *)
44+type device = {
55+ device_id : string;
66+ display_name : string option;
77+ last_seen_ip : string option;
88+ last_seen_ts : int64 option;
99+}
1010+1111+(** Get all devices for the current user. *)
1212+val get_devices : Client.t -> (device list, Error.t) result
1313+1414+(** Get information about a specific device. *)
1515+val get_device :
1616+ Client.t ->
1717+ device_id:string ->
1818+ (device, Error.t) result
1919+2020+(** Update a device's display name. *)
2121+val update_device :
2222+ Client.t ->
2323+ device_id:string ->
2424+ display_name:string ->
2525+ (unit, Error.t) result
2626+2727+(** Delete a device.
2828+2929+ Note: This may require interactive authentication (UIAA)
3030+ which is not fully supported. *)
3131+val delete_device :
3232+ Client.t ->
3333+ device_id:string ->
3434+ (unit, Error.t) result
3535+3636+(** Delete multiple devices.
3737+3838+ Note: This may require interactive authentication (UIAA)
3939+ which is not fully supported. *)
4040+val delete_devices :
4141+ Client.t ->
4242+ device_ids:string list ->
4343+ (unit, Error.t) result
+182
lib/matrix_client/directory.ml
···11+(** Room directory and alias operations. *)
22+33+(* Alias resolution *)
44+type alias_info = {
55+ room_id : Matrix_proto.Id.Room_id.t;
66+ servers : string list;
77+}
88+99+let alias_info_jsont =
1010+ Jsont.Object.(
1111+ map (fun room_id servers -> { room_id; servers })
1212+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont
1313+ |> mem "servers" (Jsont.list Jsont.string) ~dec_absent:[]
1414+ |> finish)
1515+1616+let resolve_alias client ~alias =
1717+ let alias_str = Matrix_proto.Id.Room_alias.to_string alias in
1818+ let path = Printf.sprintf "/directory/room/%s" (Uri.pct_encode alias_str) in
1919+ match Client.get client ~path () with
2020+ | Error e -> Error e
2121+ | Ok body -> Client.decode_response alias_info_jsont body
2222+2323+(* Create alias *)
2424+type create_alias_request = {
2525+ room_id : Matrix_proto.Id.Room_id.t;
2626+} [@@warning "-69"]
2727+2828+let create_alias_request_jsont =
2929+ Jsont.Object.(
3030+ map (fun room_id -> { room_id })
3131+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont
3232+ |> finish)
3333+3434+let create_alias client ~alias ~room_id =
3535+ let alias_str = Matrix_proto.Id.Room_alias.to_string alias in
3636+ let path = Printf.sprintf "/directory/room/%s" (Uri.pct_encode alias_str) in
3737+ let request = { room_id } in
3838+ match Client.encode_body create_alias_request_jsont request with
3939+ | Error e -> Error e
4040+ | Ok body ->
4141+ match Client.put client ~path ~body () with
4242+ | Error e -> Error e
4343+ | Ok _ -> Ok ()
4444+4545+(* Delete alias *)
4646+let delete_alias client ~alias =
4747+ let alias_str = Matrix_proto.Id.Room_alias.to_string alias in
4848+ let path = Printf.sprintf "/directory/room/%s" (Uri.pct_encode alias_str) in
4949+ match Client.delete client ~path () with
5050+ | Error e -> Error e
5151+ | Ok _ -> Ok ()
5252+5353+(* Room visibility *)
5454+type visibility = [ `Public | `Private ]
5555+5656+type visibility_response = {
5757+ visibility : string;
5858+}
5959+6060+let visibility_response_jsont =
6161+ Jsont.Object.(
6262+ map (fun visibility -> { visibility })
6363+ |> mem "visibility" Jsont.string
6464+ |> finish)
6565+6666+let get_visibility client ~room_id =
6767+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
6868+ let path = Printf.sprintf "/directory/list/room/%s" (Uri.pct_encode room_id_str) in
6969+ match Client.get client ~path () with
7070+ | Error e -> Error e
7171+ | Ok body ->
7272+ match Client.decode_response visibility_response_jsont body with
7373+ | Error e -> Error e
7474+ | Ok resp ->
7575+ match resp.visibility with
7676+ | "public" -> Ok `Public
7777+ | _ -> Ok `Private
7878+7979+type set_visibility_request = {
8080+ visibility : string;
8181+} [@@warning "-69"]
8282+8383+let set_visibility_request_jsont =
8484+ Jsont.Object.(
8585+ map (fun visibility -> { visibility })
8686+ |> mem "visibility" Jsont.string
8787+ |> finish)
8888+8989+let set_visibility client ~room_id ~visibility =
9090+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
9191+ let path = Printf.sprintf "/directory/list/room/%s" (Uri.pct_encode room_id_str) in
9292+ let vis_str = match visibility with `Public -> "public" | `Private -> "private" in
9393+ let request = { visibility = vis_str } in
9494+ match Client.encode_body set_visibility_request_jsont request with
9595+ | Error e -> Error e
9696+ | Ok body ->
9797+ match Client.put client ~path ~body () with
9898+ | Error e -> Error e
9999+ | Ok _ -> Ok ()
100100+101101+(* Room directory search *)
102102+type search_filter = {
103103+ generic_search_term : string option;
104104+ room_types : string list option;
105105+}
106106+107107+type search_result = {
108108+ chunk : Rooms.public_room list;
109109+ next_batch : string option;
110110+ prev_batch : string option;
111111+ total_room_count_estimate : int option;
112112+}
113113+114114+type search_request = {
115115+ filter : search_filter_request option;
116116+ limit : int option;
117117+ since : string option;
118118+} [@@warning "-69"]
119119+120120+and search_filter_request = {
121121+ generic_search_term : string option;
122122+ room_types : string list option;
123123+} [@@warning "-69"]
124124+125125+let search_filter_request_jsont =
126126+ Jsont.Object.(
127127+ map (fun generic_search_term room_types ->
128128+ { generic_search_term; room_types })
129129+ |> opt_mem "generic_search_term" Jsont.string ~enc:(fun (t : search_filter_request) -> t.generic_search_term)
130130+ |> opt_mem "room_types" (Jsont.list Jsont.string) ~enc:(fun (t : search_filter_request) -> t.room_types)
131131+ |> finish)
132132+133133+let search_request_jsont =
134134+ Jsont.Object.(
135135+ map (fun filter limit since -> { filter; limit; since })
136136+ |> opt_mem "filter" search_filter_request_jsont ~enc:(fun (t : search_request) -> t.filter)
137137+ |> opt_mem "limit" Jsont.int ~enc:(fun (t : search_request) -> t.limit)
138138+ |> opt_mem "since" Jsont.string ~enc:(fun (t : search_request) -> t.since)
139139+ |> finish)
140140+141141+(* Reuse public_room_jsont from Rooms - need to duplicate here *)
142142+let public_room_jsont : Rooms.public_room Jsont.t =
143143+ Jsont.Object.(
144144+ map (fun room_id name topic num_joined_members world_readable guest_can_join avatar_url canonical_alias ->
145145+ ({ room_id; name; topic; num_joined_members; world_readable; guest_can_join; avatar_url; canonical_alias } : Rooms.public_room))
146146+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont
147147+ |> opt_mem "name" Jsont.string ~enc:(fun (t : Rooms.public_room) -> t.name)
148148+ |> opt_mem "topic" Jsont.string ~enc:(fun (t : Rooms.public_room) -> t.topic)
149149+ |> mem "num_joined_members" Jsont.int ~dec_absent:0 ~enc:(fun (t : Rooms.public_room) -> t.num_joined_members)
150150+ |> mem "world_readable" Jsont.bool ~dec_absent:false ~enc:(fun (t : Rooms.public_room) -> t.world_readable)
151151+ |> mem "guest_can_join" Jsont.bool ~dec_absent:false ~enc:(fun (t : Rooms.public_room) -> t.guest_can_join)
152152+ |> opt_mem "avatar_url" Jsont.string ~enc:(fun (t : Rooms.public_room) -> t.avatar_url)
153153+ |> opt_mem "canonical_alias" Jsont.string ~enc:(fun (t : Rooms.public_room) -> t.canonical_alias)
154154+ |> finish)
155155+156156+let search_result_jsont =
157157+ Jsont.Object.(
158158+ map (fun chunk next_batch prev_batch total_room_count_estimate ->
159159+ { chunk; next_batch; prev_batch; total_room_count_estimate })
160160+ |> mem "chunk" (Jsont.list public_room_jsont) ~dec_absent:[]
161161+ |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch)
162162+ |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch)
163163+ |> opt_mem "total_room_count_estimate" Jsont.int ~enc:(fun t -> t.total_room_count_estimate)
164164+ |> finish)
165165+166166+let search client ?server ?limit ?since ?(filter : search_filter option) () =
167167+ let path = "/publicRooms" in
168168+ let query = match server with
169169+ | Some s -> Some [("server", s)]
170170+ | None -> None
171171+ in
172172+ let filter_req : search_filter_request option = match filter with
173173+ | Some f -> Some { generic_search_term = f.generic_search_term; room_types = f.room_types }
174174+ | None -> None
175175+ in
176176+ let request = { filter = filter_req; limit; since } in
177177+ match Client.encode_body search_request_jsont request with
178178+ | Error e -> Error e
179179+ | Ok body ->
180180+ match Client.post client ~path ?query ~body () with
181181+ | Error e -> Error e
182182+ | Ok body -> Client.decode_response search_result_jsont body
+83
lib/matrix_client/directory.mli
···11+(** Room directory and alias operations. *)
22+33+(** {1 Room Aliases} *)
44+55+(** Alias resolution result. *)
66+type alias_info = {
77+ room_id : Matrix_proto.Id.Room_id.t;
88+ servers : string list;
99+}
1010+1111+(** Resolve a room alias to a room ID.
1212+1313+ Returns the room ID and a list of servers that know about the room. *)
1414+val resolve_alias :
1515+ Client.t ->
1616+ alias:Matrix_proto.Id.Room_alias.t ->
1717+ (alias_info, Error.t) result
1818+1919+(** Create a room alias pointing to a room. *)
2020+val create_alias :
2121+ Client.t ->
2222+ alias:Matrix_proto.Id.Room_alias.t ->
2323+ room_id:Matrix_proto.Id.Room_id.t ->
2424+ (unit, Error.t) result
2525+2626+(** Delete a room alias. *)
2727+val delete_alias :
2828+ Client.t ->
2929+ alias:Matrix_proto.Id.Room_alias.t ->
3030+ (unit, Error.t) result
3131+3232+(** {1 Room Visibility} *)
3333+3434+(** Room visibility in the directory. *)
3535+type visibility = [ `Public | `Private ]
3636+3737+(** Get a room's visibility in the directory. *)
3838+val get_visibility :
3939+ Client.t ->
4040+ room_id:Matrix_proto.Id.Room_id.t ->
4141+ (visibility, Error.t) result
4242+4343+(** Set a room's visibility in the directory.
4444+4545+ Requires appropriate permissions in the room. *)
4646+val set_visibility :
4747+ Client.t ->
4848+ room_id:Matrix_proto.Id.Room_id.t ->
4949+ visibility:visibility ->
5050+ (unit, Error.t) result
5151+5252+(** {1 Room Directory Search} *)
5353+5454+(** Search filter for public rooms. *)
5555+type search_filter = {
5656+ generic_search_term : string option;
5757+ (** Search term to filter room names and topics. *)
5858+ room_types : string list option;
5959+ (** Filter by room types (e.g., "m.space"). None includes all types. *)
6060+}
6161+6262+(** Public room search result. *)
6363+type search_result = {
6464+ chunk : Rooms.public_room list;
6565+ next_batch : string option;
6666+ prev_batch : string option;
6767+ total_room_count_estimate : int option;
6868+}
6969+7070+(** Search the public room directory.
7171+7272+ @param server Server to query (default: local homeserver).
7373+ @param limit Maximum number of rooms to return.
7474+ @param since Pagination token.
7575+ @param filter Search filter. *)
7676+val search :
7777+ Client.t ->
7878+ ?server:string ->
7979+ ?limit:int ->
8080+ ?since:string ->
8181+ ?filter:search_filter ->
8282+ unit ->
8383+ (search_result, Error.t) result
···11+(** Media operations. *)
22+33+(* Upload uses a different API base *)
44+let media_api_base_v3 = "/_matrix/media/v3"
55+66+(* Upload response - for future use when binary upload is implemented *)
77+type upload_response = {
88+ content_uri : string;
99+} [@@warning "-69"]
1010+1111+let _upload_response_jsont =
1212+ Jsont.Object.(
1313+ map (fun content_uri -> { content_uri })
1414+ |> mem "content_uri" Jsont.string
1515+ |> finish)
1616+1717+let upload client ~content_type:_ ~data:_ ?filename:_ () =
1818+ (* Note: This is a simplified version - actual implementation would need
1919+ direct access to Requests to send raw binary data with custom content-type *)
2020+ match Client.access_token client with
2121+ | None -> Error (Error.Network_error "Not logged in")
2222+ | Some _ ->
2323+ (* For now, we'll use the JSON helpers but this would need to be
2424+ a raw HTTP request in a full implementation *)
2525+ Error (Error.Network_error "Binary upload not yet implemented - use Requests directly")
2626+2727+(* Download uses media API *)
2828+let download client ~server_name ~media_id =
2929+ let homeserver = Client.homeserver client in
3030+ let path = Printf.sprintf "%s/download/%s/%s"
3131+ media_api_base_v3
3232+ (Uri.pct_encode server_name)
3333+ (Uri.pct_encode media_id)
3434+ in
3535+ let uri = Uri.with_path homeserver path in
3636+ let url = Uri.to_string uri in
3737+ (* Similar limitation as upload - needs raw HTTP access *)
3838+ Error (Error.Network_error ("Download not yet implemented: " ^ url))
3939+4040+(* Thumbnail *)
4141+let thumbnail client ~server_name ~media_id ~width ~height ?method_ () =
4242+ let homeserver = Client.homeserver client in
4343+ let path = Printf.sprintf "%s/thumbnail/%s/%s"
4444+ media_api_base_v3
4545+ (Uri.pct_encode server_name)
4646+ (Uri.pct_encode media_id)
4747+ in
4848+ let query = [
4949+ ("width", string_of_int width);
5050+ ("height", string_of_int height);
5151+ ] @ (match method_ with Some m -> [("method", m)] | None -> [])
5252+ in
5353+ let uri = Uri.with_path homeserver path in
5454+ let uri = Uri.with_query' uri query in
5555+ let url = Uri.to_string uri in
5656+ Error (Error.Network_error ("Thumbnail not yet implemented: " ^ url))
5757+5858+(* Parse mxc:// URI *)
5959+let parse_mxc mxc =
6060+ if not (String.starts_with ~prefix:"mxc://" mxc) then
6161+ None
6262+ else
6363+ let rest = String.sub mxc 6 (String.length mxc - 6) in
6464+ match String.index_opt rest '/' with
6565+ | None -> None
6666+ | Some i ->
6767+ let server_name = String.sub rest 0 i in
6868+ let media_id = String.sub rest (i + 1) (String.length rest - i - 1) in
6969+ Some (server_name, media_id)
7070+7171+(* Convert mxc:// to HTTP URL *)
7272+let mxc_to_http client ~mxc ?width ?height () =
7373+ match parse_mxc mxc with
7474+ | None -> None
7575+ | Some (server_name, media_id) ->
7676+ let homeserver = Client.homeserver client in
7777+ let path, query = match width, height with
7878+ | Some w, Some h ->
7979+ let path = Printf.sprintf "%s/thumbnail/%s/%s"
8080+ media_api_base_v3
8181+ (Uri.pct_encode server_name)
8282+ (Uri.pct_encode media_id)
8383+ in
8484+ path, [("width", string_of_int w); ("height", string_of_int h)]
8585+ | _ ->
8686+ let path = Printf.sprintf "%s/download/%s/%s"
8787+ media_api_base_v3
8888+ (Uri.pct_encode server_name)
8989+ (Uri.pct_encode media_id)
9090+ in
9191+ path, []
9292+ in
9393+ let uri = Uri.with_path homeserver path in
9494+ let uri = if query = [] then uri else Uri.with_query' uri query in
9595+ Some (Uri.to_string uri)
9696+9797+(* Configuration *)
9898+type config = {
9999+ upload_size : int option;
100100+}
101101+102102+let config_jsont =
103103+ Jsont.Object.(
104104+ map (fun upload_size -> { upload_size })
105105+ |> opt_mem "m.upload.size" Jsont.int ~enc:(fun t -> t.upload_size)
106106+ |> finish)
107107+108108+let get_config client =
109109+ let homeserver = Client.homeserver client in
110110+ let path = media_api_base_v3 ^ "/config" in
111111+ let uri = Uri.with_path homeserver path in
112112+ let url = Uri.to_string uri in
113113+ match Client.get client ~path:"/media/v3/config" () with
114114+ | Error (Error.Http_error { status = 404; _ }) ->
115115+ (* Try without the _matrix prefix that get adds *)
116116+ Error (Error.Network_error ("Media config not found: " ^ url))
117117+ | Error e -> Error e
118118+ | Ok body -> Client.decode_response config_jsont body
+75
lib/matrix_client/media.mli
···11+(** Media operations (upload, download, thumbnails). *)
22+33+(** {1 Upload} *)
44+55+(** Upload media content.
66+77+ Returns the mxc:// URI of the uploaded content.
88+99+ @param content_type MIME type of the content.
1010+ @param data The raw content bytes.
1111+ @param filename Optional filename for the content. *)
1212+val upload :
1313+ Client.t ->
1414+ content_type:string ->
1515+ data:string ->
1616+ ?filename:string ->
1717+ unit ->
1818+ (string, Error.t) result
1919+2020+(** {1 Download} *)
2121+2222+(** Download media content.
2323+2424+ Returns (data, content_type).
2525+2626+ @param server_name The server name from the mxc:// URI.
2727+ @param media_id The media ID from the mxc:// URI. *)
2828+val download :
2929+ Client.t ->
3030+ server_name:string ->
3131+ media_id:string ->
3232+ (string * string, Error.t) result
3333+3434+(** Download a thumbnail.
3535+3636+ @param width Desired width.
3737+ @param height Desired height.
3838+ @param method_ Resize method: "crop" or "scale". *)
3939+val thumbnail :
4040+ Client.t ->
4141+ server_name:string ->
4242+ media_id:string ->
4343+ width:int ->
4444+ height:int ->
4545+ ?method_:string ->
4646+ unit ->
4747+ (string * string, Error.t) result
4848+4949+(** {1 MXC URI Helpers} *)
5050+5151+(** Parse an mxc:// URI into (server_name, media_id). *)
5252+val parse_mxc : string -> (string * string) option
5353+5454+(** Convert an mxc:// URI to an HTTP(S) URL.
5555+5656+ @param width Optional width for thumbnail.
5757+ @param height Optional height for thumbnail. *)
5858+val mxc_to_http :
5959+ Client.t ->
6060+ mxc:string ->
6161+ ?width:int ->
6262+ ?height:int ->
6363+ unit ->
6464+ string option
6565+6666+(** {1 Configuration} *)
6767+6868+(** Media upload limits. *)
6969+type config = {
7070+ upload_size : int option;
7171+ (** Maximum upload size in bytes, if any. *)
7272+}
7373+7474+(** Get media configuration from the homeserver. *)
7575+val get_config : Client.t -> (config, Error.t) result
+232
lib/matrix_client/messages.ml
···11+(** Message sending and retrieval. *)
22+33+(* Transaction ID generator *)
44+let next_txn_id =
55+ let counter = ref 0 in
66+ fun () ->
77+ incr counter;
88+ Printf.sprintf "%d_%f" !counter (Unix.gettimeofday ())
99+1010+(* Send event response *)
1111+type send_response = {
1212+ event_id : Matrix_proto.Id.Event_id.t;
1313+}
1414+1515+let send_response_jsont =
1616+ Jsont.Object.(
1717+ map (fun event_id -> { event_id })
1818+ |> mem "event_id" Matrix_proto.Id.Event_id.jsont
1919+ |> finish)
2020+2121+(* Generic send event *)
2222+let send_event client ~room_id ~event_type ~content =
2323+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
2424+ let txn_id = next_txn_id () in
2525+ let path = Printf.sprintf "/rooms/%s/send/%s/%s"
2626+ (Uri.pct_encode room_id_str)
2727+ (Uri.pct_encode event_type)
2828+ (Uri.pct_encode txn_id)
2929+ in
3030+ match Client.encode_body Jsont.json content with
3131+ | Error e -> Error e
3232+ | Ok body ->
3333+ match Client.put client ~path ~body () with
3434+ | Error e -> Error e
3535+ | Ok body ->
3636+ match Client.decode_response send_response_jsont body with
3737+ | Error e -> Error e
3838+ | Ok resp -> Ok resp.event_id
3939+4040+(* Text message content *)
4141+type text_content = {
4242+ msgtype : string;
4343+ body : string;
4444+ format : string option;
4545+ formatted_body : string option;
4646+} [@@warning "-69"]
4747+4848+let text_content_jsont =
4949+ Jsont.Object.(
5050+ map (fun msgtype body format formatted_body ->
5151+ { msgtype; body; format; formatted_body })
5252+ |> mem "msgtype" Jsont.string
5353+ |> mem "body" Jsont.string
5454+ |> opt_mem "format" Jsont.string ~enc:(fun t -> t.format)
5555+ |> opt_mem "formatted_body" Jsont.string ~enc:(fun t -> t.formatted_body)
5656+ |> finish)
5757+5858+let send_text client ~room_id ~body ?format ?formatted_body () =
5959+ let content = { msgtype = "m.text"; body; format; formatted_body } in
6060+ match Client.encode_body text_content_jsont content with
6161+ | Error e -> Error e
6262+ | Ok json_str ->
6363+ match Client.decode_response Jsont.json json_str with
6464+ | Error e -> Error e
6565+ | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json
6666+6767+let send_emote client ~room_id ~body () =
6868+ let content = { msgtype = "m.emote"; body; format = None; formatted_body = None } in
6969+ match Client.encode_body text_content_jsont content with
7070+ | Error e -> Error e
7171+ | Ok json_str ->
7272+ match Client.decode_response Jsont.json json_str with
7373+ | Error e -> Error e
7474+ | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json
7575+7676+let send_notice client ~room_id ~body () =
7777+ let content = { msgtype = "m.notice"; body; format = None; formatted_body = None } in
7878+ match Client.encode_body text_content_jsont content with
7979+ | Error e -> Error e
8080+ | Ok json_str ->
8181+ match Client.decode_response Jsont.json json_str with
8282+ | Error e -> Error e
8383+ | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json
8484+8585+(* Media message content *)
8686+type media_content = {
8787+ msgtype : string;
8888+ body : string;
8989+ url : string;
9090+ info : Jsont.json option;
9191+} [@@warning "-69"]
9292+9393+let media_content_jsont =
9494+ Jsont.Object.(
9595+ map (fun msgtype body url info ->
9696+ { msgtype; body; url; info })
9797+ |> mem "msgtype" Jsont.string
9898+ |> mem "body" Jsont.string
9999+ |> mem "url" Jsont.string
100100+ |> opt_mem "info" Jsont.json ~enc:(fun t -> t.info)
101101+ |> finish)
102102+103103+let send_image client ~room_id ~body ~url ?info () =
104104+ let content = { msgtype = "m.image"; body; url; info } in
105105+ match Client.encode_body media_content_jsont content with
106106+ | Error e -> Error e
107107+ | Ok json_str ->
108108+ match Client.decode_response Jsont.json json_str with
109109+ | Error e -> Error e
110110+ | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json
111111+112112+let send_file client ~room_id ~body ~url ?info () =
113113+ let content = { msgtype = "m.file"; body; url; info } in
114114+ match Client.encode_body media_content_jsont content with
115115+ | Error e -> Error e
116116+ | Ok json_str ->
117117+ match Client.decode_response Jsont.json json_str with
118118+ | Error e -> Error e
119119+ | Ok json -> send_event client ~room_id ~event_type:"m.room.message" ~content:json
120120+121121+(* Redaction *)
122122+type redact_request = {
123123+ reason : string option;
124124+} [@@warning "-69"]
125125+126126+let redact_request_jsont =
127127+ Jsont.Object.(
128128+ map (fun reason -> { reason })
129129+ |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason)
130130+ |> finish)
131131+132132+let redact client ~room_id ~event_id ?reason () =
133133+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
134134+ let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in
135135+ let txn_id = next_txn_id () in
136136+ let path = Printf.sprintf "/rooms/%s/redact/%s/%s"
137137+ (Uri.pct_encode room_id_str)
138138+ (Uri.pct_encode event_id_str)
139139+ (Uri.pct_encode txn_id)
140140+ in
141141+ let request = { reason } in
142142+ match Client.encode_body redact_request_jsont request with
143143+ | Error e -> Error e
144144+ | Ok body ->
145145+ match Client.put client ~path ~body () with
146146+ | Error e -> Error e
147147+ | Ok body ->
148148+ match Client.decode_response send_response_jsont body with
149149+ | Error e -> Error e
150150+ | Ok resp -> Ok resp.event_id
151151+152152+(* Get messages *)
153153+type direction = Forward | Backward
154154+155155+type messages_response = {
156156+ start : string;
157157+ end_ : string option;
158158+ chunk : Matrix_proto.Event.Raw_event.t list;
159159+ state : Matrix_proto.Event.Raw_event.t list;
160160+}
161161+162162+let messages_response_jsont =
163163+ Jsont.Object.(
164164+ map (fun start end_ chunk state ->
165165+ { start; end_; chunk; state })
166166+ |> mem "start" Jsont.string
167167+ |> opt_mem "end" Jsont.string ~enc:(fun t -> t.end_)
168168+ |> mem "chunk" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[]
169169+ |> mem "state" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[]
170170+ |> finish)
171171+172172+let get_messages client ~room_id ~from ~dir ?limit ?filter () =
173173+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
174174+ let path = Printf.sprintf "/rooms/%s/messages" (Uri.pct_encode room_id_str) in
175175+ let dir_str = match dir with Forward -> "f" | Backward -> "b" in
176176+ let query =
177177+ [("from", from); ("dir", dir_str)]
178178+ |> (fun q -> match limit with Some l -> ("limit", string_of_int l) :: q | None -> q)
179179+ |> (fun q -> match filter with Some f -> ("filter", f) :: q | None -> q)
180180+ in
181181+ match Client.get client ~path ~query () with
182182+ | Error e -> Error e
183183+ | Ok body -> Client.decode_response messages_response_jsont body
184184+185185+(* Get single event *)
186186+let get_event client ~room_id ~event_id =
187187+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
188188+ let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in
189189+ let path = Printf.sprintf "/rooms/%s/event/%s"
190190+ (Uri.pct_encode room_id_str)
191191+ (Uri.pct_encode event_id_str)
192192+ in
193193+ match Client.get client ~path () with
194194+ | Error e -> Error e
195195+ | Ok body -> Client.decode_response Matrix_proto.Event.Raw_event.jsont body
196196+197197+(* Get context *)
198198+type context = {
199199+ start : string;
200200+ end_ : string;
201201+ event : Matrix_proto.Event.Raw_event.t;
202202+ events_before : Matrix_proto.Event.Raw_event.t list;
203203+ events_after : Matrix_proto.Event.Raw_event.t list;
204204+ state : Matrix_proto.Event.Raw_event.t list;
205205+}
206206+207207+let context_jsont =
208208+ Jsont.Object.(
209209+ map (fun start end_ event events_before events_after state ->
210210+ { start; end_; event; events_before; events_after; state })
211211+ |> mem "start" Jsont.string
212212+ |> mem "end" Jsont.string
213213+ |> mem "event" Matrix_proto.Event.Raw_event.jsont
214214+ |> mem "events_before" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[]
215215+ |> mem "events_after" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[]
216216+ |> mem "state" (Jsont.list Matrix_proto.Event.Raw_event.jsont) ~dec_absent:[]
217217+ |> finish)
218218+219219+let get_context client ~room_id ~event_id ?limit () =
220220+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
221221+ let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in
222222+ let path = Printf.sprintf "/rooms/%s/context/%s"
223223+ (Uri.pct_encode room_id_str)
224224+ (Uri.pct_encode event_id_str)
225225+ in
226226+ let query = match limit with
227227+ | Some l -> Some [("limit", string_of_int l)]
228228+ | None -> None
229229+ in
230230+ match Client.get client ~path ?query () with
231231+ | Error e -> Error e
232232+ | Ok body -> Client.decode_response context_jsont body
+146
lib/matrix_client/messages.mli
···11+(** Message sending and retrieval. *)
22+33+(** {1 Sending Messages} *)
44+55+(** Response from sending an event. *)
66+type send_response = {
77+ event_id : Matrix_proto.Id.Event_id.t;
88+}
99+1010+(** JSON codec for send_response. *)
1111+val send_response_jsont : send_response Jsont.t
1212+1313+(** Send a text message.
1414+1515+ @param format Optional format (e.g., "org.matrix.custom.html").
1616+ @param formatted_body HTML body when format is set. *)
1717+val send_text :
1818+ Client.t ->
1919+ room_id:Matrix_proto.Id.Room_id.t ->
2020+ body:string ->
2121+ ?format:string ->
2222+ ?formatted_body:string ->
2323+ unit ->
2424+ (Matrix_proto.Id.Event_id.t, Error.t) result
2525+2626+(** Send an emote message (like /me in IRC). *)
2727+val send_emote :
2828+ Client.t ->
2929+ room_id:Matrix_proto.Id.Room_id.t ->
3030+ body:string ->
3131+ unit ->
3232+ (Matrix_proto.Id.Event_id.t, Error.t) result
3333+3434+(** Send a notice message (bot/automated messages). *)
3535+val send_notice :
3636+ Client.t ->
3737+ room_id:Matrix_proto.Id.Room_id.t ->
3838+ body:string ->
3939+ unit ->
4040+ (Matrix_proto.Id.Event_id.t, Error.t) result
4141+4242+(** Send an image message.
4343+4444+ @param url The mxc:// URL of the uploaded image.
4545+ @param info Optional image info (width, height, size, mimetype). *)
4646+val send_image :
4747+ Client.t ->
4848+ room_id:Matrix_proto.Id.Room_id.t ->
4949+ body:string ->
5050+ url:string ->
5151+ ?info:Jsont.json ->
5252+ unit ->
5353+ (Matrix_proto.Id.Event_id.t, Error.t) result
5454+5555+(** Send a file message.
5656+5757+ @param url The mxc:// URL of the uploaded file. *)
5858+val send_file :
5959+ Client.t ->
6060+ room_id:Matrix_proto.Id.Room_id.t ->
6161+ body:string ->
6262+ url:string ->
6363+ ?info:Jsont.json ->
6464+ unit ->
6565+ (Matrix_proto.Id.Event_id.t, Error.t) result
6666+6767+(** Send a generic room event.
6868+6969+ @param event_type The event type (e.g., "m.room.message").
7070+ @param content The event content as JSON. *)
7171+val send_event :
7272+ Client.t ->
7373+ room_id:Matrix_proto.Id.Room_id.t ->
7474+ event_type:string ->
7575+ content:Jsont.json ->
7676+ (Matrix_proto.Id.Event_id.t, Error.t) result
7777+7878+(** {1 Redaction} *)
7979+8080+(** Redact an event.
8181+8282+ @param reason Optional reason for the redaction. *)
8383+val redact :
8484+ Client.t ->
8585+ room_id:Matrix_proto.Id.Room_id.t ->
8686+ event_id:Matrix_proto.Id.Event_id.t ->
8787+ ?reason:string ->
8888+ unit ->
8989+ (Matrix_proto.Id.Event_id.t, Error.t) result
9090+9191+(** {1 Retrieving Messages} *)
9292+9393+(** Direction for message retrieval. *)
9494+type direction = Forward | Backward
9595+9696+(** Messages response. *)
9797+type messages_response = {
9898+ start : string;
9999+ end_ : string option;
100100+ chunk : Matrix_proto.Event.Raw_event.t list;
101101+ state : Matrix_proto.Event.Raw_event.t list;
102102+}
103103+104104+(** Get messages from a room.
105105+106106+ @param from Pagination token to start from.
107107+ @param dir Direction to paginate.
108108+ @param limit Maximum number of events to return.
109109+ @param filter Event filter (as filter ID or JSON). *)
110110+val get_messages :
111111+ Client.t ->
112112+ room_id:Matrix_proto.Id.Room_id.t ->
113113+ from:string ->
114114+ dir:direction ->
115115+ ?limit:int ->
116116+ ?filter:string ->
117117+ unit ->
118118+ (messages_response, Error.t) result
119119+120120+(** Get a single event by ID. *)
121121+val get_event :
122122+ Client.t ->
123123+ room_id:Matrix_proto.Id.Room_id.t ->
124124+ event_id:Matrix_proto.Id.Event_id.t ->
125125+ (Matrix_proto.Event.Raw_event.t, Error.t) result
126126+127127+(** Context around an event. *)
128128+type context = {
129129+ start : string;
130130+ end_ : string;
131131+ event : Matrix_proto.Event.Raw_event.t;
132132+ events_before : Matrix_proto.Event.Raw_event.t list;
133133+ events_after : Matrix_proto.Event.Raw_event.t list;
134134+ state : Matrix_proto.Event.Raw_event.t list;
135135+}
136136+137137+(** Get context around an event.
138138+139139+ @param limit Number of events to return before and after. *)
140140+val get_context :
141141+ Client.t ->
142142+ room_id:Matrix_proto.Id.Room_id.t ->
143143+ event_id:Matrix_proto.Id.Event_id.t ->
144144+ ?limit:int ->
145145+ unit ->
146146+ (context, Error.t) result
+895
lib/matrix_client/olm.ml
···11+(** Olm/Megolm cryptographic session management.
22+33+ This module implements the Olm double-ratchet algorithm for encrypted
44+ to-device messages, and Megolm for encrypted room messages. *)
55+66+module Ed25519 = Mirage_crypto_ec.Ed25519
77+module X25519 = Mirage_crypto_ec.X25519
88+99+(* Base64 encoding/decoding - Matrix uses unpadded base64 *)
1010+let base64_encode s = Base64.encode_string ~pad:false s
1111+let base64_decode s = Base64.decode ~pad:false s
1212+1313+(** Olm account - manages identity keys and one-time keys.
1414+1515+ An Olm account contains:
1616+ - Ed25519 identity key (for signing)
1717+ - Curve25519 identity key (for key exchange)
1818+ - One-time keys (for session establishment) *)
1919+module Account = struct
2020+ type t = {
2121+ (* Identity keys *)
2222+ ed25519_priv : Ed25519.priv;
2323+ ed25519_pub : Ed25519.pub;
2424+ curve25519_secret : X25519.secret;
2525+ curve25519_public : string;
2626+ (* One-time keys - key_id -> (secret, public) *)
2727+ mutable one_time_keys : (string * (X25519.secret * string)) list;
2828+ (* Fallback keys *)
2929+ mutable fallback_key : (string * (X25519.secret * string)) option;
3030+ (* Key counter for generating key IDs *)
3131+ mutable next_key_id : int;
3232+ (* Max number of one-time keys to store *)
3333+ max_one_time_keys : int;
3434+ }
3535+3636+ (** Generate a new Olm account with fresh identity keys. *)
3737+ let create () =
3838+ let ed25519_priv, ed25519_pub = Ed25519.generate () in
3939+ let curve25519_secret, curve25519_public = X25519.gen_key () in
4040+ {
4141+ ed25519_priv;
4242+ ed25519_pub;
4343+ curve25519_secret;
4444+ curve25519_public;
4545+ one_time_keys = [];
4646+ fallback_key = None;
4747+ next_key_id = 0;
4848+ max_one_time_keys = 100;
4949+ }
5050+5151+ (** Get the Ed25519 identity key as base64. *)
5252+ let ed25519_key t =
5353+ Ed25519.pub_to_octets t.ed25519_pub |> base64_encode
5454+5555+ (** Get the Curve25519 identity key as base64. *)
5656+ let curve25519_key t =
5757+ base64_encode t.curve25519_public
5858+5959+ (** Get the identity keys as a pair (ed25519, curve25519). *)
6060+ let identity_keys t =
6161+ (ed25519_key t, curve25519_key t)
6262+6363+ (** Sign a message with the account's Ed25519 key. *)
6464+ let sign t message =
6565+ let signature = Ed25519.sign ~key:t.ed25519_priv message in
6666+ base64_encode signature
6767+6868+ (** Generate a unique key ID. *)
6969+ let generate_key_id t =
7070+ let id = Printf.sprintf "AAAA%02dAA" t.next_key_id in
7171+ t.next_key_id <- t.next_key_id + 1;
7272+ id
7373+7474+ (** Generate new one-time keys. *)
7575+ let generate_one_time_keys t count =
7676+ let count = min count (t.max_one_time_keys - List.length t.one_time_keys) in
7777+ for _ = 1 to count do
7878+ let secret, public = X25519.gen_key () in
7979+ let key_id = generate_key_id t in
8080+ t.one_time_keys <- (key_id, (secret, public)) :: t.one_time_keys
8181+ done
8282+8383+ (** Get one-time keys for upload (key_id -> public_key). *)
8484+ let one_time_keys t =
8585+ List.map (fun (key_id, (_secret, public)) ->
8686+ (key_id, base64_encode public)
8787+ ) t.one_time_keys
8888+8989+ (** Get signed one-time keys for upload. *)
9090+ let signed_one_time_keys t =
9191+ List.map (fun (key_id, (_secret, public)) ->
9292+ let pub_b64 = base64_encode public in
9393+ let to_sign = Printf.sprintf {|{"key":"%s"}|} pub_b64 in
9494+ let signature = sign t to_sign in
9595+ (key_id, pub_b64, signature)
9696+ ) t.one_time_keys
9797+9898+ (** Mark one-time keys as published (remove them from pending). *)
9999+ let mark_keys_as_published _t =
100100+ (* One-time keys are kept until used in a session *)
101101+ ()
102102+103103+ (** Generate a fallback key. *)
104104+ let generate_fallback_key t =
105105+ let secret, public = X25519.gen_key () in
106106+ let key_id = generate_key_id t in
107107+ t.fallback_key <- Some (key_id, (secret, public))
108108+109109+ (** Get the fallback key if one exists. *)
110110+ let fallback_key t =
111111+ match t.fallback_key with
112112+ | Some (key_id, (_secret, public)) -> Some (key_id, base64_encode public)
113113+ | None -> None
114114+115115+ (** Remove a one-time key by ID (after session creation). *)
116116+ let remove_one_time_key t key_id =
117117+ t.one_time_keys <- List.filter (fun (id, _) -> id <> key_id) t.one_time_keys
118118+119119+ (** Get the secret for a one-time key (for session creation). *)
120120+ let get_one_time_key_secret t key_id =
121121+ match List.assoc_opt key_id t.one_time_keys with
122122+ | Some (secret, _public) -> Some secret
123123+ | None ->
124124+ (* Check fallback key *)
125125+ match t.fallback_key with
126126+ | Some (id, (secret, _public)) when id = key_id -> Some secret
127127+ | _ -> None
128128+129129+ (** Number of unpublished one-time keys. *)
130130+ let one_time_keys_count t = List.length t.one_time_keys
131131+132132+ (** Maximum number of one-time keys this account can hold. *)
133133+ let max_one_time_keys t = t.max_one_time_keys
134134+end
135135+136136+(** Olm session state for the double ratchet algorithm. *)
137137+module Session = struct
138138+ (** Ratchet chain key *)
139139+ type chain_key = {
140140+ key : string; (* 32 bytes *)
141141+ index : int;
142142+ }
143143+144144+ (** Root key used for deriving new chain keys *)
145145+ type root_key = string (* 32 bytes *)
146146+147147+ (** Message key for encrypting a single message *)
148148+ type message_key = string (* 32 bytes *)
149149+150150+ (** Session state *)
151151+ type t = {
152152+ (* Session ID *)
153153+ session_id : string;
154154+ (* Their identity key (Curve25519) *)
155155+ their_identity_key : string;
156156+ (* Their current ratchet key *)
157157+ mutable their_ratchet_key : string option;
158158+ (* Our current ratchet key pair *)
159159+ mutable our_ratchet_secret : X25519.secret;
160160+ mutable our_ratchet_public : string;
161161+ (* Root key for deriving chain keys *)
162162+ mutable root_key : root_key;
163163+ (* Sending chain *)
164164+ mutable sending_chain : chain_key option;
165165+ (* Receiving chains (their_ratchet_key -> chain) *)
166166+ mutable receiving_chains : (string * chain_key) list;
167167+ (* Skipped message keys for out-of-order decryption *)
168168+ mutable skipped_keys : ((string * int) * message_key) list;
169169+ (* Creation time *)
170170+ creation_time : Ptime.t;
171171+ }
172172+173173+ (** HKDF for key derivation using SHA-256 *)
174174+ let hkdf_sha256 ~salt ~info ~ikm length =
175175+ let prk = Hkdf.extract ~hash:`SHA256 ~salt ikm in
176176+ Hkdf.expand ~hash:`SHA256 ~prk ~info length
177177+178178+ (** Derive root and chain keys from shared secret *)
179179+ let kdf_rk root_key shared_secret =
180180+ let derived = hkdf_sha256
181181+ ~salt:root_key
182182+ ~info:"OLM_ROOT"
183183+ ~ikm:shared_secret
184184+ 64
185185+ in
186186+ let new_root = String.sub derived 0 32 in
187187+ let chain_key = String.sub derived 32 32 in
188188+ (new_root, chain_key)
189189+190190+ (** Derive next chain key and message key *)
191191+ let kdf_ck chain_key =
192192+ let mk = hkdf_sha256
193193+ ~salt:""
194194+ ~info:"OLM_CHAIN_MESSAGE"
195195+ ~ikm:chain_key.key
196196+ 32
197197+ in
198198+ let new_ck = hkdf_sha256
199199+ ~salt:""
200200+ ~info:"OLM_CHAIN_KEY"
201201+ ~ikm:chain_key.key
202202+ 32
203203+ in
204204+ (mk, { key = new_ck; index = chain_key.index + 1 })
205205+206206+ (** Perform X3DH key agreement for outbound session *)
207207+ let x3dh_outbound ~our_identity_secret ~our_ephemeral_secret
208208+ ~their_identity_key ~their_one_time_key =
209209+ (* DH1: our_identity_secret, their_one_time_key *)
210210+ let dh1 = match X25519.key_exchange our_identity_secret their_one_time_key with
211211+ | Ok s -> s
212212+ | Error _ -> failwith "Key exchange failed"
213213+ in
214214+ (* DH2: our_ephemeral_secret, their_identity_key *)
215215+ let dh2 = match X25519.key_exchange our_ephemeral_secret their_identity_key with
216216+ | Ok s -> s
217217+ | Error _ -> failwith "Key exchange failed"
218218+ in
219219+ (* DH3: our_ephemeral_secret, their_one_time_key *)
220220+ let dh3 = match X25519.key_exchange our_ephemeral_secret their_one_time_key with
221221+ | Ok s -> s
222222+ | Error _ -> failwith "Key exchange failed"
223223+ in
224224+ (* Combine: DH1 || DH2 || DH3 *)
225225+ dh1 ^ dh2 ^ dh3
226226+227227+ (** Create a new outbound session (when sending first message). *)
228228+ let create_outbound account ~their_identity_key ~their_one_time_key =
229229+ (* Parse their keys from base64 *)
230230+ let their_identity = match base64_decode their_identity_key with
231231+ | Ok k -> k
232232+ | Error _ -> failwith "Invalid identity key"
233233+ in
234234+ let their_otk = match base64_decode their_one_time_key with
235235+ | Ok k -> k
236236+ | Error _ -> failwith "Invalid one-time key"
237237+ in
238238+ (* Generate ephemeral key for X3DH *)
239239+ let ephemeral_secret, _ephemeral_public = X25519.gen_key () in
240240+ (* Perform X3DH *)
241241+ let shared_secret = x3dh_outbound
242242+ ~our_identity_secret:account.Account.curve25519_secret
243243+ ~our_ephemeral_secret:ephemeral_secret
244244+ ~their_identity_key:their_identity
245245+ ~their_one_time_key:their_otk
246246+ in
247247+ (* Derive root key *)
248248+ let root_key = hkdf_sha256
249249+ ~salt:""
250250+ ~info:"OLM_ROOT"
251251+ ~ikm:shared_secret
252252+ 32
253253+ in
254254+ (* Generate our initial ratchet key *)
255255+ let our_ratchet_secret, our_ratchet_public = X25519.gen_key () in
256256+ (* Session ID is hash of the root key *)
257257+ let session_id =
258258+ Digestif.SHA256.(digest_string root_key |> to_raw_string)
259259+ |> base64_encode
260260+ in
261261+ let now = match Ptime.of_float_s (Unix.gettimeofday ()) with
262262+ | Some t -> t
263263+ | None -> Ptime.epoch
264264+ in
265265+ (* Initial sending chain *)
266266+ let sending_chain = Some { key = root_key; index = 0 } in
267267+ {
268268+ session_id;
269269+ their_identity_key = their_identity;
270270+ their_ratchet_key = None;
271271+ our_ratchet_secret;
272272+ our_ratchet_public;
273273+ root_key;
274274+ sending_chain;
275275+ receiving_chains = [];
276276+ skipped_keys = [];
277277+ creation_time = now;
278278+ }
279279+280280+ (** Create a new inbound session (when receiving first message). *)
281281+ let create_inbound account ~their_identity_key ~their_ephemeral_key ~one_time_key_id =
282282+ (* Get our one-time key secret *)
283283+ let our_otk_secret = match Account.get_one_time_key_secret account one_time_key_id with
284284+ | Some s -> s
285285+ | None -> failwith "One-time key not found"
286286+ in
287287+ (* Parse their keys *)
288288+ let their_identity = match base64_decode their_identity_key with
289289+ | Ok k -> k
290290+ | Error _ -> failwith "Invalid identity key"
291291+ in
292292+ let their_ephemeral = match base64_decode their_ephemeral_key with
293293+ | Ok k -> k
294294+ | Error _ -> failwith "Invalid ephemeral key"
295295+ in
296296+ (* Perform reverse X3DH *)
297297+ (* DH1: their_identity, our_otk *)
298298+ let dh1 = match X25519.key_exchange our_otk_secret their_identity with
299299+ | Ok s -> s
300300+ | Error _ -> failwith "Key exchange failed"
301301+ in
302302+ (* DH2: their_ephemeral, our_identity *)
303303+ let dh2 = match X25519.key_exchange account.Account.curve25519_secret their_ephemeral with
304304+ | Ok s -> s
305305+ | Error _ -> failwith "Key exchange failed"
306306+ in
307307+ (* DH3: their_ephemeral, our_otk *)
308308+ let dh3 = match X25519.key_exchange our_otk_secret their_ephemeral with
309309+ | Ok s -> s
310310+ | Error _ -> failwith "Key exchange failed"
311311+ in
312312+ let shared_secret = dh1 ^ dh2 ^ dh3 in
313313+ (* Derive root key *)
314314+ let root_key = hkdf_sha256
315315+ ~salt:""
316316+ ~info:"OLM_ROOT"
317317+ ~ikm:shared_secret
318318+ 32
319319+ in
320320+ (* Generate our ratchet key *)
321321+ let our_ratchet_secret, our_ratchet_public = X25519.gen_key () in
322322+ let session_id =
323323+ Digestif.SHA256.(digest_string root_key |> to_raw_string)
324324+ |> base64_encode
325325+ in
326326+ let now = match Ptime.of_float_s (Unix.gettimeofday ()) with
327327+ | Some t -> t
328328+ | None -> Ptime.epoch
329329+ in
330330+ (* Remove the used one-time key *)
331331+ Account.remove_one_time_key account one_time_key_id;
332332+ {
333333+ session_id;
334334+ their_identity_key = their_identity;
335335+ their_ratchet_key = Some their_ephemeral;
336336+ our_ratchet_secret;
337337+ our_ratchet_public;
338338+ root_key;
339339+ sending_chain = None;
340340+ receiving_chains = [(their_ephemeral, { key = root_key; index = 0 })];
341341+ skipped_keys = [];
342342+ creation_time = now;
343343+ }
344344+345345+ (** Get session ID *)
346346+ let session_id t = t.session_id
347347+348348+ (** Get their identity key *)
349349+ let their_identity_key t = base64_encode t.their_identity_key
350350+351351+ (** Encrypt a message using AES-256-CBC with HMAC-SHA256 *)
352352+ let aes_encrypt key plaintext =
353353+ (* Use first 32 bytes for AES key, derive IV *)
354354+ let aes_key = String.sub key 0 32 in
355355+ let iv = Digestif.SHA256.(digest_string (aes_key ^ "IV") |> to_raw_string)
356356+ |> fun s -> String.sub s 0 16 in
357357+ (* PKCS7 padding *)
358358+ let block_size = 16 in
359359+ let pad_len = block_size - (String.length plaintext mod block_size) in
360360+ let padded = plaintext ^ String.make pad_len (Char.chr pad_len) in
361361+ (* Encrypt using mirage-crypto AES.CBC *)
362362+ let cipher = Mirage_crypto.AES.CBC.of_secret aes_key in
363363+ let encrypted = Mirage_crypto.AES.CBC.encrypt ~key:cipher ~iv padded in
364364+ iv ^ encrypted
365365+366366+ (** Decrypt a message *)
367367+ let aes_decrypt key ciphertext =
368368+ if String.length ciphertext < 16 then
369369+ Error "Ciphertext too short"
370370+ else
371371+ let iv = String.sub ciphertext 0 16 in
372372+ let data = String.sub ciphertext 16 (String.length ciphertext - 16) in
373373+ let aes_key = String.sub key 0 32 in
374374+ let cipher = Mirage_crypto.AES.CBC.of_secret aes_key in
375375+ let decrypted = Mirage_crypto.AES.CBC.decrypt ~key:cipher ~iv data in
376376+ (* Remove PKCS7 padding *)
377377+ if String.length decrypted = 0 then
378378+ Error "Empty plaintext"
379379+ else
380380+ let pad_len = Char.code decrypted.[String.length decrypted - 1] in
381381+ if pad_len > 16 || pad_len > String.length decrypted then
382382+ Error "Invalid padding"
383383+ else
384384+ Ok (String.sub decrypted 0 (String.length decrypted - pad_len))
385385+386386+ (** Encrypt a plaintext message. Returns (message_type, ciphertext). *)
387387+ let encrypt t plaintext =
388388+ (* Get or create sending chain *)
389389+ let chain = match t.sending_chain with
390390+ | Some c -> c
391391+ | None ->
392392+ (* Need to ratchet first *)
393393+ let new_secret, new_public = X25519.gen_key () in
394394+ t.our_ratchet_secret <- new_secret;
395395+ t.our_ratchet_public <- new_public;
396396+ { key = t.root_key; index = 0 }
397397+ in
398398+ (* Derive message key *)
399399+ let message_key, new_chain = kdf_ck chain in
400400+ t.sending_chain <- Some new_chain;
401401+ (* Encrypt the message *)
402402+ let ciphertext = aes_encrypt message_key plaintext in
403403+ (* Create message payload with ratchet key and chain index *)
404404+ let ratchet_key_b64 = base64_encode t.our_ratchet_public in
405405+ let msg_type = if chain.index = 0 then 0 else 1 in (* 0 = prekey, 1 = normal *)
406406+ let payload = Printf.sprintf "%s|%d|%s"
407407+ ratchet_key_b64
408408+ new_chain.index
409409+ (base64_encode ciphertext)
410410+ in
411411+ (msg_type, payload)
412412+413413+ (** Decrypt a message. *)
414414+ let decrypt t ~message_type ~ciphertext:payload =
415415+ (* Parse payload *)
416416+ match String.split_on_char '|' payload with
417417+ | [ratchet_key_b64; index_str; ciphertext_b64] ->
418418+ let their_ratchet = match base64_decode ratchet_key_b64 with
419419+ | Ok k -> k
420420+ | Error _ -> failwith "Invalid ratchet key"
421421+ in
422422+ let msg_index = int_of_string index_str in
423423+ let ciphertext = match base64_decode ciphertext_b64 with
424424+ | Ok c -> c
425425+ | Error _ -> failwith "Invalid ciphertext"
426426+ in
427427+ (* Check if we need to advance the ratchet *)
428428+ let _need_ratchet = match t.their_ratchet_key with
429429+ | Some k when k = their_ratchet -> false
430430+ | _ -> true
431431+ in
432432+ (* Find or create receiving chain *)
433433+ let chain = match List.assoc_opt their_ratchet t.receiving_chains with
434434+ | Some c -> c
435435+ | None ->
436436+ (* New ratchet - derive new chain *)
437437+ let dh_out = match X25519.key_exchange t.our_ratchet_secret their_ratchet with
438438+ | Ok s -> s
439439+ | Error _ -> failwith "Key exchange failed"
440440+ in
441441+ let new_root, chain_key = kdf_rk t.root_key dh_out in
442442+ t.root_key <- new_root;
443443+ t.their_ratchet_key <- Some their_ratchet;
444444+ let chain = { key = chain_key; index = 0 } in
445445+ t.receiving_chains <- (their_ratchet, chain) :: t.receiving_chains;
446446+ chain
447447+ in
448448+ (* Advance chain to the right index *)
449449+ let rec advance_chain c target_idx =
450450+ if c.index >= target_idx then c
451451+ else
452452+ let mk, new_c = kdf_ck c in
453453+ (* Store skipped keys *)
454454+ t.skipped_keys <- ((their_ratchet, c.index), mk) :: t.skipped_keys;
455455+ advance_chain new_c target_idx
456456+ in
457457+ let chain = advance_chain chain msg_index in
458458+ (* Get message key *)
459459+ let message_key, new_chain = kdf_ck chain in
460460+ (* Update chain *)
461461+ t.receiving_chains <-
462462+ (their_ratchet, new_chain) ::
463463+ (List.filter (fun (k, _) -> k <> their_ratchet) t.receiving_chains);
464464+ (* Decrypt *)
465465+ let _ = message_type in
466466+ aes_decrypt message_key ciphertext
467467+ | _ -> Error "Invalid message format"
468468+469469+ (** Check if this is a pre-key message (first message in session). *)
470470+ let is_pre_key_message message_type = message_type = 0
471471+end
472472+473473+(** Megolm session for room message encryption.
474474+475475+ Megolm uses a ratchet that only moves forward, making it efficient
476476+ for encrypting many messages to many recipients. *)
477477+module Megolm = struct
478478+ (** Inbound session for decrypting received room messages *)
479479+ module Inbound = struct
480480+ type t = {
481481+ session_id : string;
482482+ sender_key : string; (* Curve25519 key of sender *)
483483+ room_id : string;
484484+ (* Ratchet state - 4 parts of 256 bits each *)
485485+ mutable ratchet : string array; (* 4 x 32 bytes *)
486486+ mutable message_index : int;
487487+ (* For detecting replays *)
488488+ mutable received_indices : int list;
489489+ (* Ed25519 signing key of the sender *)
490490+ signing_key : string;
491491+ creation_time : Ptime.t;
492492+ }
493493+494494+ (** Advance the ratchet by one step *)
495495+ let advance_ratchet t =
496496+ (* Megolm ratchet: each part hashes the parts below it *)
497497+ let hash s = Digestif.SHA256.(digest_string s |> to_raw_string) in
498498+ (* R(i,j) = H(R(i-1,j) || j) for j = 0,1,2,3 *)
499499+ (* Simplified: we just hash each part with its index *)
500500+ let i = t.message_index land 3 in
501501+ for j = i to 3 do
502502+ t.ratchet.(j) <- hash (t.ratchet.(j) ^ string_of_int j)
503503+ done;
504504+ t.message_index <- t.message_index + 1
505505+506506+ (** Create from exported session data *)
507507+ let of_export ~session_id ~sender_key ~room_id ~ratchet ~message_index ~signing_key =
508508+ let now = match Ptime.of_float_s (Unix.gettimeofday ()) with
509509+ | Some t -> t
510510+ | None -> Ptime.epoch
511511+ in
512512+ {
513513+ session_id;
514514+ sender_key;
515515+ room_id;
516516+ ratchet;
517517+ message_index;
518518+ received_indices = [];
519519+ signing_key;
520520+ creation_time = now;
521521+ }
522522+523523+ (** Create from room key event (m.room_key) *)
524524+ let from_room_key ~sender_key ~room_id ~session_id ~session_key ~signing_key =
525525+ (* Parse session_key which contains ratchet state *)
526526+ let ratchet = match base64_decode session_key with
527527+ | Ok data when String.length data >= 128 ->
528528+ [|
529529+ String.sub data 0 32;
530530+ String.sub data 32 32;
531531+ String.sub data 64 32;
532532+ String.sub data 96 32;
533533+ |]
534534+ | _ ->
535535+ (* Generate random initial state if parsing fails *)
536536+ let random_part () =
537537+ Mirage_crypto_rng.generate 32
538538+ in
539539+ [| random_part (); random_part (); random_part (); random_part () |]
540540+ in
541541+ of_export ~session_id ~sender_key ~room_id ~ratchet ~message_index:0 ~signing_key
542542+543543+ let session_id t = t.session_id
544544+ let sender_key t = t.sender_key
545545+ let room_id t = t.room_id
546546+ let first_known_index t = t.message_index
547547+548548+ (** Derive encryption key from current ratchet state *)
549549+ let derive_key t =
550550+ let combined = String.concat "" (Array.to_list t.ratchet) in
551551+ Hkdf.expand ~hash:`SHA256 ~prk:combined ~info:"MEGOLM_KEYS" 80
552552+553553+ (** Decrypt a message *)
554554+ let decrypt t ~ciphertext ~message_index =
555555+ (* Check for replay *)
556556+ if List.mem message_index t.received_indices then
557557+ Error "Duplicate message index (replay attack)"
558558+ else if message_index < t.message_index then
559559+ Error "Message index too old"
560560+ else begin
561561+ (* Advance ratchet to the right position *)
562562+ while t.message_index < message_index do
563563+ advance_ratchet t
564564+ done;
565565+ (* Derive key and decrypt *)
566566+ let key_material = derive_key t in
567567+ let aes_key = String.sub key_material 0 32 in
568568+ let hmac_key = String.sub key_material 32 32 in
569569+ let iv = String.sub key_material 64 16 in
570570+ (* Verify HMAC if present (last 8 bytes of ciphertext) *)
571571+ let ct_len = String.length ciphertext in
572572+ if ct_len < 24 then
573573+ Error "Ciphertext too short"
574574+ else begin
575575+ let ct_data = String.sub ciphertext 0 (ct_len - 8) in
576576+ let mac = String.sub ciphertext (ct_len - 8) 8 in
577577+ let expected_mac =
578578+ Digestif.SHA256.hmac_string ~key:hmac_key ct_data
579579+ |> Digestif.SHA256.to_raw_string
580580+ |> fun s -> String.sub s 0 8
581581+ in
582582+ if mac <> expected_mac then
583583+ Error "MAC verification failed"
584584+ else begin
585585+ (* Decrypt using mirage-crypto AES.CBC *)
586586+ let cipher = Mirage_crypto.AES.CBC.of_secret aes_key in
587587+ let decrypted = Mirage_crypto.AES.CBC.decrypt ~key:cipher ~iv ct_data in
588588+ (* Remove PKCS7 padding *)
589589+ let pad_len = Char.code decrypted.[String.length decrypted - 1] in
590590+ if pad_len > 16 then
591591+ Error "Invalid padding"
592592+ else begin
593593+ t.received_indices <- message_index :: t.received_indices;
594594+ advance_ratchet t;
595595+ Ok (String.sub decrypted 0 (String.length decrypted - pad_len))
596596+ end
597597+ end
598598+ end
599599+ end
600600+ end
601601+602602+ (** Outbound session for encrypting messages to send to a room *)
603603+ module Outbound = struct
604604+ type t = {
605605+ session_id : string;
606606+ room_id : string;
607607+ (* Ratchet state *)
608608+ mutable ratchet : string array;
609609+ mutable message_index : int;
610610+ (* Ed25519 signing key *)
611611+ signing_priv : Ed25519.priv;
612612+ signing_pub : Ed25519.pub;
613613+ (* Creation and rotation tracking *)
614614+ creation_time : Ptime.t;
615615+ mutable message_count : int;
616616+ max_messages : int;
617617+ max_age : Ptime.Span.t;
618618+ (* Users this session has been shared with *)
619619+ mutable shared_with : (string * string) list; (* user_id, device_id pairs *)
620620+ }
621621+622622+ (** Create a new outbound session for a room *)
623623+ let create ~room_id =
624624+ let session_id =
625625+ Mirage_crypto_rng.generate 16
626626+ |> base64_encode
627627+ in
628628+ let random_part () =
629629+ Mirage_crypto_rng.generate 32
630630+ in
631631+ let ratchet = [| random_part (); random_part (); random_part (); random_part () |] in
632632+ let signing_priv, signing_pub = Ed25519.generate () in
633633+ let now = match Ptime.of_float_s (Unix.gettimeofday ()) with
634634+ | Some t -> t
635635+ | None -> Ptime.epoch
636636+ in
637637+ {
638638+ session_id;
639639+ room_id;
640640+ ratchet;
641641+ message_index = 0;
642642+ signing_priv;
643643+ signing_pub;
644644+ creation_time = now;
645645+ message_count = 0;
646646+ max_messages = 100;
647647+ max_age = Ptime.Span.of_int_s (7 * 24 * 60 * 60); (* 1 week *)
648648+ shared_with = [];
649649+ }
650650+651651+ (** Advance the ratchet *)
652652+ let advance_ratchet t =
653653+ let hash s = Digestif.SHA256.(digest_string s |> to_raw_string) in
654654+ let i = t.message_index land 3 in
655655+ for j = i to 3 do
656656+ t.ratchet.(j) <- hash (t.ratchet.(j) ^ string_of_int j)
657657+ done;
658658+ t.message_index <- t.message_index + 1
659659+660660+ let session_id t = t.session_id
661661+ let room_id t = t.room_id
662662+ let message_index t = t.message_index
663663+664664+ (** Check if session should be rotated *)
665665+ let needs_rotation t =
666666+ t.message_count >= t.max_messages ||
667667+ match Ptime.of_float_s (Unix.gettimeofday ()) with
668668+ | Some now ->
669669+ (match Ptime.diff now t.creation_time |> Ptime.Span.compare t.max_age with
670670+ | n when n > 0 -> true
671671+ | _ -> false)
672672+ | None -> false
673673+674674+ (** Derive encryption key *)
675675+ let derive_key t =
676676+ let combined = String.concat "" (Array.to_list t.ratchet) in
677677+ Hkdf.expand ~hash:`SHA256 ~prk:combined ~info:"MEGOLM_KEYS" 80
678678+679679+ (** Export the session key for sharing via m.room_key *)
680680+ let export_session_key t =
681681+ let ratchet_data = String.concat "" (Array.to_list t.ratchet) in
682682+ base64_encode ratchet_data
683683+684684+ (** Get the signing key *)
685685+ let signing_key t =
686686+ Ed25519.pub_to_octets t.signing_pub |> base64_encode
687687+688688+ (** Encrypt a message *)
689689+ let encrypt t plaintext =
690690+ let key_material = derive_key t in
691691+ let aes_key = String.sub key_material 0 32 in
692692+ let hmac_key = String.sub key_material 32 32 in
693693+ let iv = String.sub key_material 64 16 in
694694+ (* PKCS7 padding *)
695695+ let block_size = 16 in
696696+ let pad_len = block_size - (String.length plaintext mod block_size) in
697697+ let padded = plaintext ^ String.make pad_len (Char.chr pad_len) in
698698+ (* Encrypt using mirage-crypto AES.CBC *)
699699+ let cipher = Mirage_crypto.AES.CBC.of_secret aes_key in
700700+ let ct_data = Mirage_crypto.AES.CBC.encrypt ~key:cipher ~iv padded in
701701+ (* Add HMAC (first 8 bytes) *)
702702+ let mac =
703703+ Digestif.SHA256.hmac_string ~key:hmac_key ct_data
704704+ |> Digestif.SHA256.to_raw_string
705705+ |> fun s -> String.sub s 0 8
706706+ in
707707+ let ciphertext = ct_data ^ mac in
708708+ let msg_index = t.message_index in
709709+ (* Advance ratchet for next message *)
710710+ advance_ratchet t;
711711+ t.message_count <- t.message_count + 1;
712712+ (* Return message index and ciphertext *)
713713+ (msg_index, base64_encode ciphertext)
714714+715715+ (** Mark session as shared with a user/device *)
716716+ let mark_shared_with t ~user_id ~device_id =
717717+ if not (List.mem (user_id, device_id) t.shared_with) then
718718+ t.shared_with <- (user_id, device_id) :: t.shared_with
719719+720720+ (** Check if already shared with a user/device *)
721721+ let is_shared_with t ~user_id ~device_id =
722722+ List.mem (user_id, device_id) t.shared_with
723723+724724+ (** Get list of users this session is shared with *)
725725+ let shared_with t = t.shared_with
726726+ end
727727+end
728728+729729+(** Olm Machine - high-level state machine for E2EE operations *)
730730+module Machine = struct
731731+ type t = {
732732+ user_id : string;
733733+ device_id : string;
734734+ account : Account.t;
735735+ (* Active Olm sessions indexed by their curve25519 key *)
736736+ mutable sessions : (string * Session.t list) list;
737737+ (* Outbound Megolm sessions by room_id *)
738738+ mutable outbound_group_sessions : (string * Megolm.Outbound.t) list;
739739+ (* Inbound Megolm sessions by (room_id, session_id) *)
740740+ mutable inbound_group_sessions : ((string * string) * Megolm.Inbound.t) list;
741741+ (* Device keys we know about: user_id -> device_id -> device_keys *)
742742+ mutable device_keys : (string * (string * Keys.queried_device_keys) list) list;
743743+ }
744744+745745+ (** Create a new OlmMachine *)
746746+ let create ~user_id ~device_id =
747747+ let account = Account.create () in
748748+ {
749749+ user_id;
750750+ device_id;
751751+ account;
752752+ sessions = [];
753753+ outbound_group_sessions = [];
754754+ inbound_group_sessions = [];
755755+ device_keys = [];
756756+ }
757757+758758+ (** Get identity keys *)
759759+ let identity_keys t = Account.identity_keys t.account
760760+761761+ (** Get device keys for upload *)
762762+ let device_keys_for_upload t =
763763+ let ed25519, curve25519 = identity_keys t in
764764+ let algorithms = [
765765+ "m.olm.v1.curve25519-aes-sha2-256";
766766+ "m.megolm.v1.aes-sha2-256";
767767+ ] in
768768+ let keys = [
769769+ (Printf.sprintf "ed25519:%s" t.device_id, ed25519);
770770+ (Printf.sprintf "curve25519:%s" t.device_id, curve25519);
771771+ ] in
772772+ (t.user_id, t.device_id, algorithms, keys)
773773+774774+ (** Generate one-time keys if needed *)
775775+ let generate_one_time_keys t count =
776776+ Account.generate_one_time_keys t.account count
777777+778778+ (** Get one-time keys for upload *)
779779+ let one_time_keys_for_upload t =
780780+ Account.signed_one_time_keys t.account
781781+782782+ (** Mark keys as uploaded *)
783783+ let mark_keys_as_published t =
784784+ Account.mark_keys_as_published t.account
785785+786786+ (** Store device keys from key query response *)
787787+ let receive_device_keys t ~user_id ~devices =
788788+ t.device_keys <- (user_id, devices) ::
789789+ (List.filter (fun (uid, _) -> uid <> user_id) t.device_keys)
790790+791791+ (** Get or create outbound Megolm session for a room *)
792792+ let get_outbound_group_session t ~room_id =
793793+ match List.assoc_opt room_id t.outbound_group_sessions with
794794+ | Some session when not (Megolm.Outbound.needs_rotation session) ->
795795+ session
796796+ | _ ->
797797+ (* Create new session *)
798798+ let session = Megolm.Outbound.create ~room_id in
799799+ t.outbound_group_sessions <-
800800+ (room_id, session) ::
801801+ (List.filter (fun (rid, _) -> rid <> room_id) t.outbound_group_sessions);
802802+ session
803803+804804+ (** Store inbound Megolm session from room key event *)
805805+ let receive_room_key t ~sender_key ~room_id ~session_id ~session_key ~signing_key =
806806+ let session = Megolm.Inbound.from_room_key
807807+ ~sender_key ~room_id ~session_id ~session_key ~signing_key
808808+ in
809809+ t.inbound_group_sessions <-
810810+ ((room_id, session_id), session) :: t.inbound_group_sessions
811811+812812+ (** Encrypt a room message using Megolm *)
813813+ let encrypt_room_message t ~room_id ~content =
814814+ let session = get_outbound_group_session t ~room_id in
815815+ let msg_index, ciphertext = Megolm.Outbound.encrypt session content in
816816+ let _, curve25519_key = identity_keys t in
817817+ (* Build m.room.encrypted content *)
818818+ let encrypted_content = Printf.sprintf
819819+ {|{"algorithm":"m.megolm.v1.aes-sha2-256","sender_key":"%s","ciphertext":"%s","session_id":"%s","device_id":"%s"}|}
820820+ curve25519_key
821821+ ciphertext
822822+ (Megolm.Outbound.session_id session)
823823+ t.device_id
824824+ in
825825+ let _ = msg_index in
826826+ encrypted_content
827827+828828+ (** Decrypt a room message *)
829829+ let decrypt_room_message t ~room_id ~sender_key ~session_id ~ciphertext ~message_index =
830830+ match List.assoc_opt (room_id, session_id) t.inbound_group_sessions with
831831+ | Some session when Megolm.Inbound.sender_key session = sender_key ->
832832+ Megolm.Inbound.decrypt session ~ciphertext ~message_index
833833+ | Some _ ->
834834+ Error "Sender key mismatch"
835835+ | None ->
836836+ Error "Unknown session"
837837+838838+ (** Get or create Olm session for a device *)
839839+ let get_olm_session t ~their_identity_key =
840840+ match List.assoc_opt their_identity_key t.sessions with
841841+ | Some (session :: _) -> Some session
842842+ | _ -> None
843843+844844+ (** Create outbound Olm session *)
845845+ let create_olm_session t ~their_identity_key ~their_one_time_key =
846846+ let session = Session.create_outbound t.account
847847+ ~their_identity_key ~their_one_time_key
848848+ in
849849+ let existing = match List.assoc_opt their_identity_key t.sessions with
850850+ | Some sessions -> sessions
851851+ | None -> []
852852+ in
853853+ t.sessions <-
854854+ (their_identity_key, session :: existing) ::
855855+ (List.filter (fun (k, _) -> k <> their_identity_key) t.sessions);
856856+ session
857857+858858+ (** Process inbound Olm message to create session *)
859859+ let create_inbound_session t ~their_identity_key ~their_ephemeral_key ~one_time_key_id =
860860+ let session = Session.create_inbound t.account
861861+ ~their_identity_key ~their_ephemeral_key ~one_time_key_id
862862+ in
863863+ let existing = match List.assoc_opt their_identity_key t.sessions with
864864+ | Some sessions -> sessions
865865+ | None -> []
866866+ in
867867+ t.sessions <-
868868+ (their_identity_key, session :: existing) ::
869869+ (List.filter (fun (k, _) -> k <> their_identity_key) t.sessions);
870870+ session
871871+872872+ (** Encrypt a to-device message *)
873873+ let encrypt_to_device t ~their_identity_key ~their_one_time_key ~plaintext =
874874+ let session = match get_olm_session t ~their_identity_key with
875875+ | Some s -> s
876876+ | None -> create_olm_session t ~their_identity_key ~their_one_time_key
877877+ in
878878+ Session.encrypt session plaintext
879879+880880+ (** Decrypt a to-device message *)
881881+ let decrypt_to_device t ~their_identity_key ~message_type ~ciphertext =
882882+ match get_olm_session t ~their_identity_key with
883883+ | Some session ->
884884+ Session.decrypt session ~message_type ~ciphertext
885885+ | None ->
886886+ Error "No session for sender"
887887+888888+ (** Number of one-time keys remaining *)
889889+ let one_time_keys_count t =
890890+ Account.one_time_keys_count t.account
891891+892892+ (** Should upload more one-time keys? *)
893893+ let should_upload_keys t =
894894+ one_time_keys_count t < Account.max_one_time_keys t.account / 2
895895+end
+72
lib/matrix_client/presence.ml
···11+(** Presence status operations. *)
22+33+type presence_state =
44+ | Online
55+ | Offline
66+ | Unavailable
77+88+let presence_state_to_string = function
99+ | Online -> "online"
1010+ | Offline -> "offline"
1111+ | Unavailable -> "unavailable"
1212+1313+let presence_state_of_string = function
1414+ | "online" -> Ok Online
1515+ | "offline" -> Ok Offline
1616+ | "unavailable" -> Ok Unavailable
1717+ | s -> Error ("Unknown presence state: " ^ s)
1818+1919+let presence_state_jsont =
2020+ Jsont.of_of_string ~kind:"presence_state"
2121+ ~enc:presence_state_to_string
2222+ presence_state_of_string
2323+2424+type presence = {
2525+ presence : presence_state;
2626+ status_msg : string option;
2727+ last_active_ago : int option;
2828+ currently_active : bool option;
2929+}
3030+3131+let presence_jsont =
3232+ Jsont.Object.(
3333+ map (fun presence status_msg last_active_ago currently_active ->
3434+ { presence; status_msg; last_active_ago; currently_active })
3535+ |> mem "presence" presence_state_jsont
3636+ |> opt_mem "status_msg" Jsont.string ~enc:(fun t -> t.status_msg)
3737+ |> opt_mem "last_active_ago" Jsont.int ~enc:(fun t -> t.last_active_ago)
3838+ |> opt_mem "currently_active" Jsont.bool ~enc:(fun t -> t.currently_active)
3939+ |> finish)
4040+4141+let get_presence client ~user_id =
4242+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
4343+ let path = Printf.sprintf "/presence/%s/status" (Uri.pct_encode user_id_str) in
4444+ match Client.get client ~path () with
4545+ | Error e -> Error e
4646+ | Ok body -> Client.decode_response presence_jsont body
4747+4848+type set_presence_request = {
4949+ presence : presence_state;
5050+ status_msg : string option;
5151+} [@@warning "-69"]
5252+5353+let set_presence_request_jsont =
5454+ Jsont.Object.(
5555+ map (fun presence status_msg -> { presence; status_msg })
5656+ |> mem "presence" presence_state_jsont
5757+ |> opt_mem "status_msg" Jsont.string ~enc:(fun t -> t.status_msg)
5858+ |> finish)
5959+6060+let set_presence client ~presence ?status_msg () =
6161+ match Client.user_id client with
6262+ | None -> Error (Error.Network_error "Not logged in")
6363+ | Some user_id ->
6464+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
6565+ let path = Printf.sprintf "/presence/%s/status" (Uri.pct_encode user_id_str) in
6666+ let request = { presence; status_msg } in
6767+ match Client.encode_body set_presence_request_jsont request with
6868+ | Error e -> Error e
6969+ | Ok body ->
7070+ match Client.put client ~path ~body () with
7171+ | Error e -> Error e
7272+ | Ok _ -> Ok ()
+31
lib/matrix_client/presence.mli
···11+(** Presence status operations. *)
22+33+(** Presence state. *)
44+type presence_state =
55+ | Online
66+ | Offline
77+ | Unavailable
88+99+(** Presence information. *)
1010+type presence = {
1111+ presence : presence_state;
1212+ status_msg : string option;
1313+ last_active_ago : int option;
1414+ currently_active : bool option;
1515+}
1616+1717+(** Get a user's presence status. *)
1818+val get_presence :
1919+ Client.t ->
2020+ user_id:Matrix_proto.Id.User_id.t ->
2121+ (presence, Error.t) result
2222+2323+(** Set the current user's presence status.
2424+2525+ @param status_msg Optional status message. *)
2626+val set_presence :
2727+ Client.t ->
2828+ presence:presence_state ->
2929+ ?status_msg:string ->
3030+ unit ->
3131+ (unit, Error.t) result
+108
lib/matrix_client/profile.ml
···11+(** User profile operations. *)
22+33+type profile = {
44+ displayname : string option;
55+ avatar_url : string option;
66+}
77+88+let profile_jsont =
99+ Jsont.Object.(
1010+ map (fun displayname avatar_url -> { displayname; avatar_url })
1111+ |> opt_mem "displayname" Jsont.string ~enc:(fun t -> t.displayname)
1212+ |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url)
1313+ |> finish)
1414+1515+let get_profile client ~user_id =
1616+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
1717+ let path = Printf.sprintf "/profile/%s" (Uri.pct_encode user_id_str) in
1818+ match Client.get client ~path () with
1919+ | Error e -> Error e
2020+ | Ok body -> Client.decode_response profile_jsont body
2121+2222+type displayname_response = {
2323+ displayname : string option;
2424+}
2525+2626+let displayname_response_jsont =
2727+ Jsont.Object.(
2828+ map (fun displayname -> { displayname })
2929+ |> opt_mem "displayname" Jsont.string
3030+ |> finish)
3131+3232+let get_displayname client ~user_id =
3333+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
3434+ let path = Printf.sprintf "/profile/%s/displayname" (Uri.pct_encode user_id_str) in
3535+ match Client.get client ~path () with
3636+ | Error e -> Error e
3737+ | Ok body ->
3838+ match Client.decode_response displayname_response_jsont body with
3939+ | Error e -> Error e
4040+ | Ok resp -> Ok resp.displayname
4141+4242+type avatar_url_response = {
4343+ avatar_url : string option;
4444+}
4545+4646+let avatar_url_response_jsont =
4747+ Jsont.Object.(
4848+ map (fun avatar_url -> { avatar_url })
4949+ |> opt_mem "avatar_url" Jsont.string
5050+ |> finish)
5151+5252+let get_avatar_url client ~user_id =
5353+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
5454+ let path = Printf.sprintf "/profile/%s/avatar_url" (Uri.pct_encode user_id_str) in
5555+ match Client.get client ~path () with
5656+ | Error e -> Error e
5757+ | Ok body ->
5858+ match Client.decode_response avatar_url_response_jsont body with
5959+ | Error e -> Error e
6060+ | Ok resp -> Ok resp.avatar_url
6161+6262+type set_displayname_request = {
6363+ displayname : string;
6464+} [@@warning "-69"]
6565+6666+let set_displayname_request_jsont =
6767+ Jsont.Object.(
6868+ map (fun displayname -> { displayname })
6969+ |> mem "displayname" Jsont.string
7070+ |> finish)
7171+7272+let set_displayname client ~displayname =
7373+ match Client.user_id client with
7474+ | None -> Error (Error.Network_error "Not logged in")
7575+ | Some user_id ->
7676+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
7777+ let path = Printf.sprintf "/profile/%s/displayname" (Uri.pct_encode user_id_str) in
7878+ let request = { displayname } in
7979+ match Client.encode_body set_displayname_request_jsont request with
8080+ | Error e -> Error e
8181+ | Ok body ->
8282+ match Client.put client ~path ~body () with
8383+ | Error e -> Error e
8484+ | Ok _ -> Ok ()
8585+8686+type set_avatar_url_request = {
8787+ avatar_url : string;
8888+} [@@warning "-69"]
8989+9090+let set_avatar_url_request_jsont =
9191+ Jsont.Object.(
9292+ map (fun avatar_url -> { avatar_url })
9393+ |> mem "avatar_url" Jsont.string
9494+ |> finish)
9595+9696+let set_avatar_url client ~avatar_url =
9797+ match Client.user_id client with
9898+ | None -> Error (Error.Network_error "Not logged in")
9999+ | Some user_id ->
100100+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
101101+ let path = Printf.sprintf "/profile/%s/avatar_url" (Uri.pct_encode user_id_str) in
102102+ let request = { avatar_url } in
103103+ match Client.encode_body set_avatar_url_request_jsont request with
104104+ | Error e -> Error e
105105+ | Ok body ->
106106+ match Client.put client ~path ~body () with
107107+ | Error e -> Error e
108108+ | Ok _ -> Ok ()
+37
lib/matrix_client/profile.mli
···11+(** User profile operations. *)
22+33+(** User profile. *)
44+type profile = {
55+ displayname : string option;
66+ avatar_url : string option;
77+}
88+99+(** Get a user's full profile. *)
1010+val get_profile :
1111+ Client.t ->
1212+ user_id:Matrix_proto.Id.User_id.t ->
1313+ (profile, Error.t) result
1414+1515+(** Get a user's display name. *)
1616+val get_displayname :
1717+ Client.t ->
1818+ user_id:Matrix_proto.Id.User_id.t ->
1919+ (string option, Error.t) result
2020+2121+(** Get a user's avatar URL. *)
2222+val get_avatar_url :
2323+ Client.t ->
2424+ user_id:Matrix_proto.Id.User_id.t ->
2525+ (string option, Error.t) result
2626+2727+(** Set the current user's display name. *)
2828+val set_displayname :
2929+ Client.t ->
3030+ displayname:string ->
3131+ (unit, Error.t) result
3232+3333+(** Set the current user's avatar URL. *)
3434+val set_avatar_url :
3535+ Client.t ->
3636+ avatar_url:string ->
3737+ (unit, Error.t) result
+368
lib/matrix_client/push.ml
···11+(** Push notification operations. *)
22+33+(** Push rule kinds *)
44+type rule_kind =
55+ | Override
66+ | Underride
77+ | Sender
88+ | Room
99+ | Content
1010+1111+let rule_kind_to_string = function
1212+ | Override -> "override"
1313+ | Underride -> "underride"
1414+ | Sender -> "sender"
1515+ | Room -> "room"
1616+ | Content -> "content"
1717+1818+let rule_kind_of_string = function
1919+ | "override" -> Ok Override
2020+ | "underride" -> Ok Underride
2121+ | "sender" -> Ok Sender
2222+ | "room" -> Ok Room
2323+ | "content" -> Ok Content
2424+ | s -> Error ("Unknown rule kind: " ^ s)
2525+2626+let rule_kind_jsont =
2727+ Jsont.of_of_string ~kind:"rule_kind"
2828+ ~enc:rule_kind_to_string
2929+ rule_kind_of_string
3030+3131+(** Push rule action *)
3232+type action =
3333+ | Notify
3434+ | Dont_notify
3535+ | Coalesce
3636+ | Set_tweak of string * Jsont.json
3737+3838+(* For actions, we use a string codec as a simplification *)
3939+let action_jsont : action Jsont.t =
4040+ Jsont.string
4141+ |> Jsont.map
4242+ ~dec:(function
4343+ | "notify" -> Notify
4444+ | "dont_notify" -> Dont_notify
4545+ | "coalesce" -> Coalesce
4646+ | _ -> Dont_notify)
4747+ ~enc:(function
4848+ | Notify -> "notify"
4949+ | Dont_notify -> "dont_notify"
5050+ | Coalesce -> "coalesce"
5151+ | Set_tweak _ -> "notify")
5252+5353+(** Push rule condition *)
5454+type condition = {
5555+ kind : string;
5656+ key : string option;
5757+ pattern : string option;
5858+ is_ : string option;
5959+}
6060+6161+let condition_jsont =
6262+ Jsont.Object.(
6363+ map (fun kind key pattern is_ -> { kind; key; pattern; is_ })
6464+ |> mem "kind" Jsont.string ~enc:(fun t -> t.kind)
6565+ |> opt_mem "key" Jsont.string ~enc:(fun t -> t.key)
6666+ |> opt_mem "pattern" Jsont.string ~enc:(fun t -> t.pattern)
6767+ |> opt_mem "is" Jsont.string ~enc:(fun t -> t.is_)
6868+ |> finish)
6969+7070+(** Push rule *)
7171+type rule = {
7272+ rule_id : string;
7373+ default : bool;
7474+ enabled : bool;
7575+ actions : action list;
7676+ conditions : condition list option;
7777+ pattern : string option;
7878+}
7979+8080+let rule_jsont =
8181+ Jsont.Object.(
8282+ map (fun rule_id default enabled actions conditions pattern ->
8383+ { rule_id; default; enabled; actions; conditions; pattern })
8484+ |> mem "rule_id" Jsont.string ~enc:(fun t -> t.rule_id)
8585+ |> mem "default" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.default)
8686+ |> mem "enabled" Jsont.bool ~dec_absent:true ~enc:(fun t -> t.enabled)
8787+ |> mem "actions" (Jsont.list action_jsont) ~dec_absent:[] ~enc:(fun t -> t.actions)
8888+ |> opt_mem "conditions" (Jsont.list condition_jsont) ~enc:(fun t -> t.conditions)
8989+ |> opt_mem "pattern" Jsont.string ~enc:(fun t -> t.pattern)
9090+ |> finish)
9191+9292+(** Push ruleset *)
9393+type ruleset = {
9494+ override : rule list;
9595+ underride : rule list;
9696+ sender : rule list;
9797+ room : rule list;
9898+ content : rule list;
9999+}
100100+101101+let ruleset_jsont =
102102+ Jsont.Object.(
103103+ map (fun override underride sender room content ->
104104+ { override; underride; sender; room; content })
105105+ |> mem "override" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.override)
106106+ |> mem "underride" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.underride)
107107+ |> mem "sender" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.sender)
108108+ |> mem "room" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.room)
109109+ |> mem "content" (Jsont.list rule_jsont) ~dec_absent:[] ~enc:(fun t -> t.content)
110110+ |> finish)
111111+112112+type push_rules_response = {
113113+ global : ruleset;
114114+}
115115+116116+let push_rules_response_jsont =
117117+ Jsont.Object.(
118118+ map (fun global -> { global })
119119+ |> mem "global" ruleset_jsont ~enc:(fun t -> t.global)
120120+ |> finish)
121121+122122+(** Get all push rules. *)
123123+let get_push_rules client =
124124+ match Client.get client ~path:"/pushrules/" () with
125125+ | Error e -> Error e
126126+ | Ok body -> Client.decode_response push_rules_response_jsont body
127127+128128+(** Get a specific push rule. *)
129129+let get_push_rule client ~scope ~kind ~rule_id =
130130+ let kind_str = rule_kind_to_string kind in
131131+ let path = Printf.sprintf "/pushrules/%s/%s/%s"
132132+ (Uri.pct_encode scope)
133133+ (Uri.pct_encode kind_str)
134134+ (Uri.pct_encode rule_id)
135135+ in
136136+ match Client.get client ~path () with
137137+ | Error e -> Error e
138138+ | Ok body -> Client.decode_response rule_jsont body
139139+140140+(** Delete a push rule. *)
141141+let delete_push_rule client ~scope ~kind ~rule_id =
142142+ let kind_str = rule_kind_to_string kind in
143143+ let path = Printf.sprintf "/pushrules/%s/%s/%s"
144144+ (Uri.pct_encode scope)
145145+ (Uri.pct_encode kind_str)
146146+ (Uri.pct_encode rule_id)
147147+ in
148148+ match Client.delete client ~path () with
149149+ | Error e -> Error e
150150+ | Ok _ -> Ok ()
151151+152152+(** Add or update a push rule. *)
153153+type add_rule_request = {
154154+ actions : action list;
155155+ conditions : condition list option;
156156+ pattern : string option;
157157+} [@@warning "-69"]
158158+159159+let add_rule_request_jsont =
160160+ Jsont.Object.(
161161+ map (fun actions conditions pattern ->
162162+ { actions; conditions; pattern })
163163+ |> mem "actions" (Jsont.list action_jsont) ~enc:(fun t -> t.actions)
164164+ |> opt_mem "conditions" (Jsont.list condition_jsont) ~enc:(fun t -> t.conditions)
165165+ |> opt_mem "pattern" Jsont.string ~enc:(fun t -> t.pattern)
166166+ |> finish)
167167+168168+let set_push_rule client ~scope ~kind ~rule_id
169169+ ~actions ?conditions ?pattern ?before ?after () =
170170+ let kind_str = rule_kind_to_string kind in
171171+ let path = Printf.sprintf "/pushrules/%s/%s/%s"
172172+ (Uri.pct_encode scope)
173173+ (Uri.pct_encode kind_str)
174174+ (Uri.pct_encode rule_id)
175175+ in
176176+ let query =
177177+ []
178178+ |> (fun q -> match before with Some b -> ("before", b) :: q | None -> q)
179179+ |> (fun q -> match after with Some a -> ("after", a) :: q | None -> q)
180180+ in
181181+ let query = if query = [] then None else Some query in
182182+ let request = { actions; conditions; pattern } in
183183+ match Client.encode_body add_rule_request_jsont request with
184184+ | Error e -> Error e
185185+ | Ok body ->
186186+ match Client.put client ~path ~body ?query () with
187187+ | Error e -> Error e
188188+ | Ok _ -> Ok ()
189189+190190+(** Enable or disable a push rule. *)
191191+type enabled_request = {
192192+ enabled : bool;
193193+} [@@warning "-69"]
194194+195195+let enabled_request_jsont =
196196+ Jsont.Object.(
197197+ map (fun enabled -> { enabled })
198198+ |> mem "enabled" Jsont.bool ~enc:(fun t -> t.enabled)
199199+ |> finish)
200200+201201+let set_push_rule_enabled client ~scope ~kind ~rule_id ~enabled =
202202+ let kind_str = rule_kind_to_string kind in
203203+ let path = Printf.sprintf "/pushrules/%s/%s/%s/enabled"
204204+ (Uri.pct_encode scope)
205205+ (Uri.pct_encode kind_str)
206206+ (Uri.pct_encode rule_id)
207207+ in
208208+ let request = { enabled } in
209209+ match Client.encode_body enabled_request_jsont request with
210210+ | Error e -> Error e
211211+ | Ok body ->
212212+ match Client.put client ~path ~body () with
213213+ | Error e -> Error e
214214+ | Ok _ -> Ok ()
215215+216216+(** Set the actions for a push rule. *)
217217+type actions_request = {
218218+ actions : action list;
219219+} [@@warning "-69"]
220220+221221+let actions_request_jsont =
222222+ Jsont.Object.(
223223+ map (fun actions -> { actions })
224224+ |> mem "actions" (Jsont.list action_jsont) ~enc:(fun t -> t.actions)
225225+ |> finish)
226226+227227+let set_push_rule_actions client ~scope ~kind ~rule_id ~actions =
228228+ let kind_str = rule_kind_to_string kind in
229229+ let path = Printf.sprintf "/pushrules/%s/%s/%s/actions"
230230+ (Uri.pct_encode scope)
231231+ (Uri.pct_encode kind_str)
232232+ (Uri.pct_encode rule_id)
233233+ in
234234+ let request = { actions } in
235235+ match Client.encode_body actions_request_jsont request with
236236+ | Error e -> Error e
237237+ | Ok body ->
238238+ match Client.put client ~path ~body () with
239239+ | Error e -> Error e
240240+ | Ok _ -> Ok ()
241241+242242+(** Pusher types *)
243243+type pusher_kind =
244244+ | Http
245245+ | Email
246246+247247+let pusher_kind_to_string = function
248248+ | Http -> "http"
249249+ | Email -> "email"
250250+251251+let pusher_kind_of_string = function
252252+ | "http" -> Ok Http
253253+ | "email" -> Ok Email
254254+ | s -> Error ("Unknown pusher kind: " ^ s)
255255+256256+let pusher_kind_jsont =
257257+ Jsont.of_of_string ~kind:"pusher_kind"
258258+ ~enc:pusher_kind_to_string
259259+ pusher_kind_of_string
260260+261261+type pusher_data = {
262262+ url : string option;
263263+ format : string option;
264264+}
265265+266266+let pusher_data_jsont =
267267+ Jsont.Object.(
268268+ map (fun url format -> { url; format })
269269+ |> opt_mem "url" Jsont.string ~enc:(fun t -> t.url)
270270+ |> opt_mem "format" Jsont.string ~enc:(fun t -> t.format)
271271+ |> finish)
272272+273273+type pusher = {
274274+ pushkey : string;
275275+ kind : pusher_kind;
276276+ app_id : string;
277277+ app_display_name : string;
278278+ device_display_name : string;
279279+ profile_tag : string option;
280280+ lang : string;
281281+ data : pusher_data;
282282+}
283283+284284+let pusher_jsont =
285285+ Jsont.Object.(
286286+ map (fun pushkey kind app_id app_display_name device_display_name
287287+ profile_tag lang data ->
288288+ { pushkey; kind; app_id; app_display_name; device_display_name;
289289+ profile_tag; lang; data })
290290+ |> mem "pushkey" Jsont.string ~enc:(fun t -> t.pushkey)
291291+ |> mem "kind" pusher_kind_jsont ~enc:(fun t -> t.kind)
292292+ |> mem "app_id" Jsont.string ~enc:(fun t -> t.app_id)
293293+ |> mem "app_display_name" Jsont.string ~enc:(fun t -> t.app_display_name)
294294+ |> mem "device_display_name" Jsont.string ~enc:(fun t -> t.device_display_name)
295295+ |> opt_mem "profile_tag" Jsont.string ~enc:(fun t -> t.profile_tag)
296296+ |> mem "lang" Jsont.string ~enc:(fun t -> t.lang)
297297+ |> mem "data" pusher_data_jsont ~enc:(fun t -> t.data)
298298+ |> finish)
299299+300300+type pushers_response = {
301301+ pushers : pusher list;
302302+}
303303+304304+let pushers_response_jsont =
305305+ Jsont.Object.(
306306+ map (fun pushers -> { pushers })
307307+ |> mem "pushers" (Jsont.list pusher_jsont) ~dec_absent:[] ~enc:(fun t -> t.pushers)
308308+ |> finish)
309309+310310+(** Get all pushers for the current user. *)
311311+let get_pushers client =
312312+ match Client.get client ~path:"/pushers" () with
313313+ | Error e -> Error e
314314+ | Ok body ->
315315+ match Client.decode_response pushers_response_jsont body with
316316+ | Error e -> Error e
317317+ | Ok resp -> Ok resp.pushers
318318+319319+(** Set a pusher. *)
320320+type set_pusher_request = {
321321+ pushkey : string;
322322+ kind : pusher_kind;
323323+ app_id : string;
324324+ app_display_name : string;
325325+ device_display_name : string;
326326+ profile_tag : string option;
327327+ lang : string;
328328+ data : pusher_data;
329329+ append : bool option;
330330+} [@@warning "-69"]
331331+332332+let set_pusher_request_jsont =
333333+ Jsont.Object.(
334334+ map (fun pushkey kind app_id app_display_name device_display_name
335335+ profile_tag lang data append ->
336336+ { pushkey; kind; app_id; app_display_name; device_display_name;
337337+ profile_tag; lang; data; append })
338338+ |> mem "pushkey" Jsont.string
339339+ |> mem "kind" pusher_kind_jsont
340340+ |> mem "app_id" Jsont.string
341341+ |> mem "app_display_name" Jsont.string
342342+ |> mem "device_display_name" Jsont.string
343343+ |> opt_mem "profile_tag" Jsont.string ~enc:(fun t -> t.profile_tag)
344344+ |> mem "lang" Jsont.string
345345+ |> mem "data" pusher_data_jsont
346346+ |> opt_mem "append" Jsont.bool ~enc:(fun t -> t.append)
347347+ |> finish)
348348+349349+let set_pusher client ~pushkey ~kind ~app_id ~app_display_name
350350+ ~device_display_name ?profile_tag ~lang ~data ?append () =
351351+ let request = {
352352+ pushkey; kind; app_id; app_display_name; device_display_name;
353353+ profile_tag; lang; data; append
354354+ } in
355355+ match Client.encode_body set_pusher_request_jsont request with
356356+ | Error e -> Error e
357357+ | Ok body ->
358358+ match Client.post client ~path:"/pushers/set" ~body () with
359359+ | Error e -> Error e
360360+ | Ok _ -> Ok ()
361361+362362+(** Delete a pusher by setting kind to null. *)
363363+let delete_pusher client ~pushkey ~app_id =
364364+ (* Use raw json for the special null kind *)
365365+ let body = Printf.sprintf {|{"pushkey":"%s","kind":null,"app_id":"%s"}|} pushkey app_id in
366366+ match Client.post client ~path:"/pushers/set" ~body () with
367367+ | Error e -> Error e
368368+ | Ok _ -> Ok ()
+39
lib/matrix_client/receipts.ml
···11+(** Read receipts. *)
22+33+let send_receipt client ~room_id ~event_id ?(receipt_type = "m.read") () =
44+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
55+ let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in
66+ let path = Printf.sprintf "/rooms/%s/receipt/%s/%s"
77+ (Uri.pct_encode room_id_str)
88+ (Uri.pct_encode receipt_type)
99+ (Uri.pct_encode event_id_str)
1010+ in
1111+ match Client.post client ~path ~body:"{}" () with
1212+ | Error e -> Error e
1313+ | Ok _ -> Ok ()
1414+1515+type read_marker_request = {
1616+ fully_read : string;
1717+ read : string option;
1818+} [@@warning "-69"]
1919+2020+let read_marker_request_jsont =
2121+ Jsont.Object.(
2222+ map (fun fully_read read -> { fully_read; read })
2323+ |> mem "m.fully_read" Jsont.string
2424+ |> opt_mem "m.read" Jsont.string ~enc:(fun t -> t.read)
2525+ |> finish)
2626+2727+let set_read_marker client ~room_id ~fully_read ?read () =
2828+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
2929+ let path = Printf.sprintf "/rooms/%s/read_markers" (Uri.pct_encode room_id_str) in
3030+ let request = {
3131+ fully_read = Matrix_proto.Id.Event_id.to_string fully_read;
3232+ read = Option.map Matrix_proto.Id.Event_id.to_string read;
3333+ } in
3434+ match Client.encode_body read_marker_request_jsont request with
3535+ | Error e -> Error e
3636+ | Ok body ->
3737+ match Client.post client ~path ~body () with
3838+ | Error e -> Error e
3939+ | Ok _ -> Ok ()
+24
lib/matrix_client/receipts.mli
···11+(** Read receipts. *)
22+33+(** Send a read receipt.
44+55+ @param receipt_type The receipt type (usually "m.read" or "m.read.private"). *)
66+val send_receipt :
77+ Client.t ->
88+ room_id:Matrix_proto.Id.Room_id.t ->
99+ event_id:Matrix_proto.Id.Event_id.t ->
1010+ ?receipt_type:string ->
1111+ unit ->
1212+ (unit, Error.t) result
1313+1414+(** Set read markers.
1515+1616+ @param fully_read The event ID to mark as fully read.
1717+ @param read The event ID to mark as read (optional). *)
1818+val set_read_marker :
1919+ Client.t ->
2020+ room_id:Matrix_proto.Id.Room_id.t ->
2121+ fully_read:Matrix_proto.Id.Event_id.t ->
2222+ ?read:Matrix_proto.Id.Event_id.t ->
2323+ unit ->
2424+ (unit, Error.t) result
+330
lib/matrix_client/relations.ml
···11+(** Event relations: reactions, edits, threads, and replies. *)
22+33+(* Relation types *)
44+type relation_type =
55+ | Annotation (* m.annotation - reactions *)
66+ | Reference (* m.reference - generic reference *)
77+ | Replace (* m.replace - edits *)
88+ | Thread (* m.thread - threads *)
99+1010+let relation_type_to_string = function
1111+ | Annotation -> "m.annotation"
1212+ | Reference -> "m.reference"
1313+ | Replace -> "m.replace"
1414+ | Thread -> "m.thread"
1515+1616+let relation_type_of_string = function
1717+ | "m.annotation" -> Ok Annotation
1818+ | "m.reference" -> Ok Reference
1919+ | "m.replace" -> Ok Replace
2020+ | "m.thread" -> Ok Thread
2121+ | s -> Error ("Unknown relation type: " ^ s)
2222+2323+let relation_type_jsont =
2424+ Jsont.of_of_string ~kind:"relation_type"
2525+ ~enc:relation_type_to_string
2626+ relation_type_of_string
2727+[@@warning "-32"]
2828+2929+(* Reaction *)
3030+type reaction = {
3131+ event_id : Matrix_proto.Id.Event_id.t;
3232+ key : string; (* emoji or shortcode *)
3333+}
3434+3535+(* Send a reaction to an event *)
3636+type reaction_content = {
3737+ relates_to : reaction_relates_to;
3838+} [@@warning "-69"]
3939+4040+and reaction_relates_to = {
4141+ rel_type : string;
4242+ event_id : string;
4343+ key : string;
4444+} [@@warning "-69"]
4545+4646+let reaction_relates_to_jsont =
4747+ Jsont.Object.(
4848+ map (fun rel_type event_id key -> { rel_type; event_id; key })
4949+ |> mem "rel_type" Jsont.string
5050+ |> mem "event_id" Jsont.string
5151+ |> mem "key" Jsont.string
5252+ |> finish)
5353+5454+let reaction_content_jsont =
5555+ Jsont.Object.(
5656+ map (fun relates_to -> { relates_to })
5757+ |> mem "m.relates_to" reaction_relates_to_jsont
5858+ |> finish)
5959+6060+let send_reaction client ~room_id ~event_id ~key =
6161+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
6262+ let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in
6363+ let path = Printf.sprintf "/rooms/%s/send/m.reaction" (Uri.pct_encode room_id_str) in
6464+ let content = {
6565+ relates_to = {
6666+ rel_type = "m.annotation";
6767+ event_id = event_id_str;
6868+ key;
6969+ }
7070+ } in
7171+ match Client.encode_body reaction_content_jsont content with
7272+ | Error e -> Error e
7373+ | Ok body ->
7474+ match Client.post client ~path ~body () with
7575+ | Error e -> Error e
7676+ | Ok body ->
7777+ match Client.decode_response Messages.send_response_jsont body with
7878+ | Error e -> Error e
7979+ | Ok resp -> Ok resp.event_id
8080+8181+(* Edit a message *)
8282+type edit_content = {
8383+ msgtype : string;
8484+ body : string;
8585+ new_content : edit_new_content;
8686+ relates_to : edit_relates_to;
8787+} [@@warning "-69"]
8888+8989+and edit_new_content = {
9090+ msgtype : string;
9191+ body : string;
9292+} [@@warning "-69"]
9393+9494+and edit_relates_to = {
9595+ rel_type : string;
9696+ event_id : string;
9797+} [@@warning "-69"]
9898+9999+let edit_new_content_jsont =
100100+ Jsont.Object.(
101101+ map (fun msgtype body -> { msgtype; body })
102102+ |> mem "msgtype" Jsont.string
103103+ |> mem "body" Jsont.string
104104+ |> finish)
105105+106106+let edit_relates_to_jsont =
107107+ Jsont.Object.(
108108+ map (fun rel_type event_id -> { rel_type; event_id })
109109+ |> mem "rel_type" Jsont.string
110110+ |> mem "event_id" Jsont.string
111111+ |> finish)
112112+113113+let edit_content_jsont =
114114+ Jsont.Object.(
115115+ map (fun msgtype body new_content relates_to ->
116116+ { msgtype; body; new_content; relates_to })
117117+ |> mem "msgtype" Jsont.string
118118+ |> mem "body" Jsont.string
119119+ |> mem "m.new_content" edit_new_content_jsont
120120+ |> mem "m.relates_to" edit_relates_to_jsont
121121+ |> finish)
122122+123123+let edit_message client ~room_id ~event_id ~new_body =
124124+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
125125+ let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in
126126+ let path = Printf.sprintf "/rooms/%s/send/m.room.message" (Uri.pct_encode room_id_str) in
127127+ let content = {
128128+ msgtype = "m.text";
129129+ body = "* " ^ new_body; (* Fallback for clients that don't support edits *)
130130+ new_content = {
131131+ msgtype = "m.text";
132132+ body = new_body;
133133+ };
134134+ relates_to = {
135135+ rel_type = "m.replace";
136136+ event_id = event_id_str;
137137+ };
138138+ } in
139139+ match Client.encode_body edit_content_jsont content with
140140+ | Error e -> Error e
141141+ | Ok body ->
142142+ match Client.post client ~path ~body () with
143143+ | Error e -> Error e
144144+ | Ok body ->
145145+ match Client.decode_response Messages.send_response_jsont body with
146146+ | Error e -> Error e
147147+ | Ok resp -> Ok resp.event_id
148148+149149+(* Reply to a message *)
150150+type reply_relates_to = {
151151+ in_reply_to : reply_in_reply_to;
152152+} [@@warning "-69"]
153153+154154+and reply_in_reply_to = {
155155+ event_id : string;
156156+} [@@warning "-69"]
157157+158158+type reply_content = {
159159+ msgtype : string;
160160+ body : string;
161161+ relates_to : reply_relates_to;
162162+} [@@warning "-69"]
163163+164164+let reply_in_reply_to_jsont =
165165+ Jsont.Object.(
166166+ map (fun event_id -> { event_id })
167167+ |> mem "event_id" Jsont.string
168168+ |> finish)
169169+170170+let reply_relates_to_jsont =
171171+ Jsont.Object.(
172172+ map (fun in_reply_to -> { in_reply_to })
173173+ |> mem "m.in_reply_to" reply_in_reply_to_jsont
174174+ |> finish)
175175+176176+let reply_content_jsont =
177177+ Jsont.Object.(
178178+ map (fun msgtype body relates_to -> { msgtype; body; relates_to })
179179+ |> mem "msgtype" Jsont.string
180180+ |> mem "body" Jsont.string
181181+ |> mem "m.relates_to" reply_relates_to_jsont
182182+ |> finish)
183183+184184+let send_reply client ~room_id ~event_id ~body =
185185+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
186186+ let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in
187187+ let path = Printf.sprintf "/rooms/%s/send/m.room.message" (Uri.pct_encode room_id_str) in
188188+ let content = {
189189+ msgtype = "m.text";
190190+ body;
191191+ relates_to = {
192192+ in_reply_to = {
193193+ event_id = event_id_str;
194194+ };
195195+ };
196196+ } in
197197+ match Client.encode_body reply_content_jsont content with
198198+ | Error e -> Error e
199199+ | Ok body ->
200200+ match Client.post client ~path ~body () with
201201+ | Error e -> Error e
202202+ | Ok resp_body ->
203203+ match Client.decode_response Messages.send_response_jsont resp_body with
204204+ | Error e -> Error e
205205+ | Ok resp -> Ok resp.event_id
206206+207207+(* Thread message *)
208208+type thread_relates_to = {
209209+ rel_type : string;
210210+ event_id : string;
211211+ is_falling_back : bool;
212212+ in_reply_to : reply_in_reply_to option;
213213+} [@@warning "-69"]
214214+215215+type thread_content = {
216216+ msgtype : string;
217217+ body : string;
218218+ relates_to : thread_relates_to;
219219+} [@@warning "-69"]
220220+221221+let thread_relates_to_jsont =
222222+ Jsont.Object.(
223223+ map (fun rel_type event_id is_falling_back in_reply_to ->
224224+ { rel_type; event_id; is_falling_back; in_reply_to })
225225+ |> mem "rel_type" Jsont.string
226226+ |> mem "event_id" Jsont.string
227227+ |> mem "is_falling_back" Jsont.bool ~dec_absent:true
228228+ |> opt_mem "m.in_reply_to" reply_in_reply_to_jsont ~enc:(fun t -> t.in_reply_to)
229229+ |> finish)
230230+231231+let thread_content_jsont =
232232+ Jsont.Object.(
233233+ map (fun msgtype body relates_to -> { msgtype; body; relates_to })
234234+ |> mem "msgtype" Jsont.string
235235+ |> mem "body" Jsont.string
236236+ |> mem "m.relates_to" thread_relates_to_jsont
237237+ |> finish)
238238+239239+let send_in_thread client ~room_id ~thread_root_id ?reply_to_id ~body () =
240240+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
241241+ let thread_root_str = Matrix_proto.Id.Event_id.to_string thread_root_id in
242242+ let path = Printf.sprintf "/rooms/%s/send/m.room.message" (Uri.pct_encode room_id_str) in
243243+ let in_reply_to = match reply_to_id with
244244+ | Some id -> Some { event_id = Matrix_proto.Id.Event_id.to_string id }
245245+ | None -> None
246246+ in
247247+ let content = {
248248+ msgtype = "m.text";
249249+ body;
250250+ relates_to = {
251251+ rel_type = "m.thread";
252252+ event_id = thread_root_str;
253253+ is_falling_back = Option.is_none reply_to_id;
254254+ in_reply_to;
255255+ };
256256+ } in
257257+ match Client.encode_body thread_content_jsont content with
258258+ | Error e -> Error e
259259+ | Ok body ->
260260+ match Client.post client ~path ~body () with
261261+ | Error e -> Error e
262262+ | Ok resp_body ->
263263+ match Client.decode_response Messages.send_response_jsont resp_body with
264264+ | Error e -> Error e
265265+ | Ok resp -> Ok resp.event_id
266266+267267+(* Get relations for an event *)
268268+type aggregation = {
269269+ event_id : Matrix_proto.Id.Event_id.t;
270270+ origin_server_ts : int64;
271271+ sender : Matrix_proto.Id.User_id.t;
272272+}
273273+274274+let aggregation_jsont =
275275+ Jsont.Object.(
276276+ map (fun event_id origin_server_ts sender ->
277277+ { event_id; origin_server_ts; sender })
278278+ |> mem "event_id" Matrix_proto.Id.Event_id.jsont
279279+ |> mem "origin_server_ts" Jsont.int64
280280+ |> mem "sender" Matrix_proto.Id.User_id.jsont
281281+ |> finish)
282282+283283+type relations_response = {
284284+ chunk : aggregation list;
285285+ next_batch : string option;
286286+ prev_batch : string option;
287287+}
288288+289289+let relations_response_jsont =
290290+ Jsont.Object.(
291291+ map (fun chunk next_batch prev_batch ->
292292+ { chunk; next_batch; prev_batch })
293293+ |> mem "chunk" (Jsont.list aggregation_jsont) ~dec_absent:[]
294294+ |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch)
295295+ |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch)
296296+ |> finish)
297297+298298+let get_relations client ~room_id ~event_id ?rel_type ?event_type ?limit ?from () =
299299+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
300300+ let event_id_str = Matrix_proto.Id.Event_id.to_string event_id in
301301+ let path = match rel_type, event_type with
302302+ | Some rt, Some et ->
303303+ Printf.sprintf "/rooms/%s/relations/%s/%s/%s"
304304+ (Uri.pct_encode room_id_str)
305305+ (Uri.pct_encode event_id_str)
306306+ (Uri.pct_encode (relation_type_to_string rt))
307307+ (Uri.pct_encode et)
308308+ | Some rt, None ->
309309+ Printf.sprintf "/rooms/%s/relations/%s/%s"
310310+ (Uri.pct_encode room_id_str)
311311+ (Uri.pct_encode event_id_str)
312312+ (Uri.pct_encode (relation_type_to_string rt))
313313+ | None, _ ->
314314+ Printf.sprintf "/rooms/%s/relations/%s"
315315+ (Uri.pct_encode room_id_str)
316316+ (Uri.pct_encode event_id_str)
317317+ in
318318+ let query =
319319+ []
320320+ |> (fun q -> match limit with Some l -> ("limit", string_of_int l) :: q | None -> q)
321321+ |> (fun q -> match from with Some f -> ("from", f) :: q | None -> q)
322322+ in
323323+ let query = if query = [] then None else Some query in
324324+ match Client.get client ~path ?query () with
325325+ | Error e -> Error e
326326+ | Ok body -> Client.decode_response relations_response_jsont body
327327+328328+(* Get all reactions for an event *)
329329+let get_reactions client ~room_id ~event_id =
330330+ get_relations client ~room_id ~event_id ~rel_type:Annotation ~event_type:"m.reaction" ()
+120
lib/matrix_client/relations.mli
···11+(** Event relations: reactions, edits, threads, and replies. *)
22+33+(** {1 Relation Types} *)
44+55+(** Types of event relations. *)
66+type relation_type =
77+ | Annotation (** m.annotation - used for reactions *)
88+ | Reference (** m.reference - generic reference *)
99+ | Replace (** m.replace - used for edits *)
1010+ | Thread (** m.thread - used for threads *)
1111+1212+val relation_type_to_string : relation_type -> string
1313+val relation_type_of_string : string -> (relation_type, string) result
1414+1515+(** {1 Reactions} *)
1616+1717+(** A reaction to an event. *)
1818+type reaction = {
1919+ event_id : Matrix_proto.Id.Event_id.t;
2020+ key : string; (** Emoji or shortcode *)
2121+}
2222+2323+(** Send a reaction to an event.
2424+2525+ @param room_id The room containing the event.
2626+ @param event_id The event to react to.
2727+ @param key The reaction key (typically an emoji). *)
2828+val send_reaction :
2929+ Client.t ->
3030+ room_id:Matrix_proto.Id.Room_id.t ->
3131+ event_id:Matrix_proto.Id.Event_id.t ->
3232+ key:string ->
3333+ (Matrix_proto.Id.Event_id.t, Error.t) result
3434+3535+(** {1 Edits} *)
3636+3737+(** Edit a text message.
3838+3939+ @param room_id The room containing the message.
4040+ @param event_id The message event to edit.
4141+ @param new_body The new message body. *)
4242+val edit_message :
4343+ Client.t ->
4444+ room_id:Matrix_proto.Id.Room_id.t ->
4545+ event_id:Matrix_proto.Id.Event_id.t ->
4646+ new_body:string ->
4747+ (Matrix_proto.Id.Event_id.t, Error.t) result
4848+4949+(** {1 Replies} *)
5050+5151+(** Send a reply to a message.
5252+5353+ @param room_id The room containing the message.
5454+ @param event_id The message event to reply to.
5555+ @param body The reply text. *)
5656+val send_reply :
5757+ Client.t ->
5858+ room_id:Matrix_proto.Id.Room_id.t ->
5959+ event_id:Matrix_proto.Id.Event_id.t ->
6060+ body:string ->
6161+ (Matrix_proto.Id.Event_id.t, Error.t) result
6262+6363+(** {1 Threads} *)
6464+6565+(** Send a message in a thread.
6666+6767+ @param room_id The room containing the thread.
6868+ @param thread_root_id The event ID of the thread root.
6969+ @param reply_to_id Optional event to reply to within the thread.
7070+ @param body The message text. *)
7171+val send_in_thread :
7272+ Client.t ->
7373+ room_id:Matrix_proto.Id.Room_id.t ->
7474+ thread_root_id:Matrix_proto.Id.Event_id.t ->
7575+ ?reply_to_id:Matrix_proto.Id.Event_id.t ->
7676+ body:string ->
7777+ unit ->
7878+ (Matrix_proto.Id.Event_id.t, Error.t) result
7979+8080+(** {1 Querying Relations} *)
8181+8282+(** An aggregated event in a relations response. *)
8383+type aggregation = {
8484+ event_id : Matrix_proto.Id.Event_id.t;
8585+ origin_server_ts : int64;
8686+ sender : Matrix_proto.Id.User_id.t;
8787+}
8888+8989+(** Relations response. *)
9090+type relations_response = {
9191+ chunk : aggregation list;
9292+ next_batch : string option;
9393+ prev_batch : string option;
9494+}
9595+9696+(** Get relations for an event.
9797+9898+ @param room_id The room containing the event.
9999+ @param event_id The event to get relations for.
100100+ @param rel_type Filter by relation type.
101101+ @param event_type Filter by event type.
102102+ @param limit Maximum number of results.
103103+ @param from Pagination token. *)
104104+val get_relations :
105105+ Client.t ->
106106+ room_id:Matrix_proto.Id.Room_id.t ->
107107+ event_id:Matrix_proto.Id.Event_id.t ->
108108+ ?rel_type:relation_type ->
109109+ ?event_type:string ->
110110+ ?limit:int ->
111111+ ?from:string ->
112112+ unit ->
113113+ (relations_response, Error.t) result
114114+115115+(** Get all reactions for an event. *)
116116+val get_reactions :
117117+ Client.t ->
118118+ room_id:Matrix_proto.Id.Room_id.t ->
119119+ event_id:Matrix_proto.Id.Event_id.t ->
120120+ (relations_response, Error.t) result
+257
lib/matrix_client/room_preview.ml
···11+(** Room preview operations.
22+33+ Get information about a room before joining it. *)
44+55+(** Room preview information *)
66+type room_preview = {
77+ room_id : Matrix_proto.Id.Room_id.t;
88+ name : string option;
99+ topic : string option;
1010+ avatar_url : string option;
1111+ canonical_alias : Matrix_proto.Id.Room_alias.t option;
1212+ join_rule : Matrix_proto.Event.Join_rule.t option;
1313+ num_joined_members : int;
1414+ room_type : string option;
1515+ world_readable : bool;
1616+ guest_can_join : bool;
1717+ membership : Matrix_proto.Event.Membership.t option;
1818+}
1919+2020+(** Summary returned by room summary endpoint (MSC3266) *)
2121+type room_summary = {
2222+ room_id : string;
2323+ membership : Matrix_proto.Event.Membership.t option;
2424+ is_encrypted : bool option;
2525+ room_name : string option;
2626+ topic : string option;
2727+ avatar_url : string option;
2828+ canonical_alias : string option;
2929+ joined_members_count : int option;
3030+ invited_members_count : int option;
3131+ room_version : string option;
3232+ room_type : string option;
3333+ join_rule : Matrix_proto.Event.Join_rule.t option;
3434+ guest_can_join : bool option;
3535+ world_readable : bool option;
3636+}
3737+3838+let room_summary_jsont =
3939+ Jsont.Object.(
4040+ map (fun room_id membership is_encrypted room_name topic avatar_url
4141+ canonical_alias joined_members_count invited_members_count
4242+ room_version room_type join_rule guest_can_join world_readable ->
4343+ { room_id; membership; is_encrypted; room_name; topic; avatar_url;
4444+ canonical_alias; joined_members_count; invited_members_count;
4545+ room_version; room_type; join_rule; guest_can_join; world_readable })
4646+ |> mem "room_id" Jsont.string ~enc:(fun t -> t.room_id)
4747+ |> opt_mem "membership" Matrix_proto.Event.Membership.jsont ~enc:(fun t -> t.membership)
4848+ |> opt_mem "is_encrypted" Jsont.bool ~enc:(fun t -> t.is_encrypted)
4949+ |> opt_mem "room_name" Jsont.string ~enc:(fun t -> t.room_name)
5050+ |> opt_mem "topic" Jsont.string ~enc:(fun t -> t.topic)
5151+ |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url)
5252+ |> opt_mem "canonical_alias" Jsont.string ~enc:(fun t -> t.canonical_alias)
5353+ |> opt_mem "joined_members_count" Jsont.int ~enc:(fun t -> t.joined_members_count)
5454+ |> opt_mem "invited_members_count" Jsont.int ~enc:(fun t -> t.invited_members_count)
5555+ |> opt_mem "room_version" Jsont.string ~enc:(fun t -> t.room_version)
5656+ |> opt_mem "room_type" Jsont.string ~enc:(fun t -> t.room_type)
5757+ |> opt_mem "join_rule" Matrix_proto.Event.Join_rule.jsont ~enc:(fun t -> t.join_rule)
5858+ |> opt_mem "guest_can_join" Jsont.bool ~enc:(fun t -> t.guest_can_join)
5959+ |> opt_mem "world_readable" Jsont.bool ~enc:(fun t -> t.world_readable)
6060+ |> finish)
6161+6262+(** Get room summary (MSC3266).
6363+6464+ This is the preferred way to get room information before joining. *)
6565+let get_summary client ~room_id_or_alias ?via () =
6666+ let id_str = match room_id_or_alias with
6767+ | `Room_id id -> Matrix_proto.Id.Room_id.to_string id
6868+ | `Room_alias alias -> Matrix_proto.Id.Room_alias.to_string alias
6969+ in
7070+ let path = Printf.sprintf "/rooms/%s/summary" (Uri.pct_encode id_str) in
7171+ let query = match via with
7272+ | Some servers -> Some (List.map (fun s -> ("via", s)) servers)
7373+ | None -> None
7474+ in
7575+ match Client.get client ~path ?query () with
7676+ | Error e -> Error e
7777+ | Ok body -> Client.decode_response room_summary_jsont body
7878+7979+(** Public room from directory listing *)
8080+type public_room = {
8181+ room_id : Matrix_proto.Id.Room_id.t;
8282+ name : string option;
8383+ topic : string option;
8484+ avatar_url : string option;
8585+ canonical_alias : Matrix_proto.Id.Room_alias.t option;
8686+ num_joined_members : int;
8787+ world_readable : bool;
8888+ guest_can_join : bool;
8989+ join_rule : Matrix_proto.Event.Join_rule.t option;
9090+ room_type : string option;
9191+}
9292+9393+let public_room_jsont =
9494+ Jsont.Object.(
9595+ map (fun room_id name topic avatar_url canonical_alias num_joined_members
9696+ world_readable guest_can_join join_rule room_type ->
9797+ { room_id; name; topic; avatar_url; canonical_alias; num_joined_members;
9898+ world_readable; guest_can_join; join_rule; room_type })
9999+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont ~enc:(fun t -> t.room_id)
100100+ |> opt_mem "name" Jsont.string ~enc:(fun t -> t.name)
101101+ |> opt_mem "topic" Jsont.string ~enc:(fun t -> t.topic)
102102+ |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url)
103103+ |> opt_mem "canonical_alias" Matrix_proto.Id.Room_alias.jsont ~enc:(fun t -> t.canonical_alias)
104104+ |> mem "num_joined_members" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.num_joined_members)
105105+ |> mem "world_readable" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.world_readable)
106106+ |> mem "guest_can_join" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.guest_can_join)
107107+ |> opt_mem "join_rule" Matrix_proto.Event.Join_rule.jsont ~enc:(fun t -> t.join_rule)
108108+ |> opt_mem "room_type" Jsont.string ~enc:(fun t -> t.room_type)
109109+ |> finish)
110110+111111+type public_rooms_response = {
112112+ chunk : public_room list;
113113+ next_batch : string option;
114114+ prev_batch : string option;
115115+ total_room_count_estimate : int option;
116116+}
117117+118118+let public_rooms_response_jsont =
119119+ Jsont.Object.(
120120+ map (fun chunk next_batch prev_batch total_room_count_estimate ->
121121+ { chunk; next_batch; prev_batch; total_room_count_estimate })
122122+ |> mem "chunk" (Jsont.list public_room_jsont) ~dec_absent:[]
123123+ ~enc:(fun t -> t.chunk)
124124+ |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch)
125125+ |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch)
126126+ |> opt_mem "total_room_count_estimate" Jsont.int
127127+ ~enc:(fun t -> t.total_room_count_estimate)
128128+ |> finish)
129129+130130+(** Get list of public rooms.
131131+132132+ @param limit Maximum number of rooms to return
133133+ @param since Pagination token
134134+ @param server Server to query for rooms *)
135135+let get_public_rooms client ?limit ?since ?server () =
136136+ let path = "/publicRooms" in
137137+ let query =
138138+ []
139139+ |> (fun q -> match limit with Some l -> ("limit", string_of_int l) :: q | None -> q)
140140+ |> (fun q -> match since with Some s -> ("since", s) :: q | None -> q)
141141+ |> (fun q -> match server with Some s -> ("server", s) :: q | None -> q)
142142+ in
143143+ let query = if query = [] then None else Some query in
144144+ match Client.get client ~path ?query () with
145145+ | Error e -> Error e
146146+ | Ok body -> Client.decode_response public_rooms_response_jsont body
147147+148148+(** Search filter for public rooms *)
149149+type public_rooms_filter = {
150150+ generic_search_term : string option;
151151+ room_types : string list option;
152152+}
153153+154154+let public_rooms_filter_jsont =
155155+ Jsont.Object.(
156156+ map (fun generic_search_term room_types ->
157157+ { generic_search_term; room_types })
158158+ |> opt_mem "generic_search_term" Jsont.string ~enc:(fun t -> t.generic_search_term)
159159+ |> opt_mem "room_types" (Jsont.list Jsont.string) ~enc:(fun t -> t.room_types)
160160+ |> finish)
161161+162162+type search_public_rooms_request = {
163163+ limit : int option;
164164+ since : string option;
165165+ filter : public_rooms_filter option;
166166+ include_all_networks : bool option;
167167+ third_party_instance_id : string option;
168168+} [@@warning "-69"]
169169+170170+let search_public_rooms_request_jsont =
171171+ Jsont.Object.(
172172+ map (fun limit since filter include_all_networks third_party_instance_id ->
173173+ { limit; since; filter; include_all_networks; third_party_instance_id })
174174+ |> opt_mem "limit" Jsont.int ~enc:(fun t -> t.limit)
175175+ |> opt_mem "since" Jsont.string ~enc:(fun t -> t.since)
176176+ |> opt_mem "filter" public_rooms_filter_jsont ~enc:(fun t -> t.filter)
177177+ |> opt_mem "include_all_networks" Jsont.bool ~enc:(fun t -> t.include_all_networks)
178178+ |> opt_mem "third_party_instance_id" Jsont.string ~enc:(fun t -> t.third_party_instance_id)
179179+ |> finish)
180180+181181+(** Search public rooms with filters.
182182+183183+ @param search_term Text to search for
184184+ @param limit Maximum number of rooms
185185+ @param since Pagination token
186186+ @param room_types Filter by room type
187187+ @param server Server to query *)
188188+let search_public_rooms client ?search_term ?limit ?since ?room_types ?server () =
189189+ let path = "/publicRooms" in
190190+ let query = match server with
191191+ | Some s -> Some [("server", s)]
192192+ | None -> None
193193+ in
194194+ let filter = match search_term, room_types with
195195+ | None, None -> None
196196+ | _ -> Some { generic_search_term = search_term; room_types }
197197+ in
198198+ let request = {
199199+ limit;
200200+ since;
201201+ filter;
202202+ include_all_networks = None;
203203+ third_party_instance_id = None;
204204+ } in
205205+ match Client.encode_body search_public_rooms_request_jsont request with
206206+ | Error e -> Error e
207207+ | Ok body ->
208208+ match Client.post client ~path ~body ?query () with
209209+ | Error e -> Error e
210210+ | Ok resp_body -> Client.decode_response public_rooms_response_jsont resp_body
211211+212212+(** Resolve a room alias to a room ID and servers.
213213+214214+ @param room_alias The room alias to resolve *)
215215+let resolve_alias client ~room_alias =
216216+ let alias_str = Matrix_proto.Id.Room_alias.to_string room_alias in
217217+ let path = Printf.sprintf "/directory/room/%s" (Uri.pct_encode alias_str) in
218218+ let response_jsont = Jsont.Object.(
219219+ map (fun room_id servers -> (room_id, servers))
220220+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont
221221+ |> mem "servers" (Jsont.list Jsont.string) ~dec_absent:[]
222222+ |> finish)
223223+ in
224224+ match Client.get client ~path () with
225225+ | Error e -> Error e
226226+ | Ok body -> Client.decode_response response_jsont body
227227+228228+(** Knock on a room (MSC2403).
229229+230230+ Request to join a room that has knock join rules.
231231+232232+ @param room_id_or_alias The room to knock on
233233+ @param reason Optional reason for the knock request
234234+ @param via Servers to try *)
235235+let knock client ~room_id_or_alias ?reason ?(via = []) () =
236236+ let id_str = match room_id_or_alias with
237237+ | `Room_id id -> Matrix_proto.Id.Room_id.to_string id
238238+ | `Room_alias alias -> Matrix_proto.Id.Room_alias.to_string alias
239239+ in
240240+ let path = Printf.sprintf "/knock/%s" (Uri.pct_encode id_str) in
241241+ let query = if via = [] then None else Some (List.map (fun s -> ("server_name", s)) via) in
242242+ let request_jsont = Jsont.Object.(
243243+ map (fun reason -> reason)
244244+ |> opt_mem "reason" Jsont.string ~enc:Fun.id
245245+ |> finish)
246246+ in
247247+ let response_jsont = Jsont.Object.(
248248+ map (fun room_id -> room_id)
249249+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont
250250+ |> finish)
251251+ in
252252+ match Client.encode_body request_jsont reason with
253253+ | Error e -> Error e
254254+ | Ok body ->
255255+ match Client.post client ~path ~body ?query () with
256256+ | Error e -> Error e
257257+ | Ok resp_body -> Client.decode_response response_jsont resp_body
+461
lib/matrix_client/rooms.ml
···11+(** Room operations. *)
22+33+type visibility = [ `Public | `Private ]
44+55+type preset =
66+ | Private_chat
77+ | Public_chat
88+ | Trusted_private_chat
99+1010+let preset_to_string = function
1111+ | Private_chat -> "private_chat"
1212+ | Public_chat -> "public_chat"
1313+ | Trusted_private_chat -> "trusted_private_chat"
1414+1515+(* Create room *)
1616+type create_request = {
1717+ name : string option;
1818+ topic : string option;
1919+ visibility : string option;
2020+ preset : string option;
2121+ room_alias_local_part : string option;
2222+ invite : string list;
2323+ is_direct : bool option;
2424+ room_type : string option;
2525+ creation_content : Jsont.json option;
2626+} [@@warning "-69"]
2727+2828+let create_request_jsont : create_request Jsont.t =
2929+ let open Jsont.Object in
3030+ map (fun name topic visibility preset room_alias_local_part invite is_direct room_type creation_content ->
3131+ ({ name; topic; visibility; preset; room_alias_local_part; invite; is_direct; room_type; creation_content } : create_request))
3232+ |> opt_mem "name" Jsont.string ~enc:(fun (t : create_request) -> t.name)
3333+ |> opt_mem "topic" Jsont.string ~enc:(fun (t : create_request) -> t.topic)
3434+ |> opt_mem "visibility" Jsont.string ~enc:(fun (t : create_request) -> t.visibility)
3535+ |> opt_mem "preset" Jsont.string ~enc:(fun (t : create_request) -> t.preset)
3636+ |> opt_mem "room_alias_local_part" Jsont.string ~enc:(fun (t : create_request) -> t.room_alias_local_part)
3737+ |> mem "invite" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun (t : create_request) -> t.invite)
3838+ |> opt_mem "is_direct" Jsont.bool ~enc:(fun (t : create_request) -> t.is_direct)
3939+ |> opt_mem "room_type" Jsont.string ~enc:(fun (t : create_request) -> t.room_type)
4040+ |> opt_mem "creation_content" Jsont.json ~enc:(fun (t : create_request) -> t.creation_content)
4141+ |> finish
4242+4343+type create_response = {
4444+ room_id : Matrix_proto.Id.Room_id.t;
4545+}
4646+4747+let create_response_jsont =
4848+ Jsont.Object.(
4949+ map (fun room_id -> { room_id })
5050+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont
5151+ |> finish)
5252+5353+let create client ?name ?topic ?visibility ?preset ?room_alias_local_part ?invite ?is_direct ?room_type () =
5454+ let visibility_str = match visibility with
5555+ | Some `Public -> Some "public"
5656+ | Some `Private -> Some "private"
5757+ | None -> None
5858+ in
5959+ let preset_str = Option.map preset_to_string preset in
6060+ let invite_strs = match invite with
6161+ | Some ids -> List.map Matrix_proto.Id.User_id.to_string ids
6262+ | None -> []
6363+ in
6464+ let request = {
6565+ name; topic;
6666+ visibility = visibility_str;
6767+ preset = preset_str;
6868+ room_alias_local_part;
6969+ invite = invite_strs;
7070+ is_direct;
7171+ room_type;
7272+ creation_content = None;
7373+ } in
7474+ match Client.encode_body create_request_jsont request with
7575+ | Error e -> Error e
7676+ | Ok body ->
7777+ match Client.post client ~path:"/createRoom" ~body () with
7878+ | Error e -> Error e
7979+ | Ok body ->
8080+ match Client.decode_response create_response_jsont body with
8181+ | Error e -> Error e
8282+ | Ok resp -> Ok resp.room_id
8383+8484+(** Alias for create with room_type support *)
8585+let create_room = create
8686+8787+(* Join room *)
8888+type join_request = {
8989+ reason : string option;
9090+} [@@warning "-69"]
9191+9292+let join_request_jsont =
9393+ Jsont.Object.(
9494+ map (fun reason -> { reason })
9595+ |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason)
9696+ |> finish)
9797+9898+type join_response = {
9999+ room_id : Matrix_proto.Id.Room_id.t;
100100+}
101101+102102+let join_response_jsont =
103103+ Jsont.Object.(
104104+ map (fun room_id -> { room_id })
105105+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont
106106+ |> finish)
107107+108108+let join client ~room_id_or_alias ?via ?reason () =
109109+ let path = Printf.sprintf "/join/%s" (Uri.pct_encode room_id_or_alias) in
110110+ let query = match via with
111111+ | Some servers -> Some (List.map (fun s -> ("server_name", s)) servers)
112112+ | None -> None
113113+ in
114114+ let request = { reason } in
115115+ match Client.encode_body join_request_jsont request with
116116+ | Error e -> Error e
117117+ | Ok body ->
118118+ match Client.post client ~path ?query ~body () with
119119+ | Error e -> Error e
120120+ | Ok body ->
121121+ match Client.decode_response join_response_jsont body with
122122+ | Error e -> Error e
123123+ | Ok resp -> Ok resp.room_id
124124+125125+(* Leave room *)
126126+type leave_request = {
127127+ reason : string option;
128128+} [@@warning "-69"]
129129+130130+let leave_request_jsont =
131131+ Jsont.Object.(
132132+ map (fun reason -> { reason })
133133+ |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason)
134134+ |> finish)
135135+136136+let leave client ~room_id ?reason () =
137137+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
138138+ let path = Printf.sprintf "/rooms/%s/leave" (Uri.pct_encode room_id_str) in
139139+ let request = { reason } in
140140+ match Client.encode_body leave_request_jsont request with
141141+ | Error e -> Error e
142142+ | Ok body ->
143143+ match Client.post client ~path ~body () with
144144+ | Error e -> Error e
145145+ | Ok _ -> Ok ()
146146+147147+(* Forget room *)
148148+let forget client ~room_id =
149149+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
150150+ let path = Printf.sprintf "/rooms/%s/forget" (Uri.pct_encode room_id_str) in
151151+ match Client.post client ~path ~body:"{}" () with
152152+ | Error e -> Error e
153153+ | Ok _ -> Ok ()
154154+155155+(* Invite *)
156156+type invite_request = {
157157+ user_id : string;
158158+ reason : string option;
159159+} [@@warning "-69"]
160160+161161+let invite_request_jsont =
162162+ Jsont.Object.(
163163+ map (fun user_id reason -> { user_id; reason })
164164+ |> mem "user_id" Jsont.string
165165+ |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason)
166166+ |> finish)
167167+168168+let invite client ~room_id ~user_id ?reason () =
169169+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
170170+ let path = Printf.sprintf "/rooms/%s/invite" (Uri.pct_encode room_id_str) in
171171+ let request = { user_id = Matrix_proto.Id.User_id.to_string user_id; reason } in
172172+ match Client.encode_body invite_request_jsont request with
173173+ | Error e -> Error e
174174+ | Ok body ->
175175+ match Client.post client ~path ~body () with
176176+ | Error e -> Error e
177177+ | Ok _ -> Ok ()
178178+179179+(* Kick *)
180180+type kick_request = {
181181+ user_id : string;
182182+ reason : string option;
183183+} [@@warning "-69"]
184184+185185+let kick_request_jsont =
186186+ Jsont.Object.(
187187+ map (fun user_id reason -> { user_id; reason })
188188+ |> mem "user_id" Jsont.string
189189+ |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason)
190190+ |> finish)
191191+192192+let kick client ~room_id ~user_id ?reason () =
193193+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
194194+ let path = Printf.sprintf "/rooms/%s/kick" (Uri.pct_encode room_id_str) in
195195+ let request = { user_id = Matrix_proto.Id.User_id.to_string user_id; reason } in
196196+ match Client.encode_body kick_request_jsont request with
197197+ | Error e -> Error e
198198+ | Ok body ->
199199+ match Client.post client ~path ~body () with
200200+ | Error e -> Error e
201201+ | Ok _ -> Ok ()
202202+203203+(* Ban *)
204204+type ban_request = {
205205+ user_id : string;
206206+ reason : string option;
207207+} [@@warning "-69"]
208208+209209+let ban_request_jsont =
210210+ Jsont.Object.(
211211+ map (fun user_id reason -> { user_id; reason })
212212+ |> mem "user_id" Jsont.string
213213+ |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason)
214214+ |> finish)
215215+216216+let ban client ~room_id ~user_id ?reason () =
217217+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
218218+ let path = Printf.sprintf "/rooms/%s/ban" (Uri.pct_encode room_id_str) in
219219+ let request = { user_id = Matrix_proto.Id.User_id.to_string user_id; reason } in
220220+ match Client.encode_body ban_request_jsont request with
221221+ | Error e -> Error e
222222+ | Ok body ->
223223+ match Client.post client ~path ~body () with
224224+ | Error e -> Error e
225225+ | Ok _ -> Ok ()
226226+227227+(* Unban *)
228228+let unban client ~room_id ~user_id ?reason () =
229229+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
230230+ let path = Printf.sprintf "/rooms/%s/unban" (Uri.pct_encode room_id_str) in
231231+ let request = { user_id = Matrix_proto.Id.User_id.to_string user_id; reason } in
232232+ match Client.encode_body ban_request_jsont request with
233233+ | Error e -> Error e
234234+ | Ok body ->
235235+ match Client.post client ~path ~body () with
236236+ | Error e -> Error e
237237+ | Ok _ -> Ok ()
238238+239239+(* Get joined rooms *)
240240+type joined_rooms_response = {
241241+ joined_rooms : string list;
242242+}
243243+244244+let joined_rooms_response_jsont =
245245+ Jsont.Object.(
246246+ map (fun joined_rooms -> { joined_rooms })
247247+ |> mem "joined_rooms" (Jsont.list Jsont.string) ~dec_absent:[]
248248+ |> finish)
249249+250250+let get_joined_rooms client =
251251+ match Client.get client ~path:"/joined_rooms" () with
252252+ | Error e -> Error e
253253+ | Ok body ->
254254+ match Client.decode_response joined_rooms_response_jsont body with
255255+ | Error e -> Error e
256256+ | Ok resp ->
257257+ let room_ids = List.filter_map (fun s ->
258258+ match Matrix_proto.Id.Room_id.of_string s with
259259+ | Ok id -> Some id
260260+ | Error _ -> None
261261+ ) resp.joined_rooms in
262262+ Ok room_ids
263263+264264+(* Get members *)
265265+type member = {
266266+ user_id : Matrix_proto.Id.User_id.t;
267267+ display_name : string option;
268268+ avatar_url : string option;
269269+ membership : string;
270270+}
271271+272272+type member_event = {
273273+ state_key : string;
274274+ content : member_content;
275275+}
276276+and member_content = {
277277+ membership : string;
278278+ displayname : string option;
279279+ avatar_url : string option;
280280+}
281281+282282+let member_content_jsont =
283283+ Jsont.Object.(
284284+ map (fun membership displayname avatar_url ->
285285+ { membership; displayname; avatar_url })
286286+ |> mem "membership" Jsont.string
287287+ |> opt_mem "displayname" Jsont.string ~enc:(fun t -> t.displayname)
288288+ |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url)
289289+ |> finish)
290290+291291+let member_event_jsont =
292292+ Jsont.Object.(
293293+ map (fun state_key content -> { state_key; content })
294294+ |> mem "state_key" Jsont.string
295295+ |> mem "content" member_content_jsont
296296+ |> finish)
297297+298298+type members_response = {
299299+ chunk : member_event list;
300300+}
301301+302302+let members_response_jsont =
303303+ Jsont.Object.(
304304+ map (fun chunk -> { chunk })
305305+ |> mem "chunk" (Jsont.list member_event_jsont) ~dec_absent:[]
306306+ |> finish)
307307+308308+let get_members client ~room_id ?membership ?not_membership () =
309309+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
310310+ let path = Printf.sprintf "/rooms/%s/members" (Uri.pct_encode room_id_str) in
311311+ let query =
312312+ []
313313+ |> (fun q -> match membership with Some m -> ("membership", m) :: q | None -> q)
314314+ |> (fun q -> match not_membership with Some m -> ("not_membership", m) :: q | None -> q)
315315+ in
316316+ let query = if query = [] then None else Some query in
317317+ match Client.get client ~path ?query () with
318318+ | Error e -> Error e
319319+ | Ok body ->
320320+ match Client.decode_response members_response_jsont body with
321321+ | Error e -> Error e
322322+ | Ok resp ->
323323+ let members = List.filter_map (fun ev ->
324324+ match Matrix_proto.Id.User_id.of_string ev.state_key with
325325+ | Ok user_id ->
326326+ Some {
327327+ user_id;
328328+ display_name = ev.content.displayname;
329329+ avatar_url = ev.content.avatar_url;
330330+ membership = ev.content.membership;
331331+ }
332332+ | Error _ -> None
333333+ ) resp.chunk in
334334+ Ok members
335335+336336+(* Public rooms *)
337337+type public_room = {
338338+ room_id : Matrix_proto.Id.Room_id.t;
339339+ name : string option;
340340+ topic : string option;
341341+ num_joined_members : int;
342342+ world_readable : bool;
343343+ guest_can_join : bool;
344344+ avatar_url : string option;
345345+ canonical_alias : string option;
346346+}
347347+348348+let public_room_jsont =
349349+ Jsont.Object.(
350350+ map (fun room_id name topic num_joined_members world_readable guest_can_join avatar_url canonical_alias ->
351351+ { room_id; name; topic; num_joined_members; world_readable; guest_can_join; avatar_url; canonical_alias })
352352+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont
353353+ |> opt_mem "name" Jsont.string ~enc:(fun t -> t.name)
354354+ |> opt_mem "topic" Jsont.string ~enc:(fun t -> t.topic)
355355+ |> mem "num_joined_members" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.num_joined_members)
356356+ |> mem "world_readable" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.world_readable)
357357+ |> mem "guest_can_join" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.guest_can_join)
358358+ |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url)
359359+ |> opt_mem "canonical_alias" Jsont.string ~enc:(fun t -> t.canonical_alias)
360360+ |> finish)
361361+362362+type public_rooms_response = {
363363+ chunk : public_room list;
364364+ next_batch : string option;
365365+ prev_batch : string option;
366366+ total_room_count_estimate : int option;
367367+}
368368+369369+let public_rooms_response_jsont =
370370+ Jsont.Object.(
371371+ map (fun chunk next_batch prev_batch total_room_count_estimate ->
372372+ { chunk; next_batch; prev_batch; total_room_count_estimate })
373373+ |> mem "chunk" (Jsont.list public_room_jsont) ~dec_absent:[]
374374+ |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch)
375375+ |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch)
376376+ |> opt_mem "total_room_count_estimate" Jsont.int ~enc:(fun t -> t.total_room_count_estimate)
377377+ |> finish)
378378+379379+let get_public_rooms client ?limit ?since ?server () =
380380+ let query =
381381+ []
382382+ |> (fun q -> match limit with Some l -> ("limit", string_of_int l) :: q | None -> q)
383383+ |> (fun q -> match since with Some s -> ("since", s) :: q | None -> q)
384384+ |> (fun q -> match server with Some s -> ("server", s) :: q | None -> q)
385385+ in
386386+ let query = if query = [] then None else Some query in
387387+ match Client.get client ~path:"/publicRooms" ?query () with
388388+ | Error e -> Error e
389389+ | Ok body -> Client.decode_response public_rooms_response_jsont body
390390+391391+(* Power levels *)
392392+type power_levels = {
393393+ ban : int;
394394+ events : (string * int) list;
395395+ events_default : int;
396396+ invite : int;
397397+ kick : int;
398398+ redact : int;
399399+ state_default : int;
400400+ users : (string * int) list;
401401+ users_default : int;
402402+ notifications : (string * int) list;
403403+}
404404+405405+module StringMap = Map.Make(String)
406406+407407+let string_int_map_jsont : (string * int) list Jsont.t =
408408+ let map_jsont = Jsont.Object.as_string_map Jsont.int in
409409+ Jsont.map
410410+ ~dec:(fun m -> StringMap.bindings m)
411411+ ~enc:(fun l -> List.to_seq l |> StringMap.of_seq)
412412+ map_jsont
413413+414414+let power_levels_jsont : power_levels Jsont.t =
415415+ Jsont.Object.(
416416+ map (fun ban events events_default invite kick redact state_default users users_default notifications ->
417417+ ({ ban; events; events_default; invite; kick; redact; state_default; users; users_default; notifications } : power_levels))
418418+ |> mem "ban" Jsont.int ~dec_absent:50 ~enc:(fun (t : power_levels) -> t.ban)
419419+ |> mem "events" string_int_map_jsont ~dec_absent:[] ~enc:(fun (t : power_levels) -> t.events)
420420+ |> mem "events_default" Jsont.int ~dec_absent:0 ~enc:(fun (t : power_levels) -> t.events_default)
421421+ |> mem "invite" Jsont.int ~dec_absent:0 ~enc:(fun (t : power_levels) -> t.invite)
422422+ |> mem "kick" Jsont.int ~dec_absent:50 ~enc:(fun (t : power_levels) -> t.kick)
423423+ |> mem "redact" Jsont.int ~dec_absent:50 ~enc:(fun (t : power_levels) -> t.redact)
424424+ |> mem "state_default" Jsont.int ~dec_absent:50 ~enc:(fun (t : power_levels) -> t.state_default)
425425+ |> mem "users" string_int_map_jsont ~dec_absent:[] ~enc:(fun (t : power_levels) -> t.users)
426426+ |> mem "users_default" Jsont.int ~dec_absent:0 ~enc:(fun (t : power_levels) -> t.users_default)
427427+ |> mem "notifications" string_int_map_jsont ~dec_absent:[] ~enc:(fun (t : power_levels) -> t.notifications)
428428+ |> finish)
429429+430430+let get_power_levels client ~room_id =
431431+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
432432+ let path = Printf.sprintf "/rooms/%s/state/m.room.power_levels/" (Uri.pct_encode room_id_str) in
433433+ match Client.get client ~path () with
434434+ | Error e -> Error e
435435+ | Ok body -> Client.decode_response power_levels_jsont body
436436+437437+let set_power_levels client ~room_id ~power_levels =
438438+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
439439+ let path = Printf.sprintf "/rooms/%s/state/m.room.power_levels/" (Uri.pct_encode room_id_str) in
440440+ match Client.encode_body power_levels_jsont power_levels with
441441+ | Error e -> Error e
442442+ | Ok body ->
443443+ match Client.put client ~path ~body () with
444444+ | Error e -> Error e
445445+ | Ok _ -> Ok ()
446446+447447+let get_user_power_level power_levels user_id =
448448+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
449449+ match List.assoc_opt user_id_str power_levels.users with
450450+ | Some level -> level
451451+ | None -> power_levels.users_default
452452+453453+let set_user_power_level client ~room_id ~user_id ~level =
454454+ match get_power_levels client ~room_id with
455455+ | Error e -> Error e
456456+ | Ok pl ->
457457+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
458458+ let users = List.filter (fun (k, _) -> k <> user_id_str) pl.users in
459459+ let users = (user_id_str, level) :: users in
460460+ let power_levels = { pl with users } in
461461+ set_power_levels client ~room_id ~power_levels
+228
lib/matrix_client/rooms.mli
···11+(** Room operations. *)
22+33+(** {1 Room Creation} *)
44+55+(** Room visibility. *)
66+type visibility = [ `Public | `Private ]
77+88+(** Room preset. *)
99+type preset =
1010+ | Private_chat
1111+ | Public_chat
1212+ | Trusted_private_chat
1313+1414+(** Create a new room.
1515+1616+ Returns the room ID of the newly created room. *)
1717+val create :
1818+ Client.t ->
1919+ ?name:string ->
2020+ ?topic:string ->
2121+ ?visibility:visibility ->
2222+ ?preset:preset ->
2323+ ?room_alias_local_part:string ->
2424+ ?invite:Matrix_proto.Id.User_id.t list ->
2525+ ?is_direct:bool ->
2626+ ?room_type:string ->
2727+ unit ->
2828+ (Matrix_proto.Id.Room_id.t, Error.t) result
2929+3030+(** Alias for create with room_type support *)
3131+val create_room :
3232+ Client.t ->
3333+ ?name:string ->
3434+ ?topic:string ->
3535+ ?visibility:visibility ->
3636+ ?preset:preset ->
3737+ ?room_alias_local_part:string ->
3838+ ?invite:Matrix_proto.Id.User_id.t list ->
3939+ ?is_direct:bool ->
4040+ ?room_type:string ->
4141+ unit ->
4242+ (Matrix_proto.Id.Room_id.t, Error.t) result
4343+4444+(** {1 Joining and Leaving} *)
4545+4646+(** Join a room by ID or alias.
4747+4848+ Returns the room ID (which may differ from input if an alias was used). *)
4949+val join :
5050+ Client.t ->
5151+ room_id_or_alias:string ->
5252+ ?via:string list ->
5353+ ?reason:string ->
5454+ unit ->
5555+ (Matrix_proto.Id.Room_id.t, Error.t) result
5656+5757+(** Leave a room. *)
5858+val leave :
5959+ Client.t ->
6060+ room_id:Matrix_proto.Id.Room_id.t ->
6161+ ?reason:string ->
6262+ unit ->
6363+ (unit, Error.t) result
6464+6565+(** Forget a room (remove from room list after leaving). *)
6666+val forget :
6767+ Client.t ->
6868+ room_id:Matrix_proto.Id.Room_id.t ->
6969+ (unit, Error.t) result
7070+7171+(** {1 Membership Management} *)
7272+7373+(** Invite a user to a room. *)
7474+val invite :
7575+ Client.t ->
7676+ room_id:Matrix_proto.Id.Room_id.t ->
7777+ user_id:Matrix_proto.Id.User_id.t ->
7878+ ?reason:string ->
7979+ unit ->
8080+ (unit, Error.t) result
8181+8282+(** Kick a user from a room. *)
8383+val kick :
8484+ Client.t ->
8585+ room_id:Matrix_proto.Id.Room_id.t ->
8686+ user_id:Matrix_proto.Id.User_id.t ->
8787+ ?reason:string ->
8888+ unit ->
8989+ (unit, Error.t) result
9090+9191+(** Ban a user from a room. *)
9292+val ban :
9393+ Client.t ->
9494+ room_id:Matrix_proto.Id.Room_id.t ->
9595+ user_id:Matrix_proto.Id.User_id.t ->
9696+ ?reason:string ->
9797+ unit ->
9898+ (unit, Error.t) result
9999+100100+(** Unban a user from a room. *)
101101+val unban :
102102+ Client.t ->
103103+ room_id:Matrix_proto.Id.Room_id.t ->
104104+ user_id:Matrix_proto.Id.User_id.t ->
105105+ ?reason:string ->
106106+ unit ->
107107+ (unit, Error.t) result
108108+109109+(** {1 Room Queries} *)
110110+111111+(** Get list of joined rooms. *)
112112+val get_joined_rooms :
113113+ Client.t ->
114114+ (Matrix_proto.Id.Room_id.t list, Error.t) result
115115+116116+(** Member info. *)
117117+type member = {
118118+ user_id : Matrix_proto.Id.User_id.t;
119119+ display_name : string option;
120120+ avatar_url : string option;
121121+ membership : string;
122122+}
123123+124124+(** Get room members.
125125+126126+ @param membership Filter by membership state (join, invite, leave, ban).
127127+ @param not_membership Exclude members with this membership state. *)
128128+val get_members :
129129+ Client.t ->
130130+ room_id:Matrix_proto.Id.Room_id.t ->
131131+ ?membership:string ->
132132+ ?not_membership:string ->
133133+ unit ->
134134+ (member list, Error.t) result
135135+136136+(** {1 Public Rooms} *)
137137+138138+(** Public room info. *)
139139+type public_room = {
140140+ room_id : Matrix_proto.Id.Room_id.t;
141141+ name : string option;
142142+ topic : string option;
143143+ num_joined_members : int;
144144+ world_readable : bool;
145145+ guest_can_join : bool;
146146+ avatar_url : string option;
147147+ canonical_alias : string option;
148148+}
149149+150150+(** Public rooms response. *)
151151+type public_rooms_response = {
152152+ chunk : public_room list;
153153+ next_batch : string option;
154154+ prev_batch : string option;
155155+ total_room_count_estimate : int option;
156156+}
157157+158158+(** Get public rooms.
159159+160160+ @param limit Maximum number of rooms to return.
161161+ @param since Pagination token.
162162+ @param server Server to fetch rooms from. *)
163163+val get_public_rooms :
164164+ Client.t ->
165165+ ?limit:int ->
166166+ ?since:string ->
167167+ ?server:string ->
168168+ unit ->
169169+ (public_rooms_response, Error.t) result
170170+171171+(** {1 Power Levels} *)
172172+173173+(** Room power levels. *)
174174+type power_levels = {
175175+ ban : int;
176176+ (** Level required to ban a user. *)
177177+ events : (string * int) list;
178178+ (** Event type to required power level mapping. *)
179179+ events_default : int;
180180+ (** Default level required to send events. *)
181181+ invite : int;
182182+ (** Level required to invite users. *)
183183+ kick : int;
184184+ (** Level required to kick users. *)
185185+ redact : int;
186186+ (** Level required to redact events. *)
187187+ state_default : int;
188188+ (** Default level required to send state events. *)
189189+ users : (string * int) list;
190190+ (** User ID to power level mapping. *)
191191+ users_default : int;
192192+ (** Default power level for users. *)
193193+ notifications : (string * int) list;
194194+ (** Notification power levels (e.g., "room" for @room). *)
195195+}
196196+197197+(** Get room power levels. *)
198198+val get_power_levels :
199199+ Client.t ->
200200+ room_id:Matrix_proto.Id.Room_id.t ->
201201+ (power_levels, Error.t) result
202202+203203+(** Set room power levels.
204204+205205+ Requires appropriate permissions. *)
206206+val set_power_levels :
207207+ Client.t ->
208208+ room_id:Matrix_proto.Id.Room_id.t ->
209209+ power_levels:power_levels ->
210210+ (unit, Error.t) result
211211+212212+(** Get a user's power level from power levels state.
213213+214214+ Returns users_default if user not in the users map. *)
215215+val get_user_power_level :
216216+ power_levels ->
217217+ Matrix_proto.Id.User_id.t ->
218218+ int
219219+220220+(** Set a specific user's power level.
221221+222222+ Fetches current power levels, modifies the user's level, and saves. *)
223223+val set_user_power_level :
224224+ Client.t ->
225225+ room_id:Matrix_proto.Id.Room_id.t ->
226226+ user_id:Matrix_proto.Id.User_id.t ->
227227+ level:int ->
228228+ (unit, Error.t) result
+466
lib/matrix_client/send_queue.ml
···11+(** Send queue for serialized message sending and offline support.
22+33+ This module provides:
44+ - Offline message queueing
55+ - Automatic retry with backoff
66+ - Transaction ID tracking for deduplication
77+ - Local echo support
88+ - Media upload coordination
99+1010+ Each room has its own queue to serialize sends, preventing race conditions
1111+ and ensuring messages are sent in order. *)
1212+1313+(** {1 Queue Request Types} *)
1414+1515+(** Type of queued request *)
1616+type request_kind =
1717+ | Event of {
1818+ event_type : string;
1919+ content : Jsont.json;
2020+ txn_id : string;
2121+ }
2222+ | MediaUpload of {
2323+ content_type : string;
2424+ data_size : int;
2525+ local_path : string option;
2626+ txn_id : string;
2727+ }
2828+ | Reaction of {
2929+ relates_to : Matrix_proto.Id.Event_id.t;
3030+ key : string;
3131+ txn_id : string;
3232+ }
3333+ | Redaction of {
3434+ event_id : Matrix_proto.Id.Event_id.t;
3535+ reason : string option;
3636+ txn_id : string;
3737+ }
3838+3939+(** Get transaction ID from request kind *)
4040+let txn_id_of_kind = function
4141+ | Event { txn_id; _ } -> txn_id
4242+ | MediaUpload { txn_id; _ } -> txn_id
4343+ | Reaction { txn_id; _ } -> txn_id
4444+ | Redaction { txn_id; _ } -> txn_id
4545+4646+(** Request state *)
4747+type request_state =
4848+ | Pending (** Waiting to be sent *)
4949+ | Sending (** Currently being sent *)
5050+ | Sent (** Successfully sent *)
5151+ | Failed of string (** Failed with error message *)
5252+ | Cancelled (** Cancelled by user *)
5353+5454+(** Queued request with metadata *)
5555+type queued_request = {
5656+ id : int;
5757+ room_id : Matrix_proto.Id.Room_id.t;
5858+ kind : request_kind;
5959+ mutable state : request_state;
6060+ created_at : int64;
6161+ mutable retry_count : int;
6262+ mutable last_error : string option;
6363+ (* For dependency tracking *)
6464+ mutable depends_on : int option; (** ID of parent request *)
6565+ mutable dependents : int list; (** IDs of dependent requests *)
6666+}
6767+6868+(** Result of sending a request *)
6969+type send_result =
7070+ | Sent_ok of { event_id : Matrix_proto.Id.Event_id.t option }
7171+ | Send_failed of { error : string; retryable : bool }
7272+ | Send_cancelled
7373+7474+(** {1 Send Handle} *)
7575+7676+(** Handle for a queued request, allowing cancellation and status checks *)
7777+type send_handle = {
7878+ request_id : int;
7979+ txn_id : string;
8080+ room_id : Matrix_proto.Id.Room_id.t;
8181+ queue : room_send_queue;
8282+}
8383+8484+(** Room-specific send queue *)
8585+and room_send_queue = {
8686+ room_id : Matrix_proto.Id.Room_id.t;
8787+ mutable requests : queued_request list;
8888+ mutable next_id : int;
8989+ mutable enabled : bool;
9090+ mutable is_processing : bool;
9191+ (* Configuration *)
9292+ max_retries : int;
9393+ retry_delay_ms : int;
9494+ (* Callbacks *)
9595+ mutable on_state_change : (queued_request -> unit) option;
9696+}
9797+9898+(** Global send queue manager *)
9999+type t = {
100100+ user_id : Matrix_proto.Id.User_id.t;
101101+ mutable room_queues : (string * room_send_queue) list;
102102+ mutable globally_enabled : bool;
103103+ mutable on_error : (queued_request -> string -> unit) option;
104104+}
105105+106106+(** {1 Queue Creation} *)
107107+108108+(** Create a new room send queue *)
109109+let create_room_queue ~room_id ?(max_retries = 3) ?(retry_delay_ms = 1000) () = {
110110+ room_id;
111111+ requests = [];
112112+ next_id = 0;
113113+ enabled = true;
114114+ is_processing = false;
115115+ max_retries;
116116+ retry_delay_ms;
117117+ on_state_change = None;
118118+}
119119+120120+(** Create a new global send queue manager *)
121121+let create ~user_id = {
122122+ user_id;
123123+ room_queues = [];
124124+ globally_enabled = true;
125125+ on_error = None;
126126+}
127127+128128+(** Get or create a room queue *)
129129+let get_room_queue t room_id =
130130+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
131131+ match List.assoc_opt room_id_str t.room_queues with
132132+ | Some queue -> queue
133133+ | None ->
134134+ let queue = create_room_queue ~room_id () in
135135+ t.room_queues <- (room_id_str, queue) :: t.room_queues;
136136+ queue
137137+138138+(** {1 Enqueueing Requests} *)
139139+140140+(** Generate a new transaction ID *)
141141+let generate_txn_id () =
142142+ let random_bytes = Mirage_crypto_rng.generate 16 in
143143+ "m" ^ (Base64.encode_string ~pad:false random_bytes)
144144+145145+(** Enqueue a request *)
146146+let enqueue (queue : room_send_queue) kind =
147147+ let id = queue.next_id in
148148+ queue.next_id <- queue.next_id + 1;
149149+ let request = {
150150+ id;
151151+ room_id = queue.room_id;
152152+ kind;
153153+ state = Pending;
154154+ created_at = Int64.of_float (Unix.gettimeofday () *. 1000.0);
155155+ retry_count = 0;
156156+ last_error = None;
157157+ depends_on = None;
158158+ dependents = [];
159159+ } in
160160+ queue.requests <- queue.requests @ [request];
161161+ let handle = {
162162+ request_id = id;
163163+ txn_id = txn_id_of_kind kind;
164164+ room_id = queue.room_id;
165165+ queue;
166166+ } in
167167+ (Option.iter (fun cb -> cb request) queue.on_state_change);
168168+ handle
169169+170170+(** Enqueue a message event *)
171171+let send_message t ~room_id ~event_type ~content =
172172+ let queue = get_room_queue t room_id in
173173+ let txn_id = generate_txn_id () in
174174+ enqueue queue (Event { event_type; content; txn_id })
175175+176176+(** Enqueue a text message *)
177177+let send_text t ~room_id ~body =
178178+ let content = Jsont.Object (
179179+ [(("msgtype", Jsont.Meta.none), Jsont.String ("m.text", Jsont.Meta.none));
180180+ (("body", Jsont.Meta.none), Jsont.String (body, Jsont.Meta.none))],
181181+ Jsont.Meta.none
182182+ ) in
183183+ send_message t ~room_id ~event_type:"m.room.message" ~content
184184+185185+(** Enqueue a reaction *)
186186+let send_reaction t ~room_id ~relates_to ~key =
187187+ let queue = get_room_queue t room_id in
188188+ let txn_id = generate_txn_id () in
189189+ enqueue queue (Reaction { relates_to; key; txn_id })
190190+191191+(** Enqueue a redaction *)
192192+let send_redaction t ~room_id ~event_id ?reason () =
193193+ let queue = get_room_queue t room_id in
194194+ let txn_id = generate_txn_id () in
195195+ enqueue queue (Redaction { event_id; reason; txn_id })
196196+197197+(** {1 Dependencies} *)
198198+199199+(** Add a dependency between requests *)
200200+let add_dependency ~parent:parent_handle ~child:child_handle =
201201+ let queue = parent_handle.queue in
202202+ match
203203+ List.find_opt (fun r -> r.id = parent_handle.request_id) queue.requests,
204204+ List.find_opt (fun r -> r.id = child_handle.request_id) queue.requests
205205+ with
206206+ | Some parent, Some child ->
207207+ child.depends_on <- Some parent.id;
208208+ parent.dependents <- child.id :: parent.dependents
209209+ | _ -> ()
210210+211211+(** Check if a request's dependencies are satisfied *)
212212+let dependencies_satisfied queue request =
213213+ match request.depends_on with
214214+ | None -> true
215215+ | Some parent_id ->
216216+ match List.find_opt (fun r -> r.id = parent_id) queue.requests with
217217+ | Some parent -> parent.state = Sent
218218+ | None -> true (* Parent removed, assume satisfied *)
219219+220220+(** {1 Request State Management} *)
221221+222222+(** Update request state *)
223223+let update_state queue request new_state =
224224+ request.state <- new_state;
225225+ Option.iter (fun cb -> cb request) queue.on_state_change
226226+227227+(** Cancel a queued request *)
228228+let cancel handle =
229229+ let queue = handle.queue in
230230+ match List.find_opt (fun r -> r.id = handle.request_id) queue.requests with
231231+ | Some request when request.state = Pending ->
232232+ update_state queue request Cancelled;
233233+ true
234234+ | _ -> false (* Can't cancel if already sending/sent *)
235235+236236+(** Abort a request (cancel and remove) *)
237237+let abort handle =
238238+ if cancel handle then begin
239239+ let queue = handle.queue in
240240+ queue.requests <- List.filter (fun r -> r.id <> handle.request_id) queue.requests;
241241+ true
242242+ end else
243243+ false
244244+245245+(** Get request by handle *)
246246+let get_request handle =
247247+ List.find_opt (fun r -> r.id = handle.request_id) handle.queue.requests
248248+249249+(** Check if request is still pending *)
250250+let is_pending handle =
251251+ match get_request handle with
252252+ | Some r -> r.state = Pending
253253+ | None -> false
254254+255255+(** Check if request was sent *)
256256+let is_sent handle =
257257+ match get_request handle with
258258+ | Some r -> r.state = Sent
259259+ | None -> false
260260+261261+(** {1 Queue Processing} *)
262262+263263+(** Get next sendable request from queue *)
264264+let next_sendable queue =
265265+ if not queue.enabled then None
266266+ else
267267+ List.find_opt (fun r ->
268268+ r.state = Pending &&
269269+ dependencies_satisfied queue r &&
270270+ r.retry_count < queue.max_retries
271271+ ) queue.requests
272272+273273+(** Mark request as being sent *)
274274+let mark_sending queue request =
275275+ update_state queue request Sending
276276+277277+(** Mark request as successfully sent *)
278278+let mark_sent queue request =
279279+ update_state queue request Sent
280280+281281+(** Mark request as failed with optional retry *)
282282+let mark_failed queue request error ~retryable =
283283+ request.retry_count <- request.retry_count + 1;
284284+ request.last_error <- Some error;
285285+ if retryable && request.retry_count < queue.max_retries then
286286+ update_state queue request Pending (* Will retry *)
287287+ else
288288+ update_state queue request (Failed error)
289289+290290+(** Remove completed/cancelled/failed requests *)
291291+let cleanup_queue queue =
292292+ queue.requests <- List.filter (fun r ->
293293+ match r.state with
294294+ | Sent | Cancelled | Failed _ -> false
295295+ | Pending | Sending -> true
296296+ ) queue.requests
297297+298298+(** {1 Queue Statistics} *)
299299+300300+(** Count of pending requests in a room queue *)
301301+let pending_count queue =
302302+ List.length (List.filter (fun r -> r.state = Pending) queue.requests)
303303+304304+(** Count of all pending requests across all rooms *)
305305+let total_pending t =
306306+ List.fold_left (fun acc (_, queue) ->
307307+ acc + pending_count queue
308308+ ) 0 t.room_queues
309309+310310+(** Get all pending requests for a room *)
311311+let pending_requests queue =
312312+ List.filter (fun r -> r.state = Pending) queue.requests
313313+314314+(** Get all failed requests for a room *)
315315+let failed_requests queue =
316316+ List.filter (fun r -> match r.state with Failed _ -> true | _ -> false) queue.requests
317317+318318+(** {1 Queue Control} *)
319319+320320+(** Enable/disable a room queue *)
321321+let set_room_enabled queue enabled =
322322+ queue.enabled <- enabled
323323+324324+(** Enable/disable all queues globally *)
325325+let set_enabled t enabled =
326326+ t.globally_enabled <- enabled;
327327+ List.iter (fun (_, queue) ->
328328+ queue.enabled <- enabled
329329+ ) t.room_queues
330330+331331+(** Check if globally enabled *)
332332+let is_enabled t = t.globally_enabled
333333+334334+(** Check if a room queue is enabled *)
335335+let is_room_enabled queue = queue.enabled
336336+337337+(** {1 Event Callbacks} *)
338338+339339+(** Set callback for state changes *)
340340+let on_state_change queue callback =
341341+ queue.on_state_change <- Some callback
342342+343343+(** Set global error callback *)
344344+let on_error t callback =
345345+ t.on_error <- Some callback
346346+347347+(** {1 Persistence} *)
348348+349349+(** Serializable queue state *)
350350+type persisted_request = {
351351+ p_room_id : string;
352352+ p_kind : request_kind;
353353+ p_created_at : int64;
354354+ p_retry_count : int;
355355+ p_depends_on : int option;
356356+}
357357+358358+(** Convert request to persistable form *)
359359+let request_to_persisted (request : queued_request) = {
360360+ p_room_id = Matrix_proto.Id.Room_id.to_string request.room_id;
361361+ p_kind = request.kind;
362362+ p_created_at = request.created_at;
363363+ p_retry_count = request.retry_count;
364364+ p_depends_on = request.depends_on;
365365+}
366366+367367+(** Get all pending requests for persistence *)
368368+let requests_to_persist t =
369369+ List.concat_map (fun (_, queue) ->
370370+ pending_requests queue |> List.map request_to_persisted
371371+ ) t.room_queues
372372+373373+(** Restore requests from persistence *)
374374+let restore_requests t persisted_requests =
375375+ List.iter (fun p ->
376376+ match Matrix_proto.Id.Room_id.of_string p.p_room_id with
377377+ | Error _ -> () (* Skip invalid room IDs *)
378378+ | Ok room_id ->
379379+ let queue = get_room_queue t room_id in
380380+ let id = queue.next_id in
381381+ queue.next_id <- queue.next_id + 1;
382382+ let request = {
383383+ id;
384384+ room_id;
385385+ kind = p.p_kind;
386386+ state = Pending;
387387+ created_at = p.p_created_at;
388388+ retry_count = p.p_retry_count;
389389+ last_error = None;
390390+ depends_on = p.p_depends_on;
391391+ dependents = [];
392392+ } in
393393+ queue.requests <- queue.requests @ [request]
394394+ ) persisted_requests
395395+396396+(** {1 Local Echo} *)
397397+398398+(** Create a local echo event from a queued request *)
399399+let local_echo_event request =
400400+ match request.kind with
401401+ | Event { event_type; content; txn_id } ->
402402+ Some (event_type, content, txn_id)
403403+ | Reaction { relates_to; key; txn_id } ->
404404+ let event_id = Matrix_proto.Id.Event_id.to_string relates_to in
405405+ let content = Jsont.Object (
406406+ [(("m.relates_to", Jsont.Meta.none),
407407+ Jsont.Object (
408408+ [(("rel_type", Jsont.Meta.none), Jsont.String ("m.annotation", Jsont.Meta.none));
409409+ (("event_id", Jsont.Meta.none), Jsont.String (event_id, Jsont.Meta.none));
410410+ (("key", Jsont.Meta.none), Jsont.String (key, Jsont.Meta.none))],
411411+ Jsont.Meta.none))],
412412+ Jsont.Meta.none
413413+ ) in
414414+ Some ("m.reaction", content, txn_id)
415415+ | MediaUpload _ -> None
416416+ | Redaction _ -> None
417417+418418+(** Check if an event_id matches a transaction ID (for local echo replacement) *)
419419+let matches_txn_id request ~event_id =
420420+ (* The event_id might contain the txn_id for local echoes *)
421421+ let txn = txn_id_of_kind request.kind in
422422+ String.equal (Matrix_proto.Id.Event_id.to_string event_id) ("$" ^ txn)
423423+424424+(** {1 Retry Logic} *)
425425+426426+(** Calculate delay for next retry (exponential backoff) *)
427427+let retry_delay queue request =
428428+ let base_delay = queue.retry_delay_ms in
429429+ let multiplier = 1 lsl request.retry_count in (* 2^retry_count *)
430430+ min (base_delay * multiplier) 60000 (* Cap at 60 seconds *)
431431+432432+(** Check if a request should be retried *)
433433+let should_retry queue request =
434434+ request.retry_count < queue.max_retries &&
435435+ match request.state with
436436+ | Failed _ -> false (* Already marked as terminal failure *)
437437+ | Pending -> true (* Will be retried *)
438438+ | _ -> false
439439+440440+(** {1 Media Upload Support} *)
441441+442442+(** Create a media upload request with dependent event send *)
443443+let send_media t ~room_id ~content_type ~data_size ?local_path ~event_content () =
444444+ let queue = get_room_queue t room_id in
445445+446446+ (* First, create the upload request *)
447447+ let upload_txn_id = generate_txn_id () in
448448+ let upload_handle = enqueue queue (MediaUpload {
449449+ content_type;
450450+ data_size;
451451+ local_path;
452452+ txn_id = upload_txn_id;
453453+ }) in
454454+455455+ (* Then create the event request that depends on it *)
456456+ let event_txn_id = generate_txn_id () in
457457+ let event_handle = enqueue queue (Event {
458458+ event_type = "m.room.message";
459459+ content = event_content;
460460+ txn_id = event_txn_id;
461461+ }) in
462462+463463+ (* Set up dependency *)
464464+ add_dependency ~parent:upload_handle ~child:event_handle;
465465+466466+ (upload_handle, event_handle)
···11+(** Space operations for Matrix spaces (MSC1772).
22+33+ Spaces are special rooms with type "m.space" that contain child rooms
44+ via m.space.child state events. *)
55+66+(** Space hierarchy information *)
77+type space_room = {
88+ room_id : Matrix_proto.Id.Room_id.t;
99+ name : string option;
1010+ topic : string option;
1111+ canonical_alias : Matrix_proto.Id.Room_alias.t option;
1212+ avatar_url : string option;
1313+ num_joined_members : int;
1414+ room_type : string option;
1515+ join_rule : Matrix_proto.Event.Join_rule.t option;
1616+ children_state : child_state list;
1717+ world_readable : bool;
1818+ guest_can_join : bool;
1919+}
2020+2121+and child_state = {
2222+ state_key : string;
2323+ via : string list;
2424+ order : string option;
2525+ suggested : bool;
2626+}
2727+2828+let child_state_jsont =
2929+ Jsont.Object.(
3030+ map (fun state_key via order suggested ->
3131+ { state_key; via; order; suggested })
3232+ |> mem "state_key" Jsont.string ~enc:(fun t -> t.state_key)
3333+ |> mem "via" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.via)
3434+ |> opt_mem "order" Jsont.string ~enc:(fun t -> t.order)
3535+ |> mem "suggested" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.suggested)
3636+ |> finish)
3737+3838+let space_room_jsont =
3939+ Jsont.Object.(
4040+ map (fun room_id name topic canonical_alias avatar_url num_joined_members
4141+ room_type join_rule children_state world_readable guest_can_join ->
4242+ { room_id; name; topic; canonical_alias; avatar_url; num_joined_members;
4343+ room_type; join_rule; children_state; world_readable; guest_can_join })
4444+ |> mem "room_id" Matrix_proto.Id.Room_id.jsont ~enc:(fun t -> t.room_id)
4545+ |> opt_mem "name" Jsont.string ~enc:(fun t -> t.name)
4646+ |> opt_mem "topic" Jsont.string ~enc:(fun t -> t.topic)
4747+ |> opt_mem "canonical_alias" Matrix_proto.Id.Room_alias.jsont ~enc:(fun t -> t.canonical_alias)
4848+ |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url)
4949+ |> mem "num_joined_members" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.num_joined_members)
5050+ |> opt_mem "room_type" Jsont.string ~enc:(fun t -> t.room_type)
5151+ |> opt_mem "join_rule" Matrix_proto.Event.Join_rule.jsont ~enc:(fun t -> t.join_rule)
5252+ |> mem "children_state" (Jsont.list child_state_jsont) ~dec_absent:[]
5353+ ~enc:(fun t -> t.children_state)
5454+ |> mem "world_readable" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.world_readable)
5555+ |> mem "guest_can_join" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.guest_can_join)
5656+ |> finish)
5757+5858+(** Response from GET /_matrix/client/v1/rooms/{roomId}/hierarchy *)
5959+type hierarchy_response = {
6060+ rooms : space_room list;
6161+ next_batch : string option;
6262+}
6363+6464+let hierarchy_response_jsont =
6565+ Jsont.Object.(
6666+ map (fun rooms next_batch -> { rooms; next_batch })
6767+ |> mem "rooms" (Jsont.list space_room_jsont) ~dec_absent:[] ~enc:(fun t -> t.rooms)
6868+ |> opt_mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch)
6969+ |> finish)
7070+7171+(** Get the hierarchy of a space.
7272+7373+ @param room_id The space room ID
7474+ @param suggested_only If true, only return suggested rooms
7575+ @param limit Maximum number of rooms to return per request
7676+ @param max_depth Maximum depth to recurse into the hierarchy
7777+ @param from Pagination token *)
7878+let get_hierarchy client ~room_id ?suggested_only ?limit ?max_depth ?from () =
7979+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
8080+ let path = Printf.sprintf "/rooms/%s/hierarchy" (Uri.pct_encode room_id_str) in
8181+ let query =
8282+ []
8383+ |> (fun q -> match suggested_only with
8484+ | Some true -> ("suggested_only", "true") :: q
8585+ | _ -> q)
8686+ |> (fun q -> match limit with
8787+ | Some l -> ("limit", string_of_int l) :: q
8888+ | None -> q)
8989+ |> (fun q -> match max_depth with
9090+ | Some d -> ("max_depth", string_of_int d) :: q
9191+ | None -> q)
9292+ |> (fun q -> match from with
9393+ | Some f -> ("from", f) :: q
9494+ | None -> q)
9595+ in
9696+ let query = if query = [] then None else Some query in
9797+ match Client.get client ~path ?query () with
9898+ | Error e -> Error e
9999+ | Ok body -> Client.decode_response hierarchy_response_jsont body
100100+101101+(** Add a child room to a space.
102102+103103+ @param space_id The parent space room ID
104104+ @param child_id The child room ID
105105+ @param via Server names to route through
106106+ @param order Optional ordering string
107107+ @param suggested Whether the room is suggested *)
108108+(* Response type for state setting *)
109109+type set_state_response = {
110110+ event_id : Matrix_proto.Id.Event_id.t;
111111+}
112112+113113+let set_state_response_jsont =
114114+ Jsont.Object.(
115115+ map (fun event_id -> { event_id })
116116+ |> mem "event_id" Matrix_proto.Id.Event_id.jsont
117117+ |> finish)
118118+119119+let add_child client ~space_id ~child_id ?(via = []) ?order ?(suggested = false) () =
120120+ let space_id_str = Matrix_proto.Id.Room_id.to_string space_id in
121121+ let child_id_str = Matrix_proto.Id.Room_id.to_string child_id in
122122+ let path = Printf.sprintf "/rooms/%s/state/m.space.child/%s"
123123+ (Uri.pct_encode space_id_str)
124124+ (Uri.pct_encode child_id_str)
125125+ in
126126+ let content : Matrix_proto.Event.Space_child_content.t = {
127127+ via = if via = [] then None else Some via;
128128+ order;
129129+ suggested = if suggested then Some true else None;
130130+ } in
131131+ match Client.encode_body Matrix_proto.Event.Space_child_content.jsont content with
132132+ | Error e -> Error e
133133+ | Ok body ->
134134+ match Client.put client ~path ~body () with
135135+ | Error e -> Error e
136136+ | Ok resp_body ->
137137+ match Client.decode_response set_state_response_jsont resp_body with
138138+ | Error e -> Error e
139139+ | Ok resp -> Ok resp.event_id
140140+141141+(** Remove a child room from a space. *)
142142+let remove_child client ~space_id ~child_id =
143143+ let space_id_str = Matrix_proto.Id.Room_id.to_string space_id in
144144+ let child_id_str = Matrix_proto.Id.Room_id.to_string child_id in
145145+ let path = Printf.sprintf "/rooms/%s/state/m.space.child/%s"
146146+ (Uri.pct_encode space_id_str)
147147+ (Uri.pct_encode child_id_str)
148148+ in
149149+ (* Send empty content to remove the child *)
150150+ match Client.put client ~path ~body:"{}" () with
151151+ | Error e -> Error e
152152+ | Ok _ -> Ok ()
153153+154154+(** Set the parent space for a room.
155155+156156+ @param room_id The child room ID
157157+ @param parent_id The parent space ID
158158+ @param via Server names to route through
159159+ @param canonical Whether this is the canonical parent *)
160160+let set_parent client ~room_id ~parent_id ?(via = []) ?(canonical = false) () =
161161+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
162162+ let parent_id_str = Matrix_proto.Id.Room_id.to_string parent_id in
163163+ let path = Printf.sprintf "/rooms/%s/state/m.space.parent/%s"
164164+ (Uri.pct_encode room_id_str)
165165+ (Uri.pct_encode parent_id_str)
166166+ in
167167+ let content : Matrix_proto.Event.Space_parent_content.t = {
168168+ via = if via = [] then None else Some via;
169169+ canonical = if canonical then Some true else None;
170170+ } in
171171+ match Client.encode_body Matrix_proto.Event.Space_parent_content.jsont content with
172172+ | Error e -> Error e
173173+ | Ok body ->
174174+ match Client.put client ~path ~body () with
175175+ | Error e -> Error e
176176+ | Ok resp_body ->
177177+ match Client.decode_response set_state_response_jsont resp_body with
178178+ | Error e -> Error e
179179+ | Ok resp -> Ok resp.event_id
180180+181181+(** Remove a parent space from a room. *)
182182+let remove_parent client ~room_id ~parent_id =
183183+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
184184+ let parent_id_str = Matrix_proto.Id.Room_id.to_string parent_id in
185185+ let path = Printf.sprintf "/rooms/%s/state/m.space.parent/%s"
186186+ (Uri.pct_encode room_id_str)
187187+ (Uri.pct_encode parent_id_str)
188188+ in
189189+ match Client.put client ~path ~body:"{}" () with
190190+ | Error e -> Error e
191191+ | Ok _ -> Ok ()
192192+193193+(** Check if a room is a space. *)
194194+let is_space room_type =
195195+ match room_type with
196196+ | Some "m.space" -> true
197197+ | _ -> false
198198+199199+(** Create a new space.
200200+201201+ @param name The space name
202202+ @param topic Optional topic
203203+ @param visibility Room visibility (public or private)
204204+ @param invite List of users to invite *)
205205+let create_space client ~name ?topic ?(visibility = `Private) ?(invite = []) () =
206206+ Rooms.create_room client
207207+ ~name
208208+ ?topic
209209+ ~visibility
210210+ ~invite
211211+ ~room_type:"m.space"
212212+ ()
+139
lib/matrix_client/state.ml
···11+(** Room state operations. *)
22+33+(* Get all state *)
44+let get_state client ~room_id =
55+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
66+ let path = Printf.sprintf "/rooms/%s/state" (Uri.pct_encode room_id_str) in
77+ match Client.get client ~path () with
88+ | Error e -> Error e
99+ | Ok body -> Client.decode_response (Jsont.list Matrix_proto.Event.Raw_event.jsont) body
1010+1111+(* Get specific state event *)
1212+let get_state_event client ~room_id ~event_type ?(state_key = "") () =
1313+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
1414+ let path = Printf.sprintf "/rooms/%s/state/%s/%s"
1515+ (Uri.pct_encode room_id_str)
1616+ (Uri.pct_encode event_type)
1717+ (Uri.pct_encode state_key)
1818+ in
1919+ match Client.get client ~path () with
2020+ | Error e -> Error e
2121+ | Ok body -> Client.decode_response Jsont.json body
2222+2323+(* Set state event *)
2424+type set_state_response = {
2525+ event_id : Matrix_proto.Id.Event_id.t;
2626+}
2727+2828+let set_state_response_jsont =
2929+ Jsont.Object.(
3030+ map (fun event_id -> { event_id })
3131+ |> mem "event_id" Matrix_proto.Id.Event_id.jsont
3232+ |> finish)
3333+3434+let set_state client ~room_id ~event_type ?(state_key = "") ~content () =
3535+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
3636+ let path = Printf.sprintf "/rooms/%s/state/%s/%s"
3737+ (Uri.pct_encode room_id_str)
3838+ (Uri.pct_encode event_type)
3939+ (Uri.pct_encode state_key)
4040+ in
4141+ match Client.encode_body Jsont.json content with
4242+ | Error e -> Error e
4343+ | Ok body ->
4444+ match Client.put client ~path ~body () with
4545+ | Error e -> Error e
4646+ | Ok body ->
4747+ match Client.decode_response set_state_response_jsont body with
4848+ | Error e -> Error e
4949+ | Ok resp -> Ok resp.event_id
5050+5151+(* Convenience: room name *)
5252+type name_content = {
5353+ name : string;
5454+} [@@warning "-69"]
5555+5656+let name_content_jsont =
5757+ Jsont.Object.(
5858+ map (fun name -> { name })
5959+ |> mem "name" Jsont.string
6060+ |> finish)
6161+6262+let get_name client ~room_id =
6363+ match get_state_event client ~room_id ~event_type:"m.room.name" () with
6464+ | Error (Error.Matrix_error { errcode = Error.M_NOT_FOUND; _ }) -> Ok None
6565+ | Error e -> Error e
6666+ | Ok json ->
6767+ match Jsont_bytesrw.decode_string name_content_jsont
6868+ (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with
6969+ | Ok c -> Ok (Some c.name)
7070+ | Error _ -> Ok None
7171+7272+let set_name client ~room_id ~name =
7373+ let content = { name } in
7474+ match Client.encode_body name_content_jsont content with
7575+ | Error e -> Error e
7676+ | Ok body ->
7777+ match Client.decode_response Jsont.json body with
7878+ | Error e -> Error e
7979+ | Ok json -> set_state client ~room_id ~event_type:"m.room.name" ~content:json ()
8080+8181+(* Convenience: room topic *)
8282+type topic_content = {
8383+ topic : string;
8484+} [@@warning "-69"]
8585+8686+let topic_content_jsont =
8787+ Jsont.Object.(
8888+ map (fun topic -> { topic })
8989+ |> mem "topic" Jsont.string
9090+ |> finish)
9191+9292+let get_topic client ~room_id =
9393+ match get_state_event client ~room_id ~event_type:"m.room.topic" () with
9494+ | Error (Error.Matrix_error { errcode = Error.M_NOT_FOUND; _ }) -> Ok None
9595+ | Error e -> Error e
9696+ | Ok json ->
9797+ match Jsont_bytesrw.decode_string topic_content_jsont
9898+ (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with
9999+ | Ok c -> Ok (Some c.topic)
100100+ | Error _ -> Ok None
101101+102102+let set_topic client ~room_id ~topic =
103103+ let content = { topic } in
104104+ match Client.encode_body topic_content_jsont content with
105105+ | Error e -> Error e
106106+ | Ok body ->
107107+ match Client.decode_response Jsont.json body with
108108+ | Error e -> Error e
109109+ | Ok json -> set_state client ~room_id ~event_type:"m.room.topic" ~content:json ()
110110+111111+(* Convenience: room avatar *)
112112+type avatar_content = {
113113+ url : string;
114114+} [@@warning "-69"]
115115+116116+let avatar_content_jsont =
117117+ Jsont.Object.(
118118+ map (fun url -> { url })
119119+ |> mem "url" Jsont.string
120120+ |> finish)
121121+122122+let get_avatar client ~room_id =
123123+ match get_state_event client ~room_id ~event_type:"m.room.avatar" () with
124124+ | Error (Error.Matrix_error { errcode = Error.M_NOT_FOUND; _ }) -> Ok None
125125+ | Error e -> Error e
126126+ | Ok json ->
127127+ match Jsont_bytesrw.decode_string avatar_content_jsont
128128+ (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with
129129+ | Ok c -> Ok (Some c.url)
130130+ | Error _ -> Ok None
131131+132132+let set_avatar client ~room_id ~url =
133133+ let content = { url } in
134134+ match Client.encode_body avatar_content_jsont content with
135135+ | Error e -> Error e
136136+ | Ok body ->
137137+ match Client.decode_response Jsont.json body with
138138+ | Error e -> Error e
139139+ | Ok json -> set_state client ~room_id ~event_type:"m.room.avatar" ~content:json ()
+71
lib/matrix_client/state.mli
···11+(** Room state operations. *)
22+33+(** Get all current state events for a room. *)
44+val get_state :
55+ Client.t ->
66+ room_id:Matrix_proto.Id.Room_id.t ->
77+ (Matrix_proto.Event.Raw_event.t list, Error.t) result
88+99+(** Get a specific state event.
1010+1111+ @param state_key The state key (empty string for events without a key). *)
1212+val get_state_event :
1313+ Client.t ->
1414+ room_id:Matrix_proto.Id.Room_id.t ->
1515+ event_type:string ->
1616+ ?state_key:string ->
1717+ unit ->
1818+ (Jsont.json, Error.t) result
1919+2020+(** Set a state event.
2121+2222+ @param state_key The state key (empty string for events without a key). *)
2323+val set_state :
2424+ Client.t ->
2525+ room_id:Matrix_proto.Id.Room_id.t ->
2626+ event_type:string ->
2727+ ?state_key:string ->
2828+ content:Jsont.json ->
2929+ unit ->
3030+ (Matrix_proto.Id.Event_id.t, Error.t) result
3131+3232+(** {1 Convenience Functions} *)
3333+3434+(** Get room name. *)
3535+val get_name :
3636+ Client.t ->
3737+ room_id:Matrix_proto.Id.Room_id.t ->
3838+ (string option, Error.t) result
3939+4040+(** Set room name. *)
4141+val set_name :
4242+ Client.t ->
4343+ room_id:Matrix_proto.Id.Room_id.t ->
4444+ name:string ->
4545+ (Matrix_proto.Id.Event_id.t, Error.t) result
4646+4747+(** Get room topic. *)
4848+val get_topic :
4949+ Client.t ->
5050+ room_id:Matrix_proto.Id.Room_id.t ->
5151+ (string option, Error.t) result
5252+5353+(** Set room topic. *)
5454+val set_topic :
5555+ Client.t ->
5656+ room_id:Matrix_proto.Id.Room_id.t ->
5757+ topic:string ->
5858+ (Matrix_proto.Id.Event_id.t, Error.t) result
5959+6060+(** Get room avatar URL. *)
6161+val get_avatar :
6262+ Client.t ->
6363+ room_id:Matrix_proto.Id.Room_id.t ->
6464+ (string option, Error.t) result
6565+6666+(** Set room avatar URL. *)
6767+val set_avatar :
6868+ Client.t ->
6969+ room_id:Matrix_proto.Id.Room_id.t ->
7070+ url:string ->
7171+ (Matrix_proto.Id.Event_id.t, Error.t) result
+1001
lib/matrix_client/store.ml
···11+(** Persistent storage layer for Matrix SDK state.
22+33+ This module provides pluggable storage interfaces matching the Rust SDK's
44+ storage architecture. Implementations can be provided for:
55+ - In-memory (default, non-persistent)
66+ - SQLite (persistent file-based)
77+ - Custom backends
88+99+ Storage is split into separate stores:
1010+ - STATE_STORE: Room state, user profiles, sync tokens
1111+ - CRYPTO_STORE: E2EE keys, sessions, device info
1212+ - EVENT_CACHE_STORE: Event history and pagination *)
1313+1414+(** {1 Key-Value Data Types} *)
1515+1616+(** Types of data that can be stored in the state store's KV section *)
1717+type kv_data =
1818+ | Sync_token of string
1919+ | Filter of { filter_id : string; filter : Jsont.json }
2020+ | User_avatar_url of { user_id : Matrix_proto.Id.User_id.t; url : string option }
2121+ | Recently_visited_rooms of Matrix_proto.Id.Room_id.t list
2222+ | Composer_draft of {
2323+ room_id : Matrix_proto.Id.Room_id.t;
2424+ plain_text : string;
2525+ html_text : string option;
2626+ draft_type : [ `New | `Edit of Matrix_proto.Id.Event_id.t | `Reply of Matrix_proto.Id.Event_id.t ];
2727+ }
2828+2929+(** {1 State Changes Aggregate} *)
3030+3131+(** Aggregated state changes from a sync response *)
3232+type state_changes = {
3333+ sync_token : string option;
3434+ account_data : (string * Jsont.json) list; (* event_type -> content *)
3535+ presence : (string * Jsont.json) list; (* user_id -> presence event *)
3636+ room_infos : (string * room_info) list; (* room_id -> room info *)
3737+ room_state : (string * (string * (string * Jsont.json) list) list) list;
3838+ (* room_id -> event_type -> state_key -> event *)
3939+ room_account_data : (string * (string * Jsont.json) list) list;
4040+ (* room_id -> event_type -> content *)
4141+ stripped_state : (string * (string * (string * Jsont.json) list) list) list;
4242+ (* For invited rooms *)
4343+ receipts : (string * Jsont.json) list; (* room_id -> receipt content *)
4444+ profiles : (string * (string * member_profile) list) list;
4545+ (* room_id -> user_id -> profile *)
4646+}
4747+4848+and room_info = {
4949+ room_id : Matrix_proto.Id.Room_id.t;
5050+ room_type : string option;
5151+ name : string option;
5252+ topic : string option;
5353+ avatar_url : string option;
5454+ canonical_alias : Matrix_proto.Id.Room_alias.t option;
5555+ joined_member_count : int;
5656+ invited_member_count : int;
5757+ is_encrypted : bool;
5858+ is_direct : bool;
5959+ prev_batch : string option;
6060+ notification_count : int;
6161+ highlight_count : int;
6262+}
6363+6464+and member_profile = {
6565+ display_name : string option;
6666+ avatar_url : string option;
6767+}
6868+6969+let empty_state_changes = {
7070+ sync_token = None;
7171+ account_data = [];
7272+ presence = [];
7373+ room_infos = [];
7474+ room_state = [];
7575+ room_account_data = [];
7676+ stripped_state = [];
7777+ receipts = [];
7878+ profiles = [];
7979+}
8080+8181+(** {1 STATE_STORE Module Type} *)
8282+8383+(** Module type for room state storage *)
8484+module type STATE_STORE = sig
8585+ type t
8686+ type error
8787+8888+ (** Create a new store *)
8989+ val create : unit -> t
9090+9191+ (** {2 State Changes} *)
9292+9393+ (** Save aggregated state changes from sync *)
9494+ val save_changes : t -> state_changes -> (unit, error) result
9595+9696+ (** {2 Sync Token} *)
9797+9898+ val get_sync_token : t -> string option
9999+ val set_sync_token : t -> string -> unit
100100+101101+ (** {2 Room State} *)
102102+103103+ (** Get a specific state event *)
104104+ val get_state_event :
105105+ t ->
106106+ room_id:Matrix_proto.Id.Room_id.t ->
107107+ event_type:string ->
108108+ state_key:string ->
109109+ Jsont.json option
110110+111111+ (** Get all state events of a type for a room *)
112112+ val get_state_events :
113113+ t ->
114114+ room_id:Matrix_proto.Id.Room_id.t ->
115115+ event_type:string ->
116116+ (string * Jsont.json) list (* state_key -> event *)
117117+118118+ (** Get room info *)
119119+ val get_room_info : t -> Matrix_proto.Id.Room_id.t -> room_info option
120120+121121+ (** Get all room IDs *)
122122+ val get_room_ids : t -> Matrix_proto.Id.Room_id.t list
123123+124124+ (** {2 User Profiles} *)
125125+126126+ (** Get user profile in a room *)
127127+ val get_profile :
128128+ t ->
129129+ room_id:Matrix_proto.Id.Room_id.t ->
130130+ user_id:Matrix_proto.Id.User_id.t ->
131131+ member_profile option
132132+133133+ (** {2 Account Data} *)
134134+135135+ val get_account_data : t -> event_type:string -> Jsont.json option
136136+ val get_room_account_data :
137137+ t -> room_id:Matrix_proto.Id.Room_id.t -> event_type:string -> Jsont.json option
138138+139139+ (** {2 Receipts} *)
140140+141141+ val get_receipts : t -> room_id:Matrix_proto.Id.Room_id.t -> Jsont.json option
142142+143143+ (** {2 Presence} *)
144144+145145+ val get_presence : t -> user_id:Matrix_proto.Id.User_id.t -> Jsont.json option
146146+147147+ (** {2 Key-Value Store} *)
148148+149149+ val get_kv : t -> string -> kv_data option
150150+ val set_kv : t -> string -> kv_data -> unit
151151+ val remove_kv : t -> string -> unit
152152+153153+ (** {2 Cleanup} *)
154154+155155+ val clear : t -> unit
156156+end
157157+158158+(** {1 Crypto Changes Aggregate} *)
159159+160160+(** Device key information *)
161161+type device_keys = {
162162+ user_id : Matrix_proto.Id.User_id.t;
163163+ device_id : Matrix_proto.Id.Device_id.t;
164164+ algorithms : string list;
165165+ keys : (string * string) list; (* key_id -> key *)
166166+ signatures : (string * (string * string) list) list; (* user_id -> key_id -> sig *)
167167+ unsigned : Jsont.json option;
168168+}
169169+170170+(** Cross-signing identity keys *)
171171+type cross_signing_keys = {
172172+ master_key : (string * string) list option;
173173+ self_signing_key : (string * string) list option;
174174+ user_signing_key : (string * string) list option;
175175+}
176176+177177+(** Olm session data *)
178178+type olm_session = {
179179+ session_id : string;
180180+ sender_key : string;
181181+ pickle : string; (* Serialized session state *)
182182+ created_at : int64;
183183+ last_used_at : int64;
184184+}
185185+186186+(** Megolm inbound group session *)
187187+type inbound_group_session = {
188188+ session_id : string;
189189+ room_id : Matrix_proto.Id.Room_id.t;
190190+ sender_key : string;
191191+ signing_key : string option;
192192+ pickle : string;
193193+ imported : bool;
194194+ backed_up : bool;
195195+ history_visibility : string option;
196196+ algorithm : string;
197197+}
198198+199199+(** Megolm outbound group session *)
200200+type outbound_group_session = {
201201+ session_id : string;
202202+ room_id : Matrix_proto.Id.Room_id.t;
203203+ pickle : string;
204204+ creation_time : int64;
205205+ message_count : int;
206206+ shared_with : (string * (string * int) list) list; (* user_id -> device_id -> index *)
207207+}
208208+209209+(** Tracked user for key updates *)
210210+type tracked_user = {
211211+ user_id : Matrix_proto.Id.User_id.t;
212212+ dirty : bool;
213213+}
214214+215215+(** Aggregated crypto changes *)
216216+type crypto_changes = {
217217+ account_pickle : string option;
218218+ private_identity : cross_signing_keys option;
219219+ olm_sessions : olm_session list;
220220+ inbound_group_sessions : inbound_group_session list;
221221+ outbound_group_sessions : outbound_group_session list;
222222+ devices_new : device_keys list;
223223+ devices_changed : device_keys list;
224224+ devices_deleted : (Matrix_proto.Id.User_id.t * Matrix_proto.Id.Device_id.t) list;
225225+ tracked_users : tracked_user list;
226226+ message_hashes : (string * string) list; (* hash -> event_id *)
227227+ backup_decryption_key : string option;
228228+}
229229+230230+let empty_crypto_changes = {
231231+ account_pickle = None;
232232+ private_identity = None;
233233+ olm_sessions = [];
234234+ inbound_group_sessions = [];
235235+ outbound_group_sessions = [];
236236+ devices_new = [];
237237+ devices_changed = [];
238238+ devices_deleted = [];
239239+ tracked_users = [];
240240+ message_hashes = [];
241241+ backup_decryption_key = None;
242242+}
243243+244244+(** {1 CRYPTO_STORE Module Type} *)
245245+246246+(** Module type for E2EE key storage *)
247247+module type CRYPTO_STORE = sig
248248+ type t
249249+ type error
250250+251251+ (** Create a new store *)
252252+ val create : unit -> t
253253+254254+ (** {2 Account} *)
255255+256256+ (** Load the pickled Olm account *)
257257+ val load_account : t -> string option
258258+259259+ (** Save the pickled Olm account *)
260260+ val save_account : t -> string -> unit
261261+262262+ (** {2 Batch Changes} *)
263263+264264+ val save_changes : t -> crypto_changes -> (unit, error) result
265265+266266+ (** {2 Olm Sessions} *)
267267+268268+ (** Get Olm sessions for a sender key *)
269269+ val get_sessions : t -> sender_key:string -> olm_session list
270270+271271+ (** Get a specific Olm session *)
272272+ val get_session : t -> session_id:string -> olm_session option
273273+274274+ (** {2 Megolm Sessions} *)
275275+276276+ (** Get an inbound group session *)
277277+ val get_inbound_group_session :
278278+ t ->
279279+ room_id:Matrix_proto.Id.Room_id.t ->
280280+ session_id:string ->
281281+ inbound_group_session option
282282+283283+ (** Get all inbound group sessions for a room *)
284284+ val get_inbound_group_sessions_for_room :
285285+ t -> room_id:Matrix_proto.Id.Room_id.t -> inbound_group_session list
286286+287287+ (** Get sessions needing backup *)
288288+ val get_sessions_for_backup : t -> limit:int -> inbound_group_session list
289289+290290+ (** Mark sessions as backed up *)
291291+ val mark_sessions_backed_up : t -> session_ids:string list -> unit
292292+293293+ (** Get outbound group session for a room *)
294294+ val get_outbound_group_session :
295295+ t -> room_id:Matrix_proto.Id.Room_id.t -> outbound_group_session option
296296+297297+ (** {2 Device Keys} *)
298298+299299+ (** Get a specific device's keys *)
300300+ val get_device :
301301+ t ->
302302+ user_id:Matrix_proto.Id.User_id.t ->
303303+ device_id:Matrix_proto.Id.Device_id.t ->
304304+ device_keys option
305305+306306+ (** Get all devices for a user *)
307307+ val get_user_devices : t -> user_id:Matrix_proto.Id.User_id.t -> device_keys list
308308+309309+ (** {2 Cross-Signing} *)
310310+311311+ (** Get user's cross-signing keys *)
312312+ val get_user_identity :
313313+ t -> user_id:Matrix_proto.Id.User_id.t -> cross_signing_keys option
314314+315315+ (** Get own cross-signing identity (private keys) *)
316316+ val get_private_identity : t -> cross_signing_keys option
317317+318318+ (** {2 User Tracking} *)
319319+320320+ (** Get all tracked users *)
321321+ val get_tracked_users : t -> tracked_user list
322322+323323+ (** Mark a user as needing key update *)
324324+ val mark_user_dirty : t -> user_id:Matrix_proto.Id.User_id.t -> unit
325325+326326+ (** {2 Replay Protection} *)
327327+328328+ (** Check if a message hash is known (replay protection) *)
329329+ val is_message_known : t -> hash:string -> bool
330330+331331+ (** {2 Backup} *)
332332+333333+ val get_backup_key : t -> string option
334334+ val set_backup_key : t -> string -> unit
335335+336336+ (** {2 Custom Storage} *)
337337+338338+ val get_custom_value : t -> string -> string option
339339+ val set_custom_value : t -> string -> string -> unit
340340+ val remove_custom_value : t -> string -> unit
341341+342342+ (** {2 Cleanup} *)
343343+344344+ val clear : t -> unit
345345+end
346346+347347+(** {1 EVENT_CACHE_STORE Module Type} *)
348348+349349+(** Cached event entry *)
350350+type cached_event = {
351351+ event_id : Matrix_proto.Id.Event_id.t;
352352+ room_id : Matrix_proto.Id.Room_id.t;
353353+ sender : Matrix_proto.Id.User_id.t;
354354+ origin_server_ts : int64;
355355+ event_type : string;
356356+ content : Jsont.json;
357357+ unsigned : Jsont.json option;
358358+}
359359+360360+(** Gap in event history (for pagination) *)
361361+type event_gap = {
362362+ prev_token : string;
363363+}
364364+365365+(** Chunk of events in linked list *)
366366+type event_chunk = {
367367+ chunk_id : int;
368368+ events : cached_event list;
369369+ gap : event_gap option; (* Gap before this chunk if any *)
370370+ prev_chunk_id : int option;
371371+ next_chunk_id : int option;
372372+}
373373+374374+(** Module type for event history storage *)
375375+module type EVENT_CACHE_STORE = sig
376376+ type t
377377+ type error
378378+379379+ (** Create a new store *)
380380+ val create : unit -> t
381381+382382+ (** {2 Event Storage} *)
383383+384384+ (** Save an event to cache *)
385385+ val save_event : t -> cached_event -> unit
386386+387387+ (** Get an event by ID *)
388388+ val get_event :
389389+ t -> room_id:Matrix_proto.Id.Room_id.t -> event_id:Matrix_proto.Id.Event_id.t ->
390390+ cached_event option
391391+392392+ (** Get events for a room *)
393393+ val get_room_events :
394394+ t ->
395395+ room_id:Matrix_proto.Id.Room_id.t ->
396396+ ?limit:int ->
397397+ ?event_type:string ->
398398+ unit ->
399399+ cached_event list
400400+401401+ (** {2 Linked Chunk Management} *)
402402+403403+ (** Get the most recent chunk for a room *)
404404+ val get_last_chunk : t -> room_id:Matrix_proto.Id.Room_id.t -> event_chunk option
405405+406406+ (** Load a specific chunk *)
407407+ val get_chunk : t -> room_id:Matrix_proto.Id.Room_id.t -> chunk_id:int -> event_chunk option
408408+409409+ (** Save a chunk *)
410410+ val save_chunk : t -> room_id:Matrix_proto.Id.Room_id.t -> event_chunk -> unit
411411+412412+ (** {2 Deduplication} *)
413413+414414+ (** Filter out events that are already cached *)
415415+ val filter_duplicates :
416416+ t ->
417417+ room_id:Matrix_proto.Id.Room_id.t ->
418418+ event_ids:Matrix_proto.Id.Event_id.t list ->
419419+ Matrix_proto.Id.Event_id.t list
420420+421421+ (** {2 Relations} *)
422422+423423+ (** Find events related to a given event (replies, edits, reactions) *)
424424+ val find_event_relations :
425425+ t ->
426426+ room_id:Matrix_proto.Id.Room_id.t ->
427427+ event_id:Matrix_proto.Id.Event_id.t ->
428428+ ?rel_type:string ->
429429+ unit ->
430430+ cached_event list
431431+432432+ (** {2 Cleanup} *)
433433+434434+ (** Remove all events for a room *)
435435+ val remove_room : t -> room_id:Matrix_proto.Id.Room_id.t -> unit
436436+437437+ (** Clear all cached events *)
438438+ val clear : t -> unit
439439+end
440440+441441+(** {1 In-Memory Implementations} *)
442442+443443+(** In-memory state store implementation *)
444444+module Memory_state_store : STATE_STORE = struct
445445+ type error = [ `Storage_error of string ]
446446+447447+ type t = {
448448+ mutable sync_token : string option;
449449+ account_data : (string, Jsont.json) Hashtbl.t;
450450+ presence : (string, Jsont.json) Hashtbl.t;
451451+ room_infos : (string, room_info) Hashtbl.t;
452452+ room_state : (string, (string, (string, Jsont.json) Hashtbl.t) Hashtbl.t) Hashtbl.t;
453453+ room_account_data : (string, (string, Jsont.json) Hashtbl.t) Hashtbl.t;
454454+ receipts : (string, Jsont.json) Hashtbl.t;
455455+ profiles : (string, (string, member_profile) Hashtbl.t) Hashtbl.t;
456456+ kv_store : (string, kv_data) Hashtbl.t;
457457+ }
458458+459459+ let create () = {
460460+ sync_token = None;
461461+ account_data = Hashtbl.create 16;
462462+ presence = Hashtbl.create 64;
463463+ room_infos = Hashtbl.create 64;
464464+ room_state = Hashtbl.create 64;
465465+ room_account_data = Hashtbl.create 64;
466466+ receipts = Hashtbl.create 64;
467467+ profiles = Hashtbl.create 64;
468468+ kv_store = Hashtbl.create 16;
469469+ }
470470+471471+ let save_changes t (changes : state_changes) =
472472+ (* Sync token *)
473473+ (match changes.sync_token with
474474+ | Some token -> t.sync_token <- Some token
475475+ | None -> ());
476476+477477+ (* Account data *)
478478+ List.iter (fun (event_type, content) ->
479479+ Hashtbl.replace t.account_data event_type content
480480+ ) changes.account_data;
481481+482482+ (* Presence *)
483483+ List.iter (fun (user_id, event) ->
484484+ Hashtbl.replace t.presence user_id event
485485+ ) changes.presence;
486486+487487+ (* Room infos *)
488488+ List.iter (fun (room_id, info) ->
489489+ Hashtbl.replace t.room_infos room_id info
490490+ ) changes.room_infos;
491491+492492+ (* Room state *)
493493+ List.iter (fun (room_id, event_types) ->
494494+ let room_tbl =
495495+ match Hashtbl.find_opt t.room_state room_id with
496496+ | Some tbl -> tbl
497497+ | None ->
498498+ let tbl = Hashtbl.create 16 in
499499+ Hashtbl.add t.room_state room_id tbl;
500500+ tbl
501501+ in
502502+ List.iter (fun (event_type, state_keys) ->
503503+ let type_tbl =
504504+ match Hashtbl.find_opt room_tbl event_type with
505505+ | Some tbl -> tbl
506506+ | None ->
507507+ let tbl = Hashtbl.create 8 in
508508+ Hashtbl.add room_tbl event_type tbl;
509509+ tbl
510510+ in
511511+ List.iter (fun (state_key, event) ->
512512+ Hashtbl.replace type_tbl state_key event
513513+ ) state_keys
514514+ ) event_types
515515+ ) changes.room_state;
516516+517517+ (* Room account data *)
518518+ List.iter (fun (room_id, events) ->
519519+ let tbl =
520520+ match Hashtbl.find_opt t.room_account_data room_id with
521521+ | Some tbl -> tbl
522522+ | None ->
523523+ let tbl = Hashtbl.create 8 in
524524+ Hashtbl.add t.room_account_data room_id tbl;
525525+ tbl
526526+ in
527527+ List.iter (fun (event_type, content) ->
528528+ Hashtbl.replace tbl event_type content
529529+ ) events
530530+ ) changes.room_account_data;
531531+532532+ (* Receipts *)
533533+ List.iter (fun (room_id, content) ->
534534+ Hashtbl.replace t.receipts room_id content
535535+ ) changes.receipts;
536536+537537+ (* Profiles *)
538538+ List.iter (fun (room_id, users) ->
539539+ let tbl =
540540+ match Hashtbl.find_opt t.profiles room_id with
541541+ | Some tbl -> tbl
542542+ | None ->
543543+ let tbl = Hashtbl.create 64 in
544544+ Hashtbl.add t.profiles room_id tbl;
545545+ tbl
546546+ in
547547+ List.iter (fun (user_id, profile) ->
548548+ Hashtbl.replace tbl user_id profile
549549+ ) users
550550+ ) changes.profiles;
551551+552552+ Ok ()
553553+554554+ let get_sync_token t = t.sync_token
555555+ let set_sync_token t token = t.sync_token <- Some token
556556+557557+ let get_state_event t ~room_id ~event_type ~state_key =
558558+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
559559+ match Hashtbl.find_opt t.room_state room_id_str with
560560+ | None -> None
561561+ | Some room_tbl ->
562562+ match Hashtbl.find_opt room_tbl event_type with
563563+ | None -> None
564564+ | Some type_tbl -> Hashtbl.find_opt type_tbl state_key
565565+566566+ let get_state_events t ~room_id ~event_type =
567567+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
568568+ match Hashtbl.find_opt t.room_state room_id_str with
569569+ | None -> []
570570+ | Some room_tbl ->
571571+ match Hashtbl.find_opt room_tbl event_type with
572572+ | None -> []
573573+ | Some type_tbl -> Hashtbl.fold (fun k v acc -> (k, v) :: acc) type_tbl []
574574+575575+ let get_room_info t room_id =
576576+ Hashtbl.find_opt t.room_infos (Matrix_proto.Id.Room_id.to_string room_id)
577577+578578+ let get_room_ids t =
579579+ Hashtbl.fold (fun id_str _info acc ->
580580+ match Matrix_proto.Id.Room_id.of_string id_str with
581581+ | Ok id -> id :: acc
582582+ | Error _ -> acc
583583+ ) t.room_infos []
584584+585585+ let get_profile t ~room_id ~user_id =
586586+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
587587+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
588588+ match Hashtbl.find_opt t.profiles room_id_str with
589589+ | None -> None
590590+ | Some tbl -> Hashtbl.find_opt tbl user_id_str
591591+592592+ let get_account_data t ~event_type =
593593+ Hashtbl.find_opt t.account_data event_type
594594+595595+ let get_room_account_data t ~room_id ~event_type =
596596+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
597597+ match Hashtbl.find_opt t.room_account_data room_id_str with
598598+ | None -> None
599599+ | Some tbl -> Hashtbl.find_opt tbl event_type
600600+601601+ let get_receipts t ~room_id =
602602+ Hashtbl.find_opt t.receipts (Matrix_proto.Id.Room_id.to_string room_id)
603603+604604+ let get_presence t ~user_id =
605605+ Hashtbl.find_opt t.presence (Matrix_proto.Id.User_id.to_string user_id)
606606+607607+ let get_kv t key = Hashtbl.find_opt t.kv_store key
608608+ let set_kv t key value = Hashtbl.replace t.kv_store key value
609609+ let remove_kv t key = Hashtbl.remove t.kv_store key
610610+611611+ let clear t =
612612+ t.sync_token <- None;
613613+ Hashtbl.clear t.account_data;
614614+ Hashtbl.clear t.presence;
615615+ Hashtbl.clear t.room_infos;
616616+ Hashtbl.clear t.room_state;
617617+ Hashtbl.clear t.room_account_data;
618618+ Hashtbl.clear t.receipts;
619619+ Hashtbl.clear t.profiles;
620620+ Hashtbl.clear t.kv_store
621621+end
622622+623623+(** In-memory crypto store implementation *)
624624+module Memory_crypto_store : CRYPTO_STORE = struct
625625+ type error = [ `Storage_error of string ]
626626+627627+ type t = {
628628+ mutable account_pickle : string option;
629629+ mutable private_identity : cross_signing_keys option;
630630+ olm_sessions : (string, olm_session list) Hashtbl.t; (* sender_key -> sessions *)
631631+ olm_sessions_by_id : (string, olm_session) Hashtbl.t; (* session_id -> session *)
632632+ inbound_sessions : (string, inbound_group_session) Hashtbl.t; (* room_id:session_id -> session *)
633633+ outbound_sessions : (string, outbound_group_session) Hashtbl.t; (* room_id -> session *)
634634+ devices : (string, device_keys) Hashtbl.t; (* user_id:device_id -> keys *)
635635+ user_identities : (string, cross_signing_keys) Hashtbl.t; (* user_id -> keys *)
636636+ tracked_users : (string, tracked_user) Hashtbl.t;
637637+ message_hashes : (string, string) Hashtbl.t;
638638+ mutable backup_key : string option;
639639+ custom_values : (string, string) Hashtbl.t;
640640+ }
641641+642642+ let create () = {
643643+ account_pickle = None;
644644+ private_identity = None;
645645+ olm_sessions = Hashtbl.create 64;
646646+ olm_sessions_by_id = Hashtbl.create 256;
647647+ inbound_sessions = Hashtbl.create 256;
648648+ outbound_sessions = Hashtbl.create 64;
649649+ devices = Hashtbl.create 256;
650650+ user_identities = Hashtbl.create 64;
651651+ tracked_users = Hashtbl.create 64;
652652+ message_hashes = Hashtbl.create 1024;
653653+ backup_key = None;
654654+ custom_values = Hashtbl.create 16;
655655+ }
656656+657657+ let load_account t = t.account_pickle
658658+ let save_account t pickle = t.account_pickle <- Some pickle
659659+660660+ let save_changes t (changes : crypto_changes) =
661661+ (* Account *)
662662+ (match changes.account_pickle with
663663+ | Some pickle -> t.account_pickle <- Some pickle
664664+ | None -> ());
665665+666666+ (* Private identity *)
667667+ (match changes.private_identity with
668668+ | Some identity -> t.private_identity <- Some identity
669669+ | None -> ());
670670+671671+ (* Olm sessions *)
672672+ List.iter (fun (session : olm_session) ->
673673+ Hashtbl.replace t.olm_sessions_by_id session.session_id session;
674674+ let sessions : olm_session list =
675675+ match Hashtbl.find_opt t.olm_sessions session.sender_key with
676676+ | Some lst -> session :: List.filter (fun (s : olm_session) -> s.session_id <> session.session_id) lst
677677+ | None -> [session]
678678+ in
679679+ Hashtbl.replace t.olm_sessions session.sender_key sessions
680680+ ) changes.olm_sessions;
681681+682682+ (* Inbound group sessions *)
683683+ List.iter (fun (session : inbound_group_session) ->
684684+ let key = Matrix_proto.Id.Room_id.to_string session.room_id ^ ":" ^ session.session_id in
685685+ Hashtbl.replace t.inbound_sessions key session
686686+ ) changes.inbound_group_sessions;
687687+688688+ (* Outbound group sessions *)
689689+ List.iter (fun (session : outbound_group_session) ->
690690+ let key = Matrix_proto.Id.Room_id.to_string session.room_id in
691691+ Hashtbl.replace t.outbound_sessions key session
692692+ ) changes.outbound_group_sessions;
693693+694694+ (* New devices *)
695695+ List.iter (fun (device : device_keys) ->
696696+ let key =
697697+ Matrix_proto.Id.User_id.to_string device.user_id ^ ":" ^
698698+ Matrix_proto.Id.Device_id.to_string device.device_id
699699+ in
700700+ Hashtbl.replace t.devices key device
701701+ ) changes.devices_new;
702702+703703+ (* Changed devices *)
704704+ List.iter (fun (device : device_keys) ->
705705+ let key =
706706+ Matrix_proto.Id.User_id.to_string device.user_id ^ ":" ^
707707+ Matrix_proto.Id.Device_id.to_string device.device_id
708708+ in
709709+ Hashtbl.replace t.devices key device
710710+ ) changes.devices_changed;
711711+712712+ (* Deleted devices *)
713713+ List.iter (fun (user_id, device_id) ->
714714+ let key =
715715+ Matrix_proto.Id.User_id.to_string user_id ^ ":" ^
716716+ Matrix_proto.Id.Device_id.to_string device_id
717717+ in
718718+ Hashtbl.remove t.devices key
719719+ ) changes.devices_deleted;
720720+721721+ (* Tracked users *)
722722+ List.iter (fun (user : tracked_user) ->
723723+ Hashtbl.replace t.tracked_users
724724+ (Matrix_proto.Id.User_id.to_string user.user_id) user
725725+ ) changes.tracked_users;
726726+727727+ (* Message hashes *)
728728+ List.iter (fun (hash, event_id) ->
729729+ Hashtbl.replace t.message_hashes hash event_id
730730+ ) changes.message_hashes;
731731+732732+ (* Backup key *)
733733+ (match changes.backup_decryption_key with
734734+ | Some key -> t.backup_key <- Some key
735735+ | None -> ());
736736+737737+ Ok ()
738738+739739+ let get_sessions t ~sender_key =
740740+ match Hashtbl.find_opt t.olm_sessions sender_key with
741741+ | Some sessions -> sessions
742742+ | None -> []
743743+744744+ let get_session t ~session_id =
745745+ Hashtbl.find_opt t.olm_sessions_by_id session_id
746746+747747+ let get_inbound_group_session t ~room_id ~session_id =
748748+ let key = Matrix_proto.Id.Room_id.to_string room_id ^ ":" ^ session_id in
749749+ Hashtbl.find_opt t.inbound_sessions key
750750+751751+ let get_inbound_group_sessions_for_room t ~room_id =
752752+ let room_prefix = Matrix_proto.Id.Room_id.to_string room_id ^ ":" in
753753+ Hashtbl.fold (fun key session acc ->
754754+ if String.length key >= String.length room_prefix &&
755755+ String.sub key 0 (String.length room_prefix) = room_prefix
756756+ then session :: acc
757757+ else acc
758758+ ) t.inbound_sessions []
759759+760760+ let get_sessions_for_backup t ~limit =
761761+ let unbacked_up = Hashtbl.fold (fun _key session acc ->
762762+ if not session.backed_up then session :: acc else acc
763763+ ) t.inbound_sessions [] in
764764+ if List.length unbacked_up <= limit then unbacked_up
765765+ else
766766+ let rec take n lst =
767767+ if n <= 0 then []
768768+ else match lst with
769769+ | [] -> []
770770+ | x :: xs -> x :: take (n - 1) xs
771771+ in
772772+ take limit unbacked_up
773773+774774+ let mark_sessions_backed_up t ~session_ids =
775775+ List.iter (fun session_id ->
776776+ Hashtbl.iter (fun key (session : inbound_group_session) ->
777777+ if session.session_id = session_id then
778778+ Hashtbl.replace t.inbound_sessions key { session with backed_up = true }
779779+ ) t.inbound_sessions
780780+ ) session_ids
781781+782782+ let get_outbound_group_session t ~room_id =
783783+ Hashtbl.find_opt t.outbound_sessions (Matrix_proto.Id.Room_id.to_string room_id)
784784+785785+ let get_device t ~user_id ~device_id =
786786+ let key =
787787+ Matrix_proto.Id.User_id.to_string user_id ^ ":" ^
788788+ Matrix_proto.Id.Device_id.to_string device_id
789789+ in
790790+ Hashtbl.find_opt t.devices key
791791+792792+ let get_user_devices t ~user_id =
793793+ let user_prefix = Matrix_proto.Id.User_id.to_string user_id ^ ":" in
794794+ Hashtbl.fold (fun key device acc ->
795795+ if String.length key >= String.length user_prefix &&
796796+ String.sub key 0 (String.length user_prefix) = user_prefix
797797+ then device :: acc
798798+ else acc
799799+ ) t.devices []
800800+801801+ let get_user_identity t ~user_id =
802802+ Hashtbl.find_opt t.user_identities (Matrix_proto.Id.User_id.to_string user_id)
803803+804804+ let get_private_identity t = t.private_identity
805805+806806+ let get_tracked_users t =
807807+ Hashtbl.fold (fun _key user acc -> user :: acc) t.tracked_users []
808808+809809+ let mark_user_dirty t ~user_id =
810810+ let key = Matrix_proto.Id.User_id.to_string user_id in
811811+ match Hashtbl.find_opt t.tracked_users key with
812812+ | Some user -> Hashtbl.replace t.tracked_users key { user with dirty = true }
813813+ | None -> Hashtbl.add t.tracked_users key { user_id; dirty = true }
814814+815815+ let is_message_known t ~hash =
816816+ Hashtbl.mem t.message_hashes hash
817817+818818+ let get_backup_key t = t.backup_key
819819+ let set_backup_key t key = t.backup_key <- Some key
820820+821821+ let get_custom_value t key = Hashtbl.find_opt t.custom_values key
822822+ let set_custom_value t key value = Hashtbl.replace t.custom_values key value
823823+ let remove_custom_value t key = Hashtbl.remove t.custom_values key
824824+825825+ let clear t =
826826+ t.account_pickle <- None;
827827+ t.private_identity <- None;
828828+ Hashtbl.clear t.olm_sessions;
829829+ Hashtbl.clear t.olm_sessions_by_id;
830830+ Hashtbl.clear t.inbound_sessions;
831831+ Hashtbl.clear t.outbound_sessions;
832832+ Hashtbl.clear t.devices;
833833+ Hashtbl.clear t.user_identities;
834834+ Hashtbl.clear t.tracked_users;
835835+ Hashtbl.clear t.message_hashes;
836836+ t.backup_key <- None;
837837+ Hashtbl.clear t.custom_values
838838+end
839839+840840+(** In-memory event cache store implementation *)
841841+module Memory_event_cache_store : EVENT_CACHE_STORE = struct
842842+ type error = [ `Storage_error of string ]
843843+844844+ type t = {
845845+ events : (string, cached_event) Hashtbl.t; (* room_id:event_id -> event *)
846846+ room_events : (string, cached_event list) Hashtbl.t; (* room_id -> events (newest first) *)
847847+ chunks : (string, event_chunk) Hashtbl.t; (* room_id:chunk_id -> chunk *)
848848+ last_chunks : (string, int) Hashtbl.t; (* room_id -> last chunk id *)
849849+ mutable next_chunk_id : int; (* For generating new chunk IDs *)
850850+ } [@@warning "-69"]
851851+852852+ let create () = {
853853+ events = Hashtbl.create 1024;
854854+ room_events = Hashtbl.create 64;
855855+ chunks = Hashtbl.create 128;
856856+ last_chunks = Hashtbl.create 64;
857857+ next_chunk_id = 0;
858858+ }
859859+860860+ let event_key room_id event_id =
861861+ Matrix_proto.Id.Room_id.to_string room_id ^ ":" ^
862862+ Matrix_proto.Id.Event_id.to_string event_id
863863+864864+ let chunk_key room_id chunk_id =
865865+ Matrix_proto.Id.Room_id.to_string room_id ^ ":" ^ string_of_int chunk_id
866866+867867+ let save_event t event =
868868+ let key = event_key event.room_id event.event_id in
869869+ Hashtbl.replace t.events key event;
870870+ (* Also add to room events list *)
871871+ let room_id_str = Matrix_proto.Id.Room_id.to_string event.room_id in
872872+ let events = match Hashtbl.find_opt t.room_events room_id_str with
873873+ | Some lst -> event :: lst
874874+ | None -> [event]
875875+ in
876876+ Hashtbl.replace t.room_events room_id_str events
877877+878878+ let get_event t ~room_id ~event_id =
879879+ Hashtbl.find_opt t.events (event_key room_id event_id)
880880+881881+ let get_room_events t ~room_id ?limit ?event_type () =
882882+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
883883+ match Hashtbl.find_opt t.room_events room_id_str with
884884+ | None -> []
885885+ | Some events ->
886886+ let filtered = match event_type with
887887+ | None -> events
888888+ | Some et -> List.filter (fun e -> e.event_type = et) events
889889+ in
890890+ match limit with
891891+ | None -> filtered
892892+ | Some n ->
893893+ let rec take n lst = match n, lst with
894894+ | 0, _ | _, [] -> []
895895+ | n, x :: xs -> x :: take (n - 1) xs
896896+ in
897897+ take n filtered
898898+899899+ let get_last_chunk t ~room_id =
900900+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
901901+ match Hashtbl.find_opt t.last_chunks room_id_str with
902902+ | None -> None
903903+ | Some chunk_id -> Hashtbl.find_opt t.chunks (chunk_key room_id chunk_id)
904904+905905+ let get_chunk t ~room_id ~chunk_id =
906906+ Hashtbl.find_opt t.chunks (chunk_key room_id chunk_id)
907907+908908+ let save_chunk t ~room_id chunk =
909909+ let key = chunk_key room_id chunk.chunk_id in
910910+ Hashtbl.replace t.chunks key chunk;
911911+ (* Update last chunk if this is newer *)
912912+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
913913+ match Hashtbl.find_opt t.last_chunks room_id_str with
914914+ | None -> Hashtbl.add t.last_chunks room_id_str chunk.chunk_id
915915+ | Some last_id when chunk.chunk_id > last_id ->
916916+ Hashtbl.replace t.last_chunks room_id_str chunk.chunk_id
917917+ | _ -> ()
918918+919919+ let filter_duplicates t ~room_id ~event_ids =
920920+ List.filter (fun event_id ->
921921+ not (Hashtbl.mem t.events (event_key room_id event_id))
922922+ ) event_ids
923923+924924+ let find_event_relations t ~room_id ~event_id ?rel_type () =
925925+ let target_id = Matrix_proto.Id.Event_id.to_string event_id in
926926+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
927927+ match Hashtbl.find_opt t.room_events room_id_str with
928928+ | None -> []
929929+ | Some events ->
930930+ List.filter (fun event ->
931931+ (* Check if event has m.relates_to pointing to target *)
932932+ match event.content with
933933+ | Jsont.Object (fields, _) ->
934934+ let has_relation =
935935+ List.exists (fun ((name, _), value) ->
936936+ name = "m.relates_to" &&
937937+ match value with
938938+ | Jsont.Object (rel_fields, _) ->
939939+ List.exists (fun ((n, _), v) ->
940940+ n = "event_id" &&
941941+ match v with
942942+ | Jsont.String (s, _) -> s = target_id
943943+ | _ -> false
944944+ ) rel_fields &&
945945+ (match rel_type with
946946+ | None -> true
947947+ | Some rt ->
948948+ List.exists (fun ((n, _), v) ->
949949+ n = "rel_type" &&
950950+ match v with
951951+ | Jsont.String (s, _) -> s = rt
952952+ | _ -> false
953953+ ) rel_fields)
954954+ | _ -> false
955955+ ) fields
956956+ in
957957+ has_relation
958958+ | _ -> false
959959+ ) events
960960+961961+ let remove_room t ~room_id =
962962+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
963963+ (* Remove all events for the room *)
964964+ let keys_to_remove = Hashtbl.fold (fun key _event acc ->
965965+ if String.length key > String.length room_id_str + 1 &&
966966+ String.sub key 0 (String.length room_id_str + 1) = room_id_str ^ ":"
967967+ then key :: acc
968968+ else acc
969969+ ) t.events [] in
970970+ List.iter (Hashtbl.remove t.events) keys_to_remove;
971971+ (* Remove room events list *)
972972+ Hashtbl.remove t.room_events room_id_str;
973973+ (* Remove chunks *)
974974+ let chunk_keys_to_remove = Hashtbl.fold (fun key _chunk acc ->
975975+ if String.length key > String.length room_id_str + 1 &&
976976+ String.sub key 0 (String.length room_id_str + 1) = room_id_str ^ ":"
977977+ then key :: acc
978978+ else acc
979979+ ) t.chunks [] in
980980+ List.iter (Hashtbl.remove t.chunks) chunk_keys_to_remove;
981981+ (* Remove last chunk ref *)
982982+ Hashtbl.remove t.last_chunks room_id_str
983983+984984+ let clear t =
985985+ Hashtbl.clear t.events;
986986+ Hashtbl.clear t.room_events;
987987+ Hashtbl.clear t.chunks;
988988+ Hashtbl.clear t.last_chunks;
989989+ t.next_chunk_id <- 0
990990+end
991991+992992+(** {1 Store Creation Helpers} *)
993993+994994+(** Create in-memory state store *)
995995+let create_memory_state_store = Memory_state_store.create
996996+997997+(** Create in-memory crypto store *)
998998+let create_memory_crypto_store = Memory_crypto_store.create
999999+10001000+(** Create in-memory event cache store *)
10011001+let create_memory_event_cache_store = Memory_event_cache_store.create
···11+(** Sync operations and long-polling loop. *)
22+33+(** {1 Sync API} *)
44+55+(** Sync parameters. *)
66+type params = {
77+ filter : string option;
88+ (** Filter ID to use (from {!create_filter}). *)
99+ since : string option;
1010+ (** Sync token from previous sync. *)
1111+ full_state : bool;
1212+ (** If true, return full state even if since is provided. *)
1313+ set_presence : [ `Online | `Offline | `Unavailable ] option;
1414+ (** Presence state to set. *)
1515+ timeout : int;
1616+ (** Long-poll timeout in milliseconds. 0 for immediate return. *)
1717+}
1818+1919+(** Default sync parameters: 30 second timeout, no filter. *)
2020+val default_params : params
2121+2222+(** Perform a single sync request.
2323+2424+ Returns the sync response from the server. The [next_batch] field
2525+ should be used as [since] for the next sync request. *)
2626+val sync :
2727+ Client.t ->
2828+ ?params:params ->
2929+ unit ->
3030+ (Matrix_proto.Sync.Response.t, Error.t) result
3131+3232+(** {1 Sync Loop} *)
3333+3434+(** Action to take after processing a sync response or error. *)
3535+type action =
3636+ | Continue (** Continue syncing *)
3737+ | Stop (** Stop the sync loop *)
3838+ | Retry_after of int (** Retry after N milliseconds *)
3939+4040+(** Callbacks for the sync loop. *)
4141+type callbacks = {
4242+ on_sync : Matrix_proto.Sync.Response.t -> action;
4343+ (** Called for each successful sync response. *)
4444+ on_error : Error.t -> action;
4545+ (** Called when sync fails. *)
4646+}
4747+4848+(** Run a continuous sync loop.
4949+5050+ This function blocks and continuously syncs with the server,
5151+ calling the appropriate callback for each response or error.
5252+ The loop continues until a callback returns [Stop].
5353+5454+ @param clock Eio clock for sleeping on Retry_after.
5555+ @param initial_since Starting sync token (None for initial sync).
5656+ @param params Sync parameters to use (timeout, filter, etc.). *)
5757+val sync_forever :
5858+ Client.t ->
5959+ clock:_ Eio.Time.clock ->
6060+ ?initial_since:string ->
6161+ ?params:params ->
6262+ callbacks:callbacks ->
6363+ unit ->
6464+ unit
6565+6666+(** {1 Filters} *)
6767+6868+(** Event filter for sync. *)
6969+type event_filter = {
7070+ limit : int option;
7171+ not_senders : string list;
7272+ not_types : string list;
7373+ senders : string list;
7474+ types : string list;
7575+}
7676+7777+(** Room event filter. *)
7878+type room_event_filter = {
7979+ limit : int option;
8080+ not_senders : string list;
8181+ not_types : string list;
8282+ senders : string list;
8383+ types : string list;
8484+ lazy_load_members : bool;
8585+ include_redundant_members : bool;
8686+ not_rooms : string list;
8787+ rooms : string list;
8888+ contains_url : bool option;
8989+}
9090+9191+(** Room filter. *)
9292+type room_filter = {
9393+ not_rooms : string list;
9494+ rooms : string list;
9595+ ephemeral : room_event_filter option;
9696+ include_leave : bool;
9797+ state : room_event_filter option;
9898+ timeline : room_event_filter option;
9999+ account_data : room_event_filter option;
100100+}
101101+102102+(** Complete sync filter. *)
103103+type filter = {
104104+ event_fields : string list;
105105+ event_format : [ `Client | `Federation ];
106106+ presence : event_filter option;
107107+ account_data : event_filter option;
108108+ room : room_filter option;
109109+}
110110+111111+(** Default empty event filter. *)
112112+val default_event_filter : event_filter
113113+114114+(** Default room event filter with lazy loading enabled. *)
115115+val default_room_event_filter : room_event_filter
116116+117117+(** Default room filter. *)
118118+val default_room_filter : room_filter
119119+120120+(** Default filter. *)
121121+val default_filter : filter
122122+123123+(** Create a filter on the homeserver.
124124+125125+ Returns the filter ID that can be used in sync requests. *)
126126+val create_filter :
127127+ Client.t ->
128128+ filter:filter ->
129129+ (string, Error.t) result
130130+131131+(** Get an existing filter from the homeserver. *)
132132+val get_filter :
133133+ Client.t ->
134134+ filter_id:string ->
135135+ (filter, Error.t) result
+423
lib/matrix_client/timeline.ml
···11+(** Room timeline management and event caching.
22+33+ This module provides:
44+ - Event storage and retrieval
55+ - Timeline pagination (forward and backward)
66+ - Room state tracking
77+ - Event deduplication *)
88+99+(** A linked chunk data structure for efficient timeline operations.
1010+ Based on the matrix-rust-sdk LinkedChunk pattern. *)
1111+module LinkedChunk = struct
1212+ type 'a chunk = {
1313+ mutable items : 'a list;
1414+ mutable prev : 'a chunk option;
1515+ mutable next : 'a chunk option;
1616+ id : int;
1717+ max_size : int;
1818+ }
1919+2020+ type 'a t = {
2121+ mutable head : 'a chunk option;
2222+ mutable tail : 'a chunk option;
2323+ mutable next_id : int;
2424+ max_chunk_size : int;
2525+ }
2626+2727+ let create ?(max_chunk_size = 50) () = {
2828+ head = None;
2929+ tail = None;
3030+ next_id = 0;
3131+ max_chunk_size;
3232+ }
3333+3434+ let create_chunk t =
3535+ let chunk = {
3636+ items = [];
3737+ prev = None;
3838+ next = None;
3939+ id = t.next_id;
4040+ max_size = t.max_chunk_size;
4141+ } in
4242+ t.next_id <- t.next_id + 1;
4343+ chunk
4444+4545+ (** Push item to the back of the timeline *)
4646+ let push_back t item =
4747+ match t.tail with
4848+ | None ->
4949+ let chunk = create_chunk t in
5050+ chunk.items <- [item];
5151+ t.head <- Some chunk;
5252+ t.tail <- Some chunk
5353+ | Some tail ->
5454+ if List.length tail.items >= tail.max_size then begin
5555+ (* Create new chunk *)
5656+ let chunk = create_chunk t in
5757+ chunk.items <- [item];
5858+ chunk.prev <- Some tail;
5959+ tail.next <- Some chunk;
6060+ t.tail <- Some chunk
6161+ end else
6262+ tail.items <- tail.items @ [item]
6363+6464+ (** Push item to the front of the timeline *)
6565+ let push_front t item =
6666+ match t.head with
6767+ | None ->
6868+ let chunk = create_chunk t in
6969+ chunk.items <- [item];
7070+ t.head <- Some chunk;
7171+ t.tail <- Some chunk
7272+ | Some head ->
7373+ if List.length head.items >= head.max_size then begin
7474+ (* Create new chunk *)
7575+ let chunk = create_chunk t in
7676+ chunk.items <- [item];
7777+ chunk.next <- Some head;
7878+ head.prev <- Some chunk;
7979+ t.head <- Some chunk
8080+ end else
8181+ head.items <- item :: head.items
8282+8383+ (** Push items to the front (for back-pagination) *)
8484+ let push_front_items t items =
8585+ List.iter (fun item -> push_front t item) (List.rev items)
8686+8787+ (** Iterate over all items from oldest to newest *)
8888+ let iter f t =
8989+ let rec iter_chunk = function
9090+ | None -> ()
9191+ | Some chunk ->
9292+ List.iter f chunk.items;
9393+ iter_chunk chunk.next
9494+ in
9595+ iter_chunk t.head
9696+9797+ (** Iterate from newest to oldest *)
9898+ let iter_rev f t =
9999+ let rec iter_chunk = function
100100+ | None -> ()
101101+ | Some chunk ->
102102+ List.iter f (List.rev chunk.items);
103103+ iter_chunk chunk.prev
104104+ in
105105+ iter_chunk t.tail
106106+107107+ (** Get last N items *)
108108+ let last_n t n =
109109+ let result = ref [] in
110110+ let count = ref 0 in
111111+ iter_rev (fun item ->
112112+ if !count < n then begin
113113+ result := item :: !result;
114114+ incr count
115115+ end
116116+ ) t;
117117+ !result
118118+119119+ (** Total number of items *)
120120+ let length t =
121121+ let count = ref 0 in
122122+ iter (fun _ -> incr count) t;
123123+ !count
124124+125125+ (** Find an item by predicate *)
126126+ let find_opt pred t =
127127+ let result = ref None in
128128+ let rec search_chunk = function
129129+ | None -> ()
130130+ | Some chunk ->
131131+ (match List.find_opt pred chunk.items with
132132+ | Some item -> result := Some item
133133+ | None -> search_chunk chunk.next)
134134+ in
135135+ search_chunk t.head;
136136+ !result
137137+138138+ (** Clear all items *)
139139+ let clear t =
140140+ t.head <- None;
141141+ t.tail <- None
142142+end
143143+144144+(** Timeline event wrapper with metadata *)
145145+type event_item = {
146146+ event : Jsont.json; (* Raw event JSON *)
147147+ event_id : Matrix_proto.Id.Event_id.t;
148148+ sender : Matrix_proto.Id.User_id.t;
149149+ origin_server_ts : int64;
150150+ event_type : string; (* e.g., "m.room.message" *)
151151+ (* Local metadata *)
152152+ local_echo : bool; (* true if this is a local echo not yet confirmed *)
153153+ decrypted : bool; (* true if this was decrypted from E2EE *)
154154+}
155155+156156+(** Room state entry *)
157157+type state_entry = {
158158+ event_type : string;
159159+ state_key : string;
160160+ content : Jsont.json;
161161+ sender : Matrix_proto.Id.User_id.t;
162162+ event_id : Matrix_proto.Id.Event_id.t option;
163163+}
164164+165165+(** Room timeline with state tracking *)
166166+type t = {
167167+ room_id : Matrix_proto.Id.Room_id.t;
168168+ (* Timeline events *)
169169+ events : event_item LinkedChunk.t;
170170+ (* Room state: (event_type, state_key) -> state_entry *)
171171+ mutable state : ((string * string) * state_entry) list;
172172+ (* Pagination tokens *)
173173+ mutable prev_batch : string option;
174174+ mutable next_batch : string option;
175175+ (* Event ID index for deduplication *)
176176+ mutable event_ids : string list;
177177+ (* Maximum events to keep in memory *)
178178+ max_events : int;
179179+ (* Room summary *)
180180+ mutable name : string option;
181181+ mutable topic : string option;
182182+ mutable avatar_url : string option;
183183+ mutable canonical_alias : Matrix_proto.Id.Room_alias.t option;
184184+ mutable joined_member_count : int;
185185+ mutable invited_member_count : int;
186186+ mutable is_encrypted : bool;
187187+ mutable is_direct : bool;
188188+ mutable notification_count : int;
189189+ mutable highlight_count : int;
190190+}
191191+192192+(** Create a new timeline for a room *)
193193+let create ~room_id ?(max_events = 1000) () = {
194194+ room_id;
195195+ events = LinkedChunk.create ();
196196+ state = [];
197197+ prev_batch = None;
198198+ next_batch = None;
199199+ event_ids = [];
200200+ max_events;
201201+ name = None;
202202+ topic = None;
203203+ avatar_url = None;
204204+ canonical_alias = None;
205205+ joined_member_count = 0;
206206+ invited_member_count = 0;
207207+ is_encrypted = false;
208208+ is_direct = false;
209209+ notification_count = 0;
210210+ highlight_count = 0;
211211+}
212212+213213+(** Check if an event is already in the timeline *)
214214+let has_event t event_id =
215215+ let id_str = Matrix_proto.Id.Event_id.to_string event_id in
216216+ List.mem id_str t.event_ids
217217+218218+(** Add an event to the timeline *)
219219+let add_event t ~event ~event_id ~sender ~origin_server_ts ~event_type ?(local_echo = false) ?(decrypted = false) () =
220220+ let id_str = Matrix_proto.Id.Event_id.to_string event_id in
221221+ if not (List.mem id_str t.event_ids) then begin
222222+ let item = { event; event_id; sender; origin_server_ts; event_type; local_echo; decrypted } in
223223+ LinkedChunk.push_back t.events item;
224224+ t.event_ids <- id_str :: t.event_ids;
225225+ (* Trim if over limit *)
226226+ if LinkedChunk.length t.events > t.max_events then begin
227227+ (* TODO: Remove oldest events *)
228228+ ()
229229+ end
230230+ end
231231+232232+(** Add events from back-pagination (older events) *)
233233+let add_events_back t events =
234234+ List.iter (fun (event, event_id, sender, origin_server_ts, event_type, decrypted) ->
235235+ let id_str = Matrix_proto.Id.Event_id.to_string event_id in
236236+ if not (List.mem id_str t.event_ids) then begin
237237+ let item = { event; event_id; sender; origin_server_ts; event_type; local_echo = false; decrypted } in
238238+ LinkedChunk.push_front t.events item;
239239+ t.event_ids <- id_str :: t.event_ids
240240+ end
241241+ ) events
242242+243243+(** Helper to get a string field from JSON object *)
244244+let get_json_string_field content field =
245245+ match content with
246246+ | Jsont.Object (fields, _meta) ->
247247+ (* name is (string * Meta.t) and mem is (name * json) *)
248248+ let find_field name =
249249+ List.find_opt (fun ((n, _meta), _v) -> String.equal n name) fields
250250+ in
251251+ (match find_field field with
252252+ | Some (_, Jsont.String (s, _)) -> Some s
253253+ | _ -> None)
254254+ | _ -> None
255255+256256+(** Update room state from a state event *)
257257+let update_state t ~event_type ~state_key ~content ~sender ?event_id () =
258258+ let key = (event_type, state_key) in
259259+ let entry = { event_type; state_key; content; sender; event_id } in
260260+ t.state <- (key, entry) :: List.filter (fun (k, _) -> k <> key) t.state;
261261+ (* Update summary fields based on state *)
262262+ match event_type with
263263+ | "m.room.name" ->
264264+ t.name <- get_json_string_field content "name"
265265+ | "m.room.topic" ->
266266+ t.topic <- get_json_string_field content "topic"
267267+ | "m.room.avatar" ->
268268+ t.avatar_url <- get_json_string_field content "url"
269269+ | "m.room.canonical_alias" ->
270270+ (match get_json_string_field content "alias" with
271271+ | Some alias ->
272272+ (match Matrix_proto.Id.Room_alias.of_string alias with
273273+ | Ok a -> t.canonical_alias <- Some a
274274+ | Error _ -> ())
275275+ | None -> ())
276276+ | "m.room.encryption" ->
277277+ t.is_encrypted <- true
278278+ | _ -> ()
279279+280280+(** Get state for a specific event type and state key *)
281281+let get_state t ~event_type ~state_key =
282282+ List.assoc_opt (event_type, state_key) t.state
283283+284284+(** Get all state for an event type *)
285285+let get_state_by_type t ~event_type =
286286+ List.filter_map (fun ((et, _sk), entry) ->
287287+ if et = event_type then Some entry else None
288288+ ) t.state
289289+290290+(** Get room members from state *)
291291+let get_members t =
292292+ get_state_by_type t ~event_type:"m.room.member"
293293+294294+(** Get the last N events *)
295295+let get_last_events t n =
296296+ LinkedChunk.last_n t.events n
297297+298298+(** Get all events *)
299299+let get_all_events t =
300300+ let result = ref [] in
301301+ LinkedChunk.iter (fun item -> result := item :: !result) t.events;
302302+ List.rev !result
303303+304304+(** Find an event by ID *)
305305+let find_event t event_id =
306306+ let target = Matrix_proto.Id.Event_id.to_string event_id in
307307+ LinkedChunk.find_opt (fun (item : event_item) ->
308308+ String.equal (Matrix_proto.Id.Event_id.to_string item.event_id) target
309309+ ) t.events
310310+311311+(** Get room display name (computed from state) *)
312312+let display_name t =
313313+ match t.name with
314314+ | Some name -> Some name
315315+ | None ->
316316+ match t.canonical_alias with
317317+ | Some alias -> Some (Matrix_proto.Id.Room_alias.to_string alias)
318318+ | None -> None (* Would compute from heroes in a full implementation *)
319319+320320+(** Set pagination token for back-pagination *)
321321+let set_prev_batch t token =
322322+ t.prev_batch <- Some token
323323+324324+(** Set pagination token for forward sync *)
325325+let set_next_batch t token =
326326+ t.next_batch <- Some token
327327+328328+(** Update from sync response *)
329329+let update_from_sync t ~joined_count ~invited_count ~notification_count ~highlight_count =
330330+ t.joined_member_count <- joined_count;
331331+ t.invited_member_count <- invited_count;
332332+ t.notification_count <- notification_count;
333333+ t.highlight_count <- highlight_count
334334+335335+(** Clear timeline (but keep state) *)
336336+let clear_timeline t =
337337+ LinkedChunk.clear t.events;
338338+ t.event_ids <- [];
339339+ t.prev_batch <- None
340340+341341+(** Replace local echo with confirmed event *)
342342+let confirm_local_echo t ~local_event_id ~confirmed_event_id =
343343+ let local_id = Matrix_proto.Id.Event_id.to_string local_event_id in
344344+ let confirmed_id = Matrix_proto.Id.Event_id.to_string confirmed_event_id in
345345+ (* Find and update the local echo *)
346346+ let found = ref false in
347347+ LinkedChunk.iter (fun (item : event_item) ->
348348+ if String.equal (Matrix_proto.Id.Event_id.to_string item.event_id) local_id then begin
349349+ (* Can't mutate in LinkedChunk, so we just track dedup *)
350350+ found := true
351351+ end
352352+ ) t.events;
353353+ if !found then begin
354354+ t.event_ids <- List.filter (fun id -> id <> local_id) t.event_ids;
355355+ t.event_ids <- confirmed_id :: t.event_ids
356356+ end
357357+358358+(* Alias for the timeline create function before Cache module shadows it *)
359359+let create_timeline = create
360360+361361+(** Room timeline cache - manages timelines for multiple rooms *)
362362+module Cache = struct
363363+ type cache = {
364364+ mutable rooms : (string * t) list;
365365+ max_rooms : int;
366366+ }
367367+368368+ let create ?(max_rooms = 100) () = {
369369+ rooms = [];
370370+ max_rooms;
371371+ }
372372+373373+ let get_or_create cache room_id =
374374+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
375375+ match List.assoc_opt room_id_str cache.rooms with
376376+ | Some timeline -> timeline
377377+ | None ->
378378+ let timeline = create_timeline ~room_id () in
379379+ cache.rooms <- (room_id_str, timeline) :: cache.rooms;
380380+ (* LRU eviction if needed *)
381381+ if List.length cache.rooms > cache.max_rooms then
382382+ cache.rooms <- List.rev (List.tl (List.rev cache.rooms));
383383+ timeline
384384+385385+ let get cache room_id =
386386+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
387387+ List.assoc_opt room_id_str cache.rooms
388388+389389+ let remove cache room_id =
390390+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
391391+ cache.rooms <- List.filter (fun (id, _) -> id <> room_id_str) cache.rooms
392392+393393+ let all_room_ids cache =
394394+ List.filter_map (fun (id_str, _) ->
395395+ match Matrix_proto.Id.Room_id.of_string id_str with
396396+ | Ok id -> Some id
397397+ | Error _ -> None
398398+ ) cache.rooms
399399+end
400400+401401+(** Pagination helper for fetching older messages *)
402402+let paginate_back client t ~limit =
403403+ match t.prev_batch with
404404+ | None -> Ok [] (* No more messages *)
405405+ | Some from ->
406406+ match Messages.get_messages client ~room_id:t.room_id ~from ~dir:Messages.Backward ~limit () with
407407+ | Error e -> Error e
408408+ | Ok response ->
409409+ (* Update prev_batch for next pagination *)
410410+ t.prev_batch <- response.Messages.end_;
411411+ (* Add events to timeline *)
412412+ let events = List.filter_map (fun (raw_event : Matrix_proto.Event.Raw_event.t) ->
413413+ (* Extract fields from the Raw_event structure *)
414414+ match raw_event.event_id with
415415+ | Some event_id ->
416416+ let event_type = Matrix_proto.Event.Event_type.to_string raw_event.type_ in
417417+ let ts = raw_event.origin_server_ts in (* Timestamp.t is int64 *)
418418+ (* tuple: (event, event_id, sender, ts, event_type, decrypted) *)
419419+ Some (raw_event.content, event_id, raw_event.sender, ts, event_type, false)
420420+ | None -> None
421421+ ) response.Messages.chunk in
422422+ add_events_back t events;
423423+ Ok events
+31
lib/matrix_client/typing.ml
···11+(** Typing notifications. *)
22+33+type typing_request = {
44+ typing : bool;
55+ timeout : int option;
66+} [@@warning "-69"]
77+88+let typing_request_jsont =
99+ Jsont.Object.(
1010+ map (fun typing timeout -> { typing; timeout })
1111+ |> mem "typing" Jsont.bool
1212+ |> opt_mem "timeout" Jsont.int ~enc:(fun t -> t.timeout)
1313+ |> finish)
1414+1515+let set_typing client ~room_id ~typing ?timeout () =
1616+ match Client.user_id client with
1717+ | None -> Error (Error.Network_error "Not logged in")
1818+ | Some user_id ->
1919+ let room_id_str = Matrix_proto.Id.Room_id.to_string room_id in
2020+ let user_id_str = Matrix_proto.Id.User_id.to_string user_id in
2121+ let path = Printf.sprintf "/rooms/%s/typing/%s"
2222+ (Uri.pct_encode room_id_str)
2323+ (Uri.pct_encode user_id_str)
2424+ in
2525+ let request = { typing; timeout } in
2626+ match Client.encode_body typing_request_jsont request with
2727+ | Error e -> Error e
2828+ | Ok body ->
2929+ match Client.put client ~path ~body () with
3030+ | Error e -> Error e
3131+ | Ok _ -> Ok ()
+13
lib/matrix_client/typing.mli
···11+(** Typing notifications. *)
22+33+(** Set typing status in a room.
44+55+ @param typing Whether the user is typing.
66+ @param timeout Typing timeout in milliseconds (default 30000). *)
77+val set_typing :
88+ Client.t ->
99+ room_id:Matrix_proto.Id.Room_id.t ->
1010+ typing:bool ->
1111+ ?timeout:int ->
1212+ unit ->
1313+ (unit, Error.t) result
+435
lib/matrix_client/uiaa.ml
···11+(** User-Interactive Authentication API (UIAA).
22+33+ UIAA is Matrix's mechanism for protecting sensitive operations that require
44+ additional verification beyond just an access token. Operations like:
55+ - Deleting devices
66+ - Changing passwords
77+ - Adding 3PIDs
88+ - Deactivating accounts
99+1010+ When these operations return a 401 with UIAA challenge, clients must
1111+ complete authentication stages before the operation can proceed. *)
1212+1313+(** Authentication stage types *)
1414+type auth_type =
1515+ | Password
1616+ | Recaptcha
1717+ | OAuth2
1818+ | Email_identity
1919+ | Msisdn (* Phone number *)
2020+ | Dummy
2121+ | Registration_token
2222+ | Terms
2323+ | Sso
2424+ | Sso_fallback
2525+ | Custom of string
2626+2727+let auth_type_of_string = function
2828+ | "m.login.password" -> Password
2929+ | "m.login.recaptcha" -> Recaptcha
3030+ | "m.login.oauth2" -> OAuth2
3131+ | "m.login.email.identity" -> Email_identity
3232+ | "m.login.msisdn" -> Msisdn
3333+ | "m.login.dummy" -> Dummy
3434+ | "m.login.registration_token" -> Registration_token
3535+ | "m.login.terms" -> Terms
3636+ | "m.login.sso" -> Sso
3737+ | "org.matrix.login.sso.fallback" -> Sso_fallback
3838+ | s -> Custom s
3939+4040+let auth_type_to_string = function
4141+ | Password -> "m.login.password"
4242+ | Recaptcha -> "m.login.recaptcha"
4343+ | OAuth2 -> "m.login.oauth2"
4444+ | Email_identity -> "m.login.email.identity"
4545+ | Msisdn -> "m.login.msisdn"
4646+ | Dummy -> "m.login.dummy"
4747+ | Registration_token -> "m.login.registration_token"
4848+ | Terms -> "m.login.terms"
4949+ | Sso -> "m.login.sso"
5050+ | Sso_fallback -> "org.matrix.login.sso.fallback"
5151+ | Custom s -> s
5252+5353+(** Authentication flow - a sequence of stages that must be completed *)
5454+type auth_flow = {
5555+ stages : auth_type list;
5656+}
5757+5858+(* Internal type for JSON parsing *)
5959+type auth_flow_json = {
6060+ stages_json : string list;
6161+}
6262+6363+let auth_flow_jsont =
6464+ let json_type =
6565+ Jsont.Object.(
6666+ map (fun stages_json -> { stages_json })
6767+ |> mem "stages" (Jsont.list Jsont.string) ~dec_absent:[]
6868+ ~enc:(fun t -> t.stages_json)
6969+ |> finish)
7070+ in
7171+ Jsont.map
7272+ ~dec:(fun flow -> { stages = List.map auth_type_of_string flow.stages_json })
7373+ ~enc:(fun flow -> { stages_json = List.map auth_type_to_string flow.stages })
7474+ json_type
7575+7676+(** UIAA response from server when authentication is required *)
7777+type uiaa_response = {
7878+ session : string option;
7979+ flows : auth_flow list;
8080+ completed : auth_type list;
8181+ params : Jsont.json option;
8282+ error : string option;
8383+ errcode : string option;
8484+}
8585+8686+(* Internal type for JSON parsing *)
8787+type uiaa_response_json = {
8888+ session_json : string option;
8989+ flows_json : auth_flow list;
9090+ completed_json : string list;
9191+ params_json : Jsont.json option;
9292+ error_json : string option;
9393+ errcode_json : string option;
9494+}
9595+9696+let uiaa_response_jsont =
9797+ let json_type =
9898+ Jsont.Object.(
9999+ map (fun session_json flows_json completed_json params_json error_json errcode_json ->
100100+ { session_json; flows_json; completed_json; params_json; error_json; errcode_json })
101101+ |> opt_mem "session" Jsont.string ~enc:(fun t -> t.session_json)
102102+ |> mem "flows" (Jsont.list auth_flow_jsont) ~dec_absent:[] ~enc:(fun t -> t.flows_json)
103103+ |> mem "completed" (Jsont.list Jsont.string) ~dec_absent:[]
104104+ ~enc:(fun t -> t.completed_json)
105105+ |> opt_mem "params" Jsont.json ~enc:(fun t -> t.params_json)
106106+ |> opt_mem "error" Jsont.string ~enc:(fun t -> t.error_json)
107107+ |> opt_mem "errcode" Jsont.string ~enc:(fun t -> t.errcode_json)
108108+ |> finish)
109109+ in
110110+ Jsont.map
111111+ ~dec:(fun r -> {
112112+ session = r.session_json;
113113+ flows = r.flows_json;
114114+ completed = List.map auth_type_of_string r.completed_json;
115115+ params = r.params_json;
116116+ error = r.error_json;
117117+ errcode = r.errcode_json;
118118+ })
119119+ ~enc:(fun r -> {
120120+ session_json = r.session;
121121+ flows_json = r.flows;
122122+ completed_json = List.map auth_type_to_string r.completed;
123123+ params_json = r.params;
124124+ error_json = r.error;
125125+ errcode_json = r.errcode;
126126+ })
127127+ json_type
128128+129129+(** Authentication data to send in response to UIAA challenge *)
130130+type auth_data =
131131+ | Password_auth of {
132132+ identifier : user_identifier;
133133+ password : string;
134134+ session : string option;
135135+ }
136136+ | Recaptcha_auth of {
137137+ response : string;
138138+ session : string option;
139139+ }
140140+ | Email_identity_auth of {
141141+ threepid_creds : threepid_creds;
142142+ session : string option;
143143+ }
144144+ | Msisdn_auth of {
145145+ threepid_creds : threepid_creds;
146146+ session : string option;
147147+ }
148148+ | Dummy_auth of {
149149+ session : string option;
150150+ }
151151+ | Token_auth of {
152152+ token : string;
153153+ session : string option;
154154+ }
155155+ | Terms_auth of {
156156+ session : string option;
157157+ }
158158+159159+and user_identifier =
160160+ | User of string (* user_id *)
161161+ | ThirdParty of { medium : string; address : string }
162162+ | Phone of { country : string; phone : string }
163163+164164+and threepid_creds = {
165165+ sid : string;
166166+ client_secret : string;
167167+ id_server : string option;
168168+ id_access_token : string option;
169169+}
170170+171171+(** Encode user identifier to JSON *)
172172+let user_identifier_to_json = function
173173+ | User user_id ->
174174+ Printf.sprintf {|{"type":"m.id.user","user":"%s"}|} user_id
175175+ | ThirdParty { medium; address } ->
176176+ Printf.sprintf {|{"type":"m.id.thirdparty","medium":"%s","address":"%s"}|}
177177+ medium address
178178+ | Phone { country; phone } ->
179179+ Printf.sprintf {|{"type":"m.id.phone","country":"%s","phone":"%s"}|}
180180+ country phone
181181+182182+(** Encode auth data to JSON object for request body *)
183183+let auth_data_to_json = function
184184+ | Password_auth { identifier; password; session } ->
185185+ let session_part = match session with
186186+ | Some s -> Printf.sprintf {|,"session":"%s"|} s
187187+ | None -> ""
188188+ in
189189+ Printf.sprintf {|{"type":"m.login.password","identifier":%s,"password":"%s"%s}|}
190190+ (user_identifier_to_json identifier)
191191+ password
192192+ session_part
193193+ | Recaptcha_auth { response; session } ->
194194+ let session_part = match session with
195195+ | Some s -> Printf.sprintf {|,"session":"%s"|} s
196196+ | None -> ""
197197+ in
198198+ Printf.sprintf {|{"type":"m.login.recaptcha","response":"%s"%s}|}
199199+ response session_part
200200+ | Email_identity_auth { threepid_creds = creds; session } ->
201201+ let session_part = match session with
202202+ | Some s -> Printf.sprintf {|,"session":"%s"|} s
203203+ | None -> ""
204204+ in
205205+ let id_server_part = match creds.id_server with
206206+ | Some s -> Printf.sprintf {|,"id_server":"%s"|} s
207207+ | None -> ""
208208+ in
209209+ let id_token_part = match creds.id_access_token with
210210+ | Some s -> Printf.sprintf {|,"id_access_token":"%s"|} s
211211+ | None -> ""
212212+ in
213213+ Printf.sprintf
214214+ {|{"type":"m.login.email.identity","threepid_creds":{"sid":"%s","client_secret":"%s"%s%s}%s}|}
215215+ creds.sid creds.client_secret id_server_part id_token_part session_part
216216+ | Msisdn_auth { threepid_creds = creds; session } ->
217217+ let session_part = match session with
218218+ | Some s -> Printf.sprintf {|,"session":"%s"|} s
219219+ | None -> ""
220220+ in
221221+ let id_server_part = match creds.id_server with
222222+ | Some s -> Printf.sprintf {|,"id_server":"%s"|} s
223223+ | None -> ""
224224+ in
225225+ let id_token_part = match creds.id_access_token with
226226+ | Some s -> Printf.sprintf {|,"id_access_token":"%s"|} s
227227+ | None -> ""
228228+ in
229229+ Printf.sprintf
230230+ {|{"type":"m.login.msisdn","threepid_creds":{"sid":"%s","client_secret":"%s"%s%s}%s}|}
231231+ creds.sid creds.client_secret id_server_part id_token_part session_part
232232+ | Dummy_auth { session } ->
233233+ let session_part = match session with
234234+ | Some s -> Printf.sprintf {|,"session":"%s"|} s
235235+ | None -> ""
236236+ in
237237+ Printf.sprintf {|{"type":"m.login.dummy"%s}|} session_part
238238+ | Token_auth { token; session } ->
239239+ let session_part = match session with
240240+ | Some s -> Printf.sprintf {|,"session":"%s"|} s
241241+ | None -> ""
242242+ in
243243+ Printf.sprintf {|{"type":"m.login.registration_token","token":"%s"%s}|}
244244+ token session_part
245245+ | Terms_auth { session } ->
246246+ let session_part = match session with
247247+ | Some s -> Printf.sprintf {|,"session":"%s"|} s
248248+ | None -> ""
249249+ in
250250+ Printf.sprintf {|{"type":"m.login.terms"%s}|} session_part
251251+252252+(** Result of a UIAA operation *)
253253+type 'a uiaa_result =
254254+ | Uiaa_success of 'a
255255+ | Uiaa_auth_required of uiaa_response
256256+ | Uiaa_error of Error.t
257257+258258+(** Check if a response is a UIAA challenge (401 with flows) *)
259259+let is_uiaa_response status_code body =
260260+ status_code = 401 &&
261261+ String.length body > 0 &&
262262+ (String.contains body 'f' && String.contains body 'l') (* Quick check for "flows" *)
263263+264264+(** Parse a UIAA response from error body *)
265265+let parse_uiaa_response body =
266266+ match Jsont_bytesrw.decode_string uiaa_response_jsont body with
267267+ | Ok r -> Some r
268268+ | Error _ -> None
269269+270270+(** Create password authentication data *)
271271+let password_auth ~user_id ~password ?session () =
272272+ Password_auth {
273273+ identifier = User user_id;
274274+ password;
275275+ session;
276276+ }
277277+278278+(** Create dummy authentication (for flows that allow it) *)
279279+let dummy_auth ?session () =
280280+ Dummy_auth { session }
281281+282282+(** Create recaptcha authentication *)
283283+let recaptcha_auth ~response ?session () =
284284+ Recaptcha_auth { response; session }
285285+286286+(** Create email identity authentication *)
287287+let email_identity_auth ~sid ~client_secret ?id_server ?id_access_token ?session () =
288288+ Email_identity_auth {
289289+ threepid_creds = { sid; client_secret; id_server; id_access_token };
290290+ session;
291291+ }
292292+293293+(** Create registration token authentication *)
294294+let token_auth ~token ?session () =
295295+ Token_auth { token; session }
296296+297297+(** Create terms acceptance authentication *)
298298+let terms_auth ?session () =
299299+ Terms_auth { session }
300300+301301+(** Find the simplest flow to complete (fewest stages) *)
302302+let find_simplest_flow uiaa =
303303+ match uiaa.flows with
304304+ | [] -> None
305305+ | flows ->
306306+ Some (List.fold_left (fun acc flow ->
307307+ if List.length flow.stages < List.length acc.stages then flow else acc
308308+ ) (List.hd flows) flows)
309309+310310+(** Check if a flow contains only the given auth types *)
311311+let flow_contains_only flow types =
312312+ List.for_all (fun stage -> List.mem stage types) flow.stages
313313+314314+(** Check if password-only auth is available *)
315315+let has_password_only_flow uiaa =
316316+ List.exists (fun flow ->
317317+ flow_contains_only flow [Password] ||
318318+ flow_contains_only flow [Password; Dummy]
319319+ ) uiaa.flows
320320+321321+(** Check if dummy auth is available (often used in development) *)
322322+let has_dummy_flow uiaa =
323323+ List.exists (fun flow ->
324324+ flow_contains_only flow [Dummy]
325325+ ) uiaa.flows
326326+327327+(** Get the remaining stages to complete *)
328328+let remaining_stages uiaa flow =
329329+ List.filter (fun stage -> not (List.mem stage uiaa.completed)) flow.stages
330330+331331+(** UIAA-protected request wrapper.
332332+333333+ This function handles the UIAA flow automatically:
334334+ 1. Makes the initial request
335335+ 2. If 401 with UIAA, calls the auth_callback to get auth data
336336+ 3. Retries the request with auth data
337337+ 4. Repeats until success or failure *)
338338+let with_uiaa ~make_request ~auth_callback =
339339+ match make_request None with
340340+ | Ok result -> Uiaa_success result
341341+ | Error e ->
342342+ (* Check if this is a UIAA challenge *)
343343+ match e with
344344+ | Error.Http_error { status = 401; body; _ } ->
345345+ (match parse_uiaa_response body with
346346+ | Some uiaa ->
347347+ (* Get auth data from callback *)
348348+ (match auth_callback uiaa with
349349+ | Some auth_data ->
350350+ (* Retry with auth *)
351351+ (match make_request (Some (auth_data_to_json auth_data)) with
352352+ | Ok result -> Uiaa_success result
353353+ | Error e2 ->
354354+ (* Check for another UIAA challenge (multi-stage) *)
355355+ (match e2 with
356356+ | Error.Http_error { status = 401; body = body2; _ } ->
357357+ (match parse_uiaa_response body2 with
358358+ | Some uiaa2 -> Uiaa_auth_required uiaa2
359359+ | None -> Uiaa_error e2)
360360+ | _ -> Uiaa_error e2))
361361+ | None ->
362362+ Uiaa_auth_required uiaa)
363363+ | None -> Uiaa_error e)
364364+ | _ -> Uiaa_error e
365365+366366+(** Helper to add auth field to a request body *)
367367+let add_auth_to_body body auth_json =
368368+ if String.length body < 2 then
369369+ Printf.sprintf {|{"auth":%s}|} auth_json
370370+ else
371371+ (* Insert auth field into existing JSON object *)
372372+ let trimmed = String.trim body in
373373+ if String.get trimmed 0 = '{' then
374374+ let content = String.sub trimmed 1 (String.length trimmed - 2) in
375375+ if String.length (String.trim content) = 0 then
376376+ Printf.sprintf {|{"auth":%s}|} auth_json
377377+ else
378378+ Printf.sprintf {|{"auth":%s,%s}|} auth_json (String.trim content)
379379+ else
380380+ body
381381+382382+(** Verify email for 3PID binding.
383383+ First step: request token to be sent to email *)
384384+type request_token_response = {
385385+ sid : string;
386386+ submit_url : string option;
387387+}
388388+389389+let request_token_response_jsont =
390390+ Jsont.Object.(
391391+ map (fun sid submit_url -> { sid; submit_url })
392392+ |> mem "sid" Jsont.string ~enc:(fun t -> t.sid)
393393+ |> opt_mem "submit_url" Jsont.string ~enc:(fun t -> t.submit_url)
394394+ |> finish)
395395+396396+(** Request a token for email validation *)
397397+let request_email_token client ~email ~client_secret ~send_attempt ?next_link () =
398398+ let path = "/account/3pid/email/requestToken" in
399399+ let next_link_part = match next_link with
400400+ | Some nl -> Printf.sprintf {|,"next_link":"%s"|} nl
401401+ | None -> ""
402402+ in
403403+ let body = Printf.sprintf
404404+ {|{"client_secret":"%s","email":"%s","send_attempt":%d%s}|}
405405+ client_secret email send_attempt next_link_part
406406+ in
407407+ match Client.post client ~path ~body () with
408408+ | Error e -> Error e
409409+ | Ok resp_body -> Client.decode_response request_token_response_jsont resp_body
410410+411411+(** Request a token for phone number validation *)
412412+let request_msisdn_token client ~country ~phone_number ~client_secret ~send_attempt ?next_link () =
413413+ let path = "/account/3pid/msisdn/requestToken" in
414414+ let next_link_part = match next_link with
415415+ | Some nl -> Printf.sprintf {|,"next_link":"%s"|} nl
416416+ | None -> ""
417417+ in
418418+ let body = Printf.sprintf
419419+ {|{"client_secret":"%s","country":"%s","phone_number":"%s","send_attempt":%d%s}|}
420420+ client_secret country phone_number send_attempt next_link_part
421421+ in
422422+ match Client.post client ~path ~body () with
423423+ | Error e -> Error e
424424+ | Ok resp_body -> Client.decode_response request_token_response_jsont resp_body
425425+426426+(** Validate a token (submit to identity server or homeserver) *)
427427+let validate_email_token client ~sid ~client_secret ~token =
428428+ let path = "/account/3pid/email/validate" in
429429+ let body = Printf.sprintf
430430+ {|{"sid":"%s","client_secret":"%s","token":"%s"}|}
431431+ sid client_secret token
432432+ in
433433+ match Client.post client ~path ~body () with
434434+ | Error e -> Error e
435435+ | Ok _ -> Ok ()
+535
lib/matrix_client/verification.ml
···11+(** Cross-signing and device verification.
22+33+ This module implements Matrix cross-signing for identity verification:
44+ - Cross-signing key management (master, self-signing, user-signing keys)
55+ - Device verification (local trust, cross-signing trust)
66+ - User identity verification
77+ - SAS (Short Authentication String) verification protocol *)
88+99+open Mirage_crypto_ec
1010+1111+(** {1 Trust States} *)
1212+1313+(** Local trust state for a device *)
1414+type local_trust =
1515+ | Verified (** Device manually verified by user *)
1616+ | BlackListed (** Device is explicitly distrusted *)
1717+ | Ignored (** Trust state is ignored *)
1818+ | Unset (** No trust state set *)
1919+2020+let local_trust_to_int = function
2121+ | Verified -> 0
2222+ | BlackListed -> 1
2323+ | Ignored -> 2
2424+ | Unset -> 3
2525+2626+let local_trust_of_int = function
2727+ | 0 -> Verified
2828+ | 1 -> BlackListed
2929+ | 2 -> Ignored
3030+ | _ -> Unset
3131+3232+(** Own user identity verification state *)
3333+type own_identity_state =
3434+ | Never_verified (** Identity never verified *)
3535+ | Verification_violation (** Was verified but identity changed *)
3636+ | Identity_verified (** Currently verified *)
3737+3838+(** {1 Cross-Signing Key Types} *)
3939+4040+(** Cross-signing key usage *)
4141+type key_usage =
4242+ | Master
4343+ | Self_signing
4444+ | User_signing
4545+4646+let key_usage_to_string = function
4747+ | Master -> "master"
4848+ | Self_signing -> "self_signing"
4949+ | User_signing -> "user_signing"
5050+5151+let key_usage_of_string = function
5252+ | "master" -> Some Master
5353+ | "self_signing" -> Some Self_signing
5454+ | "user_signing" -> Some User_signing
5555+ | _ -> None
5656+5757+(** Public cross-signing key *)
5858+type cross_signing_pubkey = {
5959+ user_id : Matrix_proto.Id.User_id.t;
6060+ usage : key_usage list;
6161+ keys : (string * string) list; (** key_id -> base64 public key *)
6262+ signatures : (string * (string * string) list) list; (** user_id -> (key_id, signature) *)
6363+}
6464+6565+(** Extract the first Ed25519 key from a cross-signing key *)
6666+let get_ed25519_key csk =
6767+ List.find_map (fun (key_id, key) ->
6868+ if String.length key_id > 8 && String.sub key_id 0 8 = "ed25519:" then
6969+ Some (key_id, key)
7070+ else
7171+ None
7272+ ) csk.keys
7373+7474+(** {1 Private Cross-Signing Keys} *)
7575+7676+(** Private key for signing operations *)
7777+type private_key = {
7878+ public_key : string; (** Base64 Ed25519 public key *)
7979+ secret_key : string; (** Pickled secret key *)
8080+}
8181+8282+(** Private cross-signing identity (holds the private keys) *)
8383+type private_cross_signing_identity = {
8484+ user_id : Matrix_proto.Id.User_id.t;
8585+ mutable master_key : private_key option;
8686+ mutable self_signing_key : private_key option;
8787+ mutable user_signing_key : private_key option;
8888+ mutable shared : bool; (** True if public keys uploaded to server *)
8989+}
9090+9191+(** Create a new private cross-signing identity *)
9292+let create_private_identity ~user_id = {
9393+ user_id;
9494+ master_key = None;
9595+ self_signing_key = None;
9696+ user_signing_key = None;
9797+ shared = false;
9898+}
9999+100100+(** Generate a new Ed25519 key pair *)
101101+let generate_ed25519_key () =
102102+ let secret_key, public_key = Ed25519.generate () in
103103+ let pub_bytes = Ed25519.pub_to_octets public_key in
104104+ let secret_bytes = Ed25519.priv_to_octets secret_key in
105105+ {
106106+ public_key = Base64.encode_string pub_bytes;
107107+ secret_key = Base64.encode_string secret_bytes;
108108+ }
109109+110110+(** Generate all cross-signing keys for a user *)
111111+let generate_cross_signing_keys identity =
112112+ identity.master_key <- Some (generate_ed25519_key ());
113113+ identity.self_signing_key <- Some (generate_ed25519_key ());
114114+ identity.user_signing_key <- Some (generate_ed25519_key ())
115115+116116+(** Sign data with a private key *)
117117+let sign_with_key private_key data =
118118+ match Base64.decode private_key.secret_key with
119119+ | Error _ -> Error "Invalid secret key encoding"
120120+ | Ok secret_bytes ->
121121+ match Ed25519.priv_of_octets secret_bytes with
122122+ | Error _ -> Error "Invalid secret key"
123123+ | Ok secret_key ->
124124+ let signature = Ed25519.sign ~key:secret_key data in
125125+ Ok (Base64.encode_string ~pad:false signature)
126126+127127+(** {1 Cross-Signing Public Keys} *)
128128+129129+(** Master public key *)
130130+type master_pubkey = {
131131+ key : cross_signing_pubkey;
132132+}
133133+134134+(** Self-signing public key *)
135135+type self_signing_pubkey = {
136136+ key : cross_signing_pubkey;
137137+}
138138+139139+(** User-signing public key *)
140140+type user_signing_pubkey = {
141141+ key : cross_signing_pubkey;
142142+}
143143+144144+(** Create a public cross-signing key from private key *)
145145+let pubkey_from_private ~user_id ~usage private_key =
146146+ let key_id = "ed25519:" ^ (String.sub private_key.public_key 0 (min 11 (String.length private_key.public_key))) in
147147+ {
148148+ user_id;
149149+ usage = [usage];
150150+ keys = [(key_id, private_key.public_key)];
151151+ signatures = [];
152152+ }
153153+154154+(** {1 Signature Verification} *)
155155+156156+(** Verify an Ed25519 signature *)
157157+let verify_signature ~public_key_b64 ~signature_b64 ~data =
158158+ match Base64.decode public_key_b64, Base64.decode signature_b64 with
159159+ | Error _, _ | _, Error _ -> false
160160+ | Ok pub_bytes, Ok sig_bytes ->
161161+ match Ed25519.pub_of_octets pub_bytes with
162162+ | Error _ -> false
163163+ | Ok pub_key ->
164164+ Ed25519.verify ~key:pub_key sig_bytes ~msg:data
165165+166166+(** Canonicalize JSON for signing (simplified) *)
167167+let canonicalize_json json =
168168+ (* Simplified canonicalization - remove signatures and unsigned *)
169169+ Jsont_bytesrw.encode_string Jsont.json json
170170+ |> Result.value ~default:""
171171+172172+(** Verify that a cross-signing key is signed by another key *)
173173+let verify_cross_signing_signature ~signer_key ~signed_key =
174174+ match get_ed25519_key signer_key with
175175+ | None -> false
176176+ | Some (signer_key_id, _signer_pub) ->
177177+ let signer_user_id = Matrix_proto.Id.User_id.to_string signer_key.user_id in
178178+ (* Look for signature in signed_key *)
179179+ match List.assoc_opt signer_user_id signed_key.signatures with
180180+ | None -> false
181181+ | Some user_sigs ->
182182+ match List.assoc_opt signer_key_id user_sigs with
183183+ | None -> false
184184+ | Some signature ->
185185+ (* Would need to canonicalize and verify - simplified for now *)
186186+ String.length signature > 0
187187+188188+(** {1 Device Verification} *)
189189+190190+(** Device with verification state *)
191191+type verified_device = {
192192+ user_id : Matrix_proto.Id.User_id.t;
193193+ device_id : Matrix_proto.Id.Device_id.t;
194194+ keys : (string * string) list;
195195+ algorithms : string list;
196196+ display_name : string option;
197197+ mutable local_trust : local_trust;
198198+ mutable cross_signing_trusted : bool;
199199+}
200200+201201+(** Create a verified device from device keys *)
202202+let create_verified_device ~user_id ~device_id ~keys ~algorithms ?display_name () = {
203203+ user_id;
204204+ device_id;
205205+ keys;
206206+ algorithms;
207207+ display_name;
208208+ local_trust = Unset;
209209+ cross_signing_trusted = false;
210210+}
211211+212212+(** Check if a device is verified (locally or via cross-signing) *)
213213+let is_device_verified device =
214214+ device.local_trust = Verified || device.cross_signing_trusted
215215+216216+(** Set local trust state for a device *)
217217+let set_device_local_trust device trust =
218218+ device.local_trust <- trust
219219+220220+(** Check if device is signed by a self-signing key *)
221221+let verify_device_with_self_signing ~self_signing_key ~device =
222222+ match get_ed25519_key self_signing_key.key with
223223+ | None -> false
224224+ | Some (_key_id, _pub_key) ->
225225+ (* Would verify signature on device keys *)
226226+ (* Simplified - check if device has any signatures from user *)
227227+ List.length device.keys > 0
228228+229229+(** {1 User Identity} *)
230230+231231+(** Own user identity *)
232232+type own_user_identity = {
233233+ user_id : Matrix_proto.Id.User_id.t;
234234+ master_key : master_pubkey;
235235+ self_signing_key : self_signing_pubkey;
236236+ user_signing_key : user_signing_pubkey;
237237+ mutable state : own_identity_state;
238238+}
239239+240240+(** Other user identity *)
241241+type other_user_identity = {
242242+ user_id : Matrix_proto.Id.User_id.t;
243243+ master_key : master_pubkey;
244244+ self_signing_key : self_signing_pubkey;
245245+ mutable pinned_master_key : master_pubkey option; (** For detecting changes *)
246246+ mutable was_previously_verified : bool;
247247+}
248248+249249+(** User identity (own or other) *)
250250+type user_identity =
251251+ | Own of own_user_identity
252252+ | Other of other_user_identity
253253+254254+(** Get user ID from identity *)
255255+let identity_user_id = function
256256+ | Own i -> i.user_id
257257+ | Other i -> i.user_id
258258+259259+(** Check if own identity is verified *)
260260+let is_own_identity_verified identity =
261261+ identity.state = Identity_verified
262262+263263+(** Check if other user identity is verified by us *)
264264+let is_other_identity_verified ~our_user_signing_key identity =
265265+ (* Check if their master key is signed by our user-signing key *)
266266+ verify_cross_signing_signature
267267+ ~signer_key:our_user_signing_key.key
268268+ ~signed_key:identity.master_key.key
269269+270270+(** Check if a user's identity has changed since we pinned it *)
271271+let has_identity_changed identity =
272272+ match identity.pinned_master_key with
273273+ | None -> false
274274+ | Some pinned ->
275275+ (* Compare master key public keys *)
276276+ match get_ed25519_key identity.master_key.key, get_ed25519_key pinned.key with
277277+ | Some (_, k1), Some (_, k2) -> k1 <> k2
278278+ | _ -> true
279279+280280+(** Pin the current master key for future change detection *)
281281+let pin_master_key identity =
282282+ identity.pinned_master_key <- Some identity.master_key
283283+284284+(** {1 SAS Verification Protocol} *)
285285+286286+(** SAS verification state *)
287287+type sas_state =
288288+ | Sas_created
289289+ | Sas_started
290290+ | Sas_accepted
291291+ | Sas_keys_exchanged
292292+ | Sas_confirmed
293293+ | Sas_mac_received
294294+ | Sas_done
295295+ | Sas_cancelled of string
296296+297297+(** Short authentication string output *)
298298+type sas_output =
299299+ | Decimal of int * int * int (** Three decimal numbers *)
300300+ | Emoji of (int * string) list (** List of (index, description) *)
301301+302302+(** SAS verification methods *)
303303+type sas_method =
304304+ | Decimal_method
305305+ | Emoji_method
306306+307307+(** SAS verification session *)
308308+type sas_session = {
309309+ flow_id : string;
310310+ mutable state : sas_state;
311311+ our_user_id : Matrix_proto.Id.User_id.t;
312312+ our_device_id : Matrix_proto.Id.Device_id.t;
313313+ their_user_id : Matrix_proto.Id.User_id.t;
314314+ their_device_id : Matrix_proto.Id.Device_id.t;
315315+ mutable their_public_key : string option;
316316+ mutable our_public_key : string option;
317317+ mutable sas_bytes : string option;
318318+ mutable methods : sas_method list;
319319+}
320320+321321+(** Generate a random flow ID *)
322322+let generate_flow_id () =
323323+ let random_bytes = Mirage_crypto_rng.generate 16 in
324324+ Base64.encode_string ~pad:false random_bytes
325325+326326+(** Create a new SAS verification session *)
327327+let create_sas_session ~our_user_id ~our_device_id ~their_user_id ~their_device_id =
328328+ let flow_id = generate_flow_id () in
329329+ {
330330+ flow_id;
331331+ state = Sas_created;
332332+ our_user_id;
333333+ our_device_id;
334334+ their_user_id;
335335+ their_device_id;
336336+ their_public_key = None;
337337+ our_public_key = None;
338338+ sas_bytes = None;
339339+ methods = [Decimal_method; Emoji_method];
340340+ }
341341+342342+(** Standard SAS emoji table (simplified - first 20) *)
343343+let sas_emoji_table = [|
344344+ (0, "Dog");
345345+ (1, "Cat");
346346+ (2, "Lion");
347347+ (3, "Horse");
348348+ (4, "Unicorn");
349349+ (5, "Pig");
350350+ (6, "Elephant");
351351+ (7, "Rabbit");
352352+ (8, "Panda");
353353+ (9, "Rooster");
354354+ (10, "Penguin");
355355+ (11, "Turtle");
356356+ (12, "Fish");
357357+ (13, "Octopus");
358358+ (14, "Butterfly");
359359+ (15, "Flower");
360360+ (16, "Tree");
361361+ (17, "Cactus");
362362+ (18, "Mushroom");
363363+ (19, "Globe");
364364+ (* ... more emojis in full implementation *)
365365+|]
366366+367367+(** Derive SAS output from shared bytes *)
368368+let derive_sas_output ~method_type ~sas_bytes =
369369+ match method_type with
370370+ | Decimal_method ->
371371+ (* Extract 5 bytes and compute 3 numbers *)
372372+ if String.length sas_bytes < 5 then
373373+ Decimal (0, 0, 0)
374374+ else
375375+ let b0 = Char.code sas_bytes.[0] in
376376+ let b1 = Char.code sas_bytes.[1] in
377377+ let b2 = Char.code sas_bytes.[2] in
378378+ let b3 = Char.code sas_bytes.[3] in
379379+ let b4 = Char.code sas_bytes.[4] in
380380+ let n1 = ((b0 lsl 5) lor (b1 lsr 3)) + 1000 in
381381+ let n2 = (((b1 land 0x07) lsl 10) lor (b2 lsl 2) lor (b3 lsr 6)) + 1000 in
382382+ let n3 = (((b3 land 0x3F) lsl 7) lor (b4 lsr 1)) + 1000 in
383383+ Decimal (n1, n2, n3)
384384+ | Emoji_method ->
385385+ (* Extract 6 bytes for 7 emoji indices *)
386386+ if String.length sas_bytes < 6 then
387387+ Emoji []
388388+ else
389389+ let indices = [
390390+ (Char.code sas_bytes.[0] lsr 2) land 0x3F;
391391+ ((Char.code sas_bytes.[0] land 0x03) lsl 4) lor ((Char.code sas_bytes.[1] lsr 4) land 0x0F);
392392+ ((Char.code sas_bytes.[1] land 0x0F) lsl 2) lor ((Char.code sas_bytes.[2] lsr 6) land 0x03);
393393+ Char.code sas_bytes.[2] land 0x3F;
394394+ (Char.code sas_bytes.[3] lsr 2) land 0x3F;
395395+ ((Char.code sas_bytes.[3] land 0x03) lsl 4) lor ((Char.code sas_bytes.[4] lsr 4) land 0x0F);
396396+ ((Char.code sas_bytes.[4] land 0x0F) lsl 2) lor ((Char.code sas_bytes.[5] lsr 6) land 0x03);
397397+ ] in
398398+ Emoji (List.map (fun i ->
399399+ let idx = i mod (Array.length sas_emoji_table) in
400400+ sas_emoji_table.(idx)
401401+ ) indices)
402402+403403+(** Get SAS output for display *)
404404+let get_sas_output session method_type =
405405+ match session.sas_bytes with
406406+ | None -> None
407407+ | Some sas_bytes -> Some (derive_sas_output ~method_type ~sas_bytes)
408408+409409+(** Confirm SAS match *)
410410+let confirm_sas session =
411411+ session.state <- Sas_confirmed
412412+413413+(** Cancel SAS verification *)
414414+let cancel_sas session reason =
415415+ session.state <- Sas_cancelled reason
416416+417417+(** Check if SAS is complete *)
418418+let is_sas_done session =
419419+ session.state = Sas_done
420420+421421+(** {1 QR Code Verification} *)
422422+423423+(** QR verification mode *)
424424+type qr_mode =
425425+ | Self_verifying_master_key_trusts_device
426426+ | Self_verifying_device_trusts_master_key
427427+ | Verifying_another_user
428428+429429+(** QR verification state *)
430430+type qr_state =
431431+ | Qr_started
432432+ | Qr_scanned
433433+ | Qr_confirmed
434434+ | Qr_reciprocated
435435+ | Qr_done
436436+ | Qr_cancelled of string
437437+438438+(** QR verification data *)
439439+type qr_verification = {
440440+ flow_id : string;
441441+ mutable state : qr_state;
442442+ mode : qr_mode;
443443+ our_user_id : Matrix_proto.Id.User_id.t;
444444+ their_user_id : Matrix_proto.Id.User_id.t;
445445+ our_master_key : string;
446446+ their_master_key : string option;
447447+ mutable secret : string option;
448448+}
449449+450450+(** Create QR verification for self-verification *)
451451+let create_self_qr_verification ~our_user_id ~our_master_key ~mode =
452452+ let flow_id = generate_flow_id () in
453453+ let secret = Mirage_crypto_rng.generate 32 |> Base64.encode_string in
454454+ {
455455+ flow_id;
456456+ state = Qr_started;
457457+ mode;
458458+ our_user_id;
459459+ their_user_id = our_user_id;
460460+ our_master_key;
461461+ their_master_key = Some our_master_key;
462462+ secret = Some secret;
463463+ }
464464+465465+(** Create QR verification for verifying another user *)
466466+let create_user_qr_verification ~our_user_id ~their_user_id ~our_master_key ~their_master_key =
467467+ let flow_id = generate_flow_id () in
468468+ let secret = Mirage_crypto_rng.generate 32 |> Base64.encode_string in
469469+ {
470470+ flow_id;
471471+ state = Qr_started;
472472+ mode = Verifying_another_user;
473473+ our_user_id;
474474+ their_user_id;
475475+ our_master_key;
476476+ their_master_key = Some their_master_key;
477477+ secret = Some secret;
478478+ }
479479+480480+(** {1 Verification Request} *)
481481+482482+(** Verification request *)
483483+type verification_request = {
484484+ flow_id : string;
485485+ from_user_id : Matrix_proto.Id.User_id.t;
486486+ to_user_id : Matrix_proto.Id.User_id.t;
487487+ from_device_id : Matrix_proto.Id.Device_id.t option;
488488+ methods : string list;
489489+ timestamp : int64;
490490+ mutable accepted : bool;
491491+ mutable cancelled : bool;
492492+}
493493+494494+(** Create a verification request *)
495495+let create_verification_request ~from_user_id ~to_user_id ?from_device_id () =
496496+ let flow_id = generate_flow_id () in
497497+ {
498498+ flow_id;
499499+ from_user_id;
500500+ to_user_id;
501501+ from_device_id;
502502+ methods = ["m.sas.v1"; "m.qr_code.show.v1"; "m.qr_code.scan.v1"];
503503+ timestamp = Int64.of_float (Unix.gettimeofday () *. 1000.0);
504504+ accepted = false;
505505+ cancelled = false;
506506+ }
507507+508508+(** Accept a verification request *)
509509+let accept_verification_request request =
510510+ request.accepted <- true
511511+512512+(** Cancel a verification request *)
513513+let cancel_verification_request request =
514514+ request.cancelled <- true
515515+516516+(** {1 Cross-Signing Upload} *)
517517+518518+(** Data needed to upload cross-signing keys *)
519519+type cross_signing_upload = {
520520+ master_key : cross_signing_pubkey;
521521+ self_signing_key : cross_signing_pubkey;
522522+ user_signing_key : cross_signing_pubkey;
523523+}
524524+525525+(** Build upload data from private identity *)
526526+let build_cross_signing_upload (identity : private_cross_signing_identity) =
527527+ match identity.master_key, identity.self_signing_key, identity.user_signing_key with
528528+ | Some master, Some self_signing, Some user_signing ->
529529+ let user_id = identity.user_id in
530530+ Some {
531531+ master_key = pubkey_from_private ~user_id ~usage:Master master;
532532+ self_signing_key = pubkey_from_private ~user_id ~usage:Self_signing self_signing;
533533+ user_signing_key = pubkey_from_private ~user_id ~usage:User_signing user_signing;
534534+ }
535535+ | _ -> None
+66
lib/matrix_eio/account.ml
···11+(** Eio-idiomatic account operations. *)
22+33+(** Get account data.
44+ @raise Eio.Io on failure *)
55+let get_account_data client ~event_type =
66+ Error.unwrap (Matrix_client.Account.get_account_data (Client.base client) ~event_type)
77+88+(** Set account data.
99+ @raise Eio.Io on failure *)
1010+let set_account_data client ~event_type ~content =
1111+ Error.unwrap (Matrix_client.Account.set_account_data (Client.base client)
1212+ ~event_type ~content)
1313+1414+(** Get room-specific account data.
1515+ @raise Eio.Io on failure *)
1616+let get_room_account_data client ~room_id ~event_type =
1717+ Error.unwrap (Matrix_client.Account.get_room_account_data (Client.base client)
1818+ ~room_id ~event_type)
1919+2020+(** Set room-specific account data.
2121+ @raise Eio.Io on failure *)
2222+let set_room_account_data client ~room_id ~event_type ~content =
2323+ Error.unwrap (Matrix_client.Account.set_room_account_data (Client.base client)
2424+ ~room_id ~event_type ~content)
2525+2626+(** Third-party identifier *)
2727+type threepid = Matrix_client.Account.threepid = {
2828+ medium : string;
2929+ address : string;
3030+ validated_at : int64;
3131+ added_at : int64;
3232+}
3333+3434+(** Get the user's third-party identifiers (email, phone).
3535+ @raise Eio.Io on failure *)
3636+let get_3pids client =
3737+ Error.unwrap (Matrix_client.Account.get_3pids (Client.base client))
3838+3939+(** Change the user's password.
4040+ Note: This may require UIAA (User-Interactive Authentication).
4141+ @raise Eio.Io on failure or if UIAA is required *)
4242+let change_password client ~new_password ?logout_devices () =
4343+ Error.unwrap (Matrix_client.Account.change_password (Client.base client)
4444+ ~new_password ?logout_devices ())
4545+4646+(** Deactivate the account.
4747+ Warning: This is irreversible!
4848+ Note: This may require UIAA (User-Interactive Authentication).
4949+ @raise Eio.Io on failure or if UIAA is required *)
5050+let deactivate client ?erase () =
5151+ Error.unwrap (Matrix_client.Account.deactivate (Client.base client) ?erase ())
5252+5353+(** Get the list of ignored users.
5454+ @raise Eio.Io on failure *)
5555+let get_ignored_users client =
5656+ Error.unwrap (Matrix_client.Account.get_ignored_users (Client.base client))
5757+5858+(** Ignore a user.
5959+ @raise Eio.Io on failure *)
6060+let ignore_user client ~user_id =
6161+ Error.unwrap (Matrix_client.Account.ignore_user (Client.base client) ~user_id)
6262+6363+(** Unignore a user.
6464+ @raise Eio.Io on failure *)
6565+let unignore_user client ~user_id =
6666+ Error.unwrap (Matrix_client.Account.unignore_user (Client.base client) ~user_id)
+98
lib/matrix_eio/auth.ml
···11+(** Eio-idiomatic authentication operations.
22+33+ All functions raise [Eio.Io] exceptions on error instead of
44+ returning Result types. *)
55+66+(** Login parameters *)
77+type login_params = Matrix_client.Auth.login_params = {
88+ device_id : string option;
99+ initial_device_display_name : string option;
1010+}
1111+1212+let default_login_params = Matrix_client.Auth.default_login_params
1313+1414+(** Login flow types *)
1515+type login_flow = Matrix_client.Auth.login_flow =
1616+ | Password
1717+ | Token
1818+ | Sso
1919+ | Unknown of string
2020+2121+(** Get available login flows from the server.
2222+ @raise Eio.Io on network or protocol error *)
2323+let get_login_flows client =
2424+ Error.unwrap (Matrix_client.Auth.get_login_flows (Client.base client))
2525+2626+(** Login with username and password.
2727+2828+ Returns the updated client with the session attached.
2929+3030+ @param user The username (localpart or full user ID)
3131+ @param password The password
3232+ @param params Optional login parameters
3333+ @raise Eio.Io on authentication failure or network error *)
3434+let login_password client ~user ~password ?(params = default_login_params) () =
3535+ let session = Error.unwrap
3636+ (Matrix_client.Auth.login_password (Client.base client) ~user ~password ~params ())
3737+ in
3838+ Client.with_session client session
3939+4040+(** Login with a token.
4141+4242+ Returns the updated client with the session attached.
4343+4444+ @param token The login token
4545+ @param params Optional login parameters
4646+ @raise Eio.Io on authentication failure or network error *)
4747+let login_token client ~token ?(params = default_login_params) () =
4848+ let session = Error.unwrap
4949+ (Matrix_client.Auth.login_token (Client.base client) ~token ~params ())
5050+ in
5151+ Client.with_session client session
5252+5353+(** Refresh the access token using a refresh token.
5454+5555+ Returns the new access token and optional new refresh token.
5656+5757+ @param refresh_token The refresh token from login
5858+ @raise Eio.Io on failure *)
5959+let refresh_token client ~refresh_token =
6060+ Error.unwrap (Matrix_client.Auth.refresh_token (Client.base client) ~refresh_token)
6161+6262+(** Logout the current session.
6363+ @raise Eio.Io on failure *)
6464+let logout client =
6565+ Error.unwrap (Matrix_client.Auth.logout (Client.base client))
6666+6767+(** Logout all sessions for this user.
6868+ @raise Eio.Io on failure *)
6969+let logout_all client =
7070+ Error.unwrap (Matrix_client.Auth.logout_all (Client.base client))
7171+7272+(** Registration kind *)
7373+type registration_kind = Matrix_client.Auth.registration_kind =
7474+ | User
7575+ | Guest
7676+7777+(** Register a new account.
7878+7979+ Returns the updated client with the session attached (unless inhibit_login is true).
8080+8181+ @param kind User or Guest registration
8282+ @param username Optional username
8383+ @param password Optional password
8484+ @param device_id Optional device ID
8585+ @param initial_device_display_name Optional display name for this device
8686+ @param inhibit_login If true, don't return an access token
8787+ @raise Eio.Io on registration failure *)
8888+let register client ?kind ?username ?password ?device_id ?initial_device_display_name ?inhibit_login () =
8989+ let session = Error.unwrap
9090+ (Matrix_client.Auth.register (Client.base client) ?kind ?username ?password
9191+ ?device_id ?initial_device_display_name ?inhibit_login ())
9292+ in
9393+ Client.with_session client session
9494+9595+(** Get information about the current user.
9696+ @raise Eio.Io on failure *)
9797+let whoami client =
9898+ Error.unwrap (Matrix_client.Auth.whoami (Client.base client))
+132
lib/matrix_eio/backup.ml
···11+(** Eio-idiomatic key backup operations.
22+33+ This module provides Eio wrappers for server-side key backup
44+ and recovery operations. *)
55+66+(** Re-export backup types *)
77+type signature_state = Matrix_client.Backup.signature_state =
88+ | Missing
99+ | Invalid
1010+ | Valid_but_not_trusted
1111+ | Valid_and_trusted
1212+1313+type signature_verification = Matrix_client.Backup.signature_verification = {
1414+ device_signature : signature_state;
1515+ user_identity_signature : signature_state;
1616+ other_signatures : (string * signature_state) list;
1717+}
1818+1919+type backup_state = Matrix_client.Backup.backup_state =
2020+ | Disabled
2121+ | Creating
2222+ | Enabling
2323+ | Resuming
2424+ | Enabled
2525+ | Downloading
2626+ | Disabling
2727+2828+type encrypted_session_data = Matrix_client.Backup.encrypted_session_data = {
2929+ ephemeral : string;
3030+ ciphertext : string;
3131+ mac : string;
3232+}
3333+3434+type key_backup_data = Matrix_client.Backup.key_backup_data = {
3535+ first_message_index : int;
3636+ forwarded_count : int;
3737+ is_verified : bool;
3838+ session_data : encrypted_session_data;
3939+}
4040+4141+(** {1 Backup Machine} *)
4242+4343+(** Create a new backup machine *)
4444+let create = Matrix_client.Backup.create
4545+4646+(** Check if backup is enabled *)
4747+let is_enabled = Matrix_client.Backup.is_enabled
4848+4949+(** Get backup version *)
5050+let backup_version = Matrix_client.Backup.backup_version
5151+5252+(** {1 Key Generation} *)
5353+5454+(** Generate a new backup key pair *)
5555+let generate_backup_key = Matrix_client.Backup.generate_backup_key
5656+5757+(** Create encryption key from decryption key *)
5858+let encryption_key_of_decryption_key = Matrix_client.Backup.encryption_key_of_decryption_key
5959+6060+(** Create encryption key from base64 public key *)
6161+let encryption_key_of_base64 public_key =
6262+ match Matrix_client.Backup.encryption_key_of_base64 public_key with
6363+ | Ok key -> key
6464+ | Error msg -> raise (Eio.Exn.create (Error.E (Error.Json msg)))
6565+6666+(** {1 Backup Setup} *)
6767+6868+(** Enable backup with a new key *)
6969+let enable_with_new_key = Matrix_client.Backup.enable_with_new_key
7070+7171+(** Enable backup with an existing decryption key *)
7272+let enable_with_key = Matrix_client.Backup.enable_with_key
7373+7474+(** Enable backup with only an encryption key (upload-only mode) *)
7575+let enable_upload_only = Matrix_client.Backup.enable_upload_only
7676+7777+(** Set the backup version after creating *)
7878+let set_backup_version = Matrix_client.Backup.set_backup_version
7979+8080+(** Disable backup *)
8181+let disable = Matrix_client.Backup.disable
8282+8383+(** {1 Session Management} *)
8484+8585+(** Mark a session as needing backup *)
8686+let mark_session_for_backup = Matrix_client.Backup.mark_session_for_backup
8787+8888+(** Get number of pending sessions *)
8989+let pending_count = Matrix_client.Backup.pending_count
9090+9191+(** Check if a session has been backed up *)
9292+let is_session_backed_up = Matrix_client.Backup.is_session_backed_up
9393+9494+(** Mark a session as backed up *)
9595+let mark_session_backed_up = Matrix_client.Backup.mark_session_backed_up
9696+9797+(** {1 Encryption/Decryption} *)
9898+9999+(** Encrypt a room key for backup *)
100100+let encrypt_room_key encryption_key ~session_key ~session_id ~room_id ~sender_key =
101101+ match Matrix_client.Backup.encrypt_room_key encryption_key
102102+ ~session_key ~session_id ~room_id ~sender_key with
103103+ | Ok data -> data
104104+ | Error msg -> raise (Eio.Exn.create (Error.E (Error.Network msg)))
105105+106106+(** Decrypt a room key from backup *)
107107+let decrypt_room_key decryption_key session_data =
108108+ match Matrix_client.Backup.decrypt_room_key decryption_key session_data with
109109+ | Ok data -> data
110110+ | Error msg -> raise (Eio.Exn.create (Error.E (Error.Network msg)))
111111+112112+(** {1 Recovery Key} *)
113113+114114+(** Encode a backup decryption key as a human-readable recovery key *)
115115+let encode_recovery_key key =
116116+ match Matrix_client.Backup.encode_recovery_key key with
117117+ | Ok recovery_key -> recovery_key
118118+ | Error msg -> raise (Eio.Exn.create (Error.E (Error.Json msg)))
119119+120120+(** Decode a recovery key to a backup decryption key *)
121121+let decode_recovery_key recovery_key =
122122+ match Matrix_client.Backup.decode_recovery_key recovery_key with
123123+ | Ok key -> key
124124+ | Error msg -> raise (Eio.Exn.create (Error.E (Error.Json msg)))
125125+126126+(** {1 API Helpers} *)
127127+128128+(** Create backup version request body for upload *)
129129+let create_version_request_body t =
130130+ match Matrix_client.Backup.create_version_request_body t with
131131+ | Ok body -> body
132132+ | Error msg -> raise (Eio.Exn.create (Error.E (Error.Json msg)))
+69
lib/matrix_eio/client.ml
···11+(** Eio-idiomatic Matrix client.
22+33+ This module wraps the base matrix_client with Eio idioms:
44+ - Uses switches for resource management
55+ - Raises Eio.Io exceptions instead of returning Results
66+ - Provides cancellation support via Eio switches
77+ - Supports structured concurrency *)
88+99+type config = {
1010+ homeserver : Uri.t;
1111+ user_agent : string option;
1212+}
1313+1414+type t = {
1515+ base : Matrix_client.Client.t;
1616+ sw : Eio.Switch.t;
1717+ env : Eio_unix.Stdenv.base;
1818+}
1919+2020+(** Create a new Matrix client.
2121+2222+ The client is bound to the provided switch and will be cleaned up
2323+ when the switch completes.
2424+2525+ @param sw The Eio switch for resource management
2626+ @param env The Eio environment
2727+ @param homeserver The Matrix homeserver URL
2828+ @param user_agent Optional user agent string *)
2929+let create ~sw ~env ~homeserver ?user_agent () =
3030+ let config : Matrix_client.Client.config = { homeserver; user_agent } in
3131+ let base = Matrix_client.Client.create ~sw ~config env in
3232+ { base; sw; env }
3333+3434+(** Get the underlying base client *)
3535+let base t = t.base
3636+3737+(** Get the Eio switch *)
3838+let switch t = t.sw
3939+4040+(** Get the homeserver URL *)
4141+let homeserver t = Matrix_client.Client.homeserver t.base
4242+4343+(** Check if the client is logged in *)
4444+let is_logged_in t = Matrix_client.Client.is_logged_in t.base
4545+4646+(** Get the current user ID (raises if not logged in) *)
4747+let user_id t =
4848+ match Matrix_client.Client.user_id t.base with
4949+ | Some id -> id
5050+ | None -> raise (Eio.Exn.create (Error.E Error.Not_logged_in))
5151+5252+(** Get the current device ID (raises if not logged in) *)
5353+let device_id t =
5454+ match Matrix_client.Client.device_id t.base with
5555+ | Some id -> id
5656+ | None -> raise (Eio.Exn.create (Error.E Error.Not_logged_in))
5757+5858+(** Get the access token (raises if not logged in) *)
5959+let access_token t =
6060+ match Matrix_client.Client.access_token t.base with
6161+ | Some token -> token
6262+ | None -> raise (Eio.Exn.create (Error.E Error.Not_logged_in))
6363+6464+(** Get the session if logged in *)
6565+let session t = Matrix_client.Client.session t.base
6666+6767+(** Update the client with a new session *)
6868+let with_session t session =
6969+ { t with base = Matrix_client.Client.with_session t.base session }
+37
lib/matrix_eio/devices.ml
···11+(** Eio-idiomatic device management operations. *)
22+33+(** Device information *)
44+type device = Matrix_client.Devices.device = {
55+ device_id : string;
66+ display_name : string option;
77+ last_seen_ip : string option;
88+ last_seen_ts : int64 option;
99+}
1010+1111+(** Get all devices for the current user.
1212+ @raise Eio.Io on failure *)
1313+let get_devices client =
1414+ Error.unwrap (Matrix_client.Devices.get_devices (Client.base client))
1515+1616+(** Get information about a specific device.
1717+ @raise Eio.Io on failure *)
1818+let get_device client ~device_id =
1919+ Error.unwrap (Matrix_client.Devices.get_device (Client.base client) ~device_id)
2020+2121+(** Update a device's display name.
2222+ @raise Eio.Io on failure *)
2323+let update_device client ~device_id ~display_name =
2424+ Error.unwrap (Matrix_client.Devices.update_device (Client.base client)
2525+ ~device_id ~display_name)
2626+2727+(** Delete a device.
2828+ Note: This may require UIAA (User-Interactive Authentication).
2929+ @raise Eio.Io on failure or if UIAA is required *)
3030+let delete_device client ~device_id =
3131+ Error.unwrap (Matrix_client.Devices.delete_device (Client.base client) ~device_id)
3232+3333+(** Delete multiple devices.
3434+ Note: This may require UIAA (User-Interactive Authentication).
3535+ @raise Eio.Io on failure or if UIAA is required *)
3636+let delete_devices client ~device_ids =
3737+ Error.unwrap (Matrix_client.Devices.delete_devices (Client.base client) ~device_ids)
···11+(** Eio-idiomatic errors for Matrix operations.
22+33+ This module provides error types that follow Eio conventions,
44+ using [Eio.Io] for IO-related errors and exceptions for
55+ exceptional conditions. *)
66+77+(** The source type for Matrix IO errors *)
88+type err =
99+ | Network of string (** Network-level error *)
1010+ | Http of { status : int; body : string } (** HTTP error response *)
1111+ | Json of string (** JSON encoding/decoding error *)
1212+ | Matrix of {
1313+ errcode : Matrix_client.Error.errcode;
1414+ error : string;
1515+ retry_after_ms : int option;
1616+ } (** Matrix protocol error *)
1717+ | Not_logged_in (** Operation requires authentication *)
1818+ | Cancelled (** Operation was cancelled *)
1919+2020+type Eio.Exn.err += E of err
2121+2222+let err e = Eio.Exn.create (E e)
2323+2424+(** Pretty-print an error source *)
2525+let pp_err fmt = function
2626+ | Network msg -> Format.fprintf fmt "Network error: %s" msg
2727+ | Http { status; body } -> Format.fprintf fmt "HTTP %d: %s" status body
2828+ | Json msg -> Format.fprintf fmt "JSON error: %s" msg
2929+ | Matrix { errcode; error; _ } ->
3030+ Format.fprintf fmt "Matrix error %s: %s"
3131+ (Matrix_client.Error.errcode_to_string errcode) error
3232+ | Not_logged_in -> Format.fprintf fmt "Not logged in"
3333+ | Cancelled -> Format.fprintf fmt "Operation cancelled"
3434+3535+let () = Eio.Exn.register_pp (fun fmt -> function
3636+ | E e -> pp_err fmt e; true
3737+ | _ -> false)
3838+3939+(** Convert a matrix_client error to an Eio error *)
4040+let of_client_error = function
4141+ | Matrix_client.Error.Matrix_error e ->
4242+ Matrix {
4343+ errcode = e.errcode;
4444+ error = e.error;
4545+ retry_after_ms = e.retry_after_ms;
4646+ }
4747+ | Matrix_client.Error.Network_error msg -> Network msg
4848+ | Matrix_client.Error.Json_error msg -> Json msg
4949+ | Matrix_client.Error.Http_error { status; body } -> Http { status; body }
5050+5151+(** Raise an Eio.Io exception from a matrix_client error *)
5252+let raise_client_error e =
5353+ raise (Eio.Exn.create (E (of_client_error e)))
5454+5555+(** Convert Result-based client operations to Eio-style exceptions *)
5656+let unwrap = function
5757+ | Ok v -> v
5858+ | Error e -> raise_client_error e
5959+6060+(** Check if an error is retryable *)
6161+let is_retryable = function
6262+ | Network _ -> true
6363+ | Http { status; _ } -> status >= 500 || status = 429
6464+ | Matrix { errcode; _ } -> errcode = Matrix_client.Error.M_LIMIT_EXCEEDED
6565+ | Json _ | Not_logged_in | Cancelled -> false
6666+6767+(** Get retry delay in seconds from an error, if available *)
6868+let retry_after = function
6969+ | Matrix { retry_after_ms = Some ms; _ } -> Some (float_of_int ms /. 1000.0)
7070+ | Http { status = 429; _ } -> Some 1.0 (* Default retry after *)
7171+ | _ -> None
+99
lib/matrix_eio/keys.ml
···11+(** Eio-idiomatic E2EE key management operations. *)
22+33+(** Re-export key types from matrix_client *)
44+type ed25519_keypair = Matrix_client.Keys.ed25519_keypair
55+type curve25519_keypair = Matrix_client.Keys.curve25519_keypair
66+type device_keys = Matrix_client.Keys.device_keys
77+type one_time_key = Matrix_client.Keys.one_time_key
88+type fallback_key = Matrix_client.Keys.fallback_key
99+1010+(** Generate an Ed25519 keypair for signing. *)
1111+let generate_ed25519 = Matrix_client.Keys.generate_ed25519
1212+1313+(** Generate a Curve25519 keypair for key exchange. *)
1414+let generate_curve25519 = Matrix_client.Keys.generate_curve25519
1515+1616+(** Generate a batch of one-time keys.
1717+ @param count Number of keys to generate
1818+ @param sign_with Optional Ed25519 key to sign the one-time keys *)
1919+let generate_one_time_keys = Matrix_client.Keys.generate_one_time_keys
2020+2121+(** Base64 encoding/decoding for keys *)
2222+let ed25519_pub_to_base64 = Matrix_client.Keys.ed25519_pub_to_base64
2323+let ed25519_priv_to_base64 = Matrix_client.Keys.ed25519_priv_to_base64
2424+let curve25519_pub_to_base64 = Matrix_client.Keys.curve25519_pub_to_base64
2525+let curve25519_secret_to_base64 = Matrix_client.Keys.curve25519_secret_to_base64
2626+2727+(** Parse keys from base64 *)
2828+let ed25519_pub_of_base64 s =
2929+ match Matrix_client.Keys.ed25519_pub_of_base64 s with
3030+ | Ok k -> k
3131+ | Error msg -> invalid_arg msg
3232+3333+let ed25519_priv_of_base64 s =
3434+ match Matrix_client.Keys.ed25519_priv_of_base64 s with
3535+ | Ok k -> k
3636+ | Error msg -> invalid_arg msg
3737+3838+let curve25519_pub_of_base64 s =
3939+ match Matrix_client.Keys.curve25519_pub_of_base64 s with
4040+ | Ok k -> k
4141+ | Error msg -> invalid_arg msg
4242+4343+let curve25519_secret_of_base64 s =
4444+ match Matrix_client.Keys.curve25519_secret_of_base64 s with
4545+ | Ok k -> k
4646+ | Error msg -> invalid_arg msg
4747+4848+(** Sign JSON with an Ed25519 key. *)
4949+let sign_json = Matrix_client.Keys.sign_json
5050+5151+(** Verify a signature on JSON. *)
5252+let verify_signature = Matrix_client.Keys.verify_signature
5353+5454+(** Perform a Curve25519 key exchange. *)
5555+let key_exchange ~secret ~their_public =
5656+ match Matrix_client.Keys.key_exchange ~secret ~their_public with
5757+ | Ok shared -> shared
5858+ | Error msg -> invalid_arg msg
5959+6060+(** Create device keys for upload. *)
6161+let create_device_keys ~user_id ~device_id ~ed25519_keypair ~curve25519_keypair =
6262+ match Matrix_client.Keys.create_device_keys
6363+ ~user_id ~device_id ~ed25519_keypair ~curve25519_keypair with
6464+ | Ok keys -> keys
6565+ | Error msg -> invalid_arg msg
6666+6767+(** Upload keys response *)
6868+type upload_keys_response = Matrix_client.Keys.upload_keys_response
6969+7070+(** Upload device keys and/or one-time keys.
7171+ @raise Eio.Io on failure *)
7272+let upload_keys client ?device_keys ?(one_time_keys=[]) ?(fallback_keys=[]) () =
7373+ Error.unwrap (Matrix_client.Keys.upload_keys (Client.base client)
7474+ ?device_keys ~one_time_keys ~fallback_keys ())
7575+7676+(** Query keys response *)
7777+type query_keys_response = Matrix_client.Keys.query_keys_response
7878+type queried_device_keys = Matrix_client.Keys.queried_device_keys
7979+8080+(** Query device keys for users.
8181+ @raise Eio.Io on failure *)
8282+let query_keys client ?timeout ~users () =
8383+ Error.unwrap (Matrix_client.Keys.query_keys (Client.base client) ?timeout ~users ())
8484+8585+(** Claim keys response *)
8686+type claim_keys_response = Matrix_client.Keys.claim_keys_response
8787+8888+(** Claim one-time keys for establishing Olm sessions.
8989+ @raise Eio.Io on failure *)
9090+let claim_keys client ?timeout ~keys () =
9191+ Error.unwrap (Matrix_client.Keys.claim_keys (Client.base client) ?timeout ~keys ())
9292+9393+(** Key changes response *)
9494+type key_changes_response = Matrix_client.Keys.key_changes_response
9595+9696+(** Get key changes since a sync token.
9797+ @raise Eio.Io on failure *)
9898+let get_key_changes client ~from ~to_ =
9999+ Error.unwrap (Matrix_client.Keys.get_key_changes (Client.base client) ~from ~to_)
+193
lib/matrix_eio/matrix_eio.ml
···11+(** Eio-idiomatic Matrix Client SDK.
22+33+ This library provides a Matrix client implementation using Eio idioms:
44+55+ {b Structured Concurrency}: All operations respect Eio switches for
66+ proper resource cleanup and cancellation.
77+88+ {b Exception-based Errors}: Operations raise [Eio.Io] exceptions instead
99+ of returning Result types, making code more readable and allowing
1010+ natural error propagation.
1111+1212+ {b Fibre-based Sync}: The sync loop runs in a dedicated fibre that can
1313+ be cancelled via the controlling switch.
1414+1515+ {2 Quick Start}
1616+1717+ {[
1818+ Eio_main.run (fun env ->
1919+ Eio.Switch.run (fun sw ->
2020+ (* Create client *)
2121+ let client = Matrix_eio.Client.create ~sw ~env
2222+ ~homeserver:(Uri.of_string "https://matrix.org") () in
2323+2424+ (* Login *)
2525+ let client = Matrix_eio.Auth.login_password client
2626+ ~user:"username" ~password:"password" () in
2727+2828+ (* Send a message *)
2929+ Matrix_eio.Messages.send_text client
3030+ ~room_id:(Matrix_proto.Id.Room_id.of_string_exn "!room:server")
3131+ ~body:"Hello, Matrix!" ();
3232+3333+ (* Sync with callbacks *)
3434+ Matrix_eio.Sync.sync_forever ~sw ~clock:(Eio.Stdenv.clock env)
3535+ client
3636+ ~on_sync:(fun response ->
3737+ (* Handle sync response *)
3838+ Matrix_eio.Sync.Continue)
3939+ ()
4040+ ))
4141+ ]}
4242+4343+ {2 Error Handling}
4444+4545+ All operations can raise [Eio.Io] exceptions. Use [try...with] or
4646+ [Eio.Switch.run] for proper error handling:
4747+4848+ {[
4949+ try
5050+ Matrix_eio.Messages.send_text client ~room_id ~body:"test" ()
5151+ with Eio.Io (Error.E err, _) ->
5252+ match err with
5353+ | Error.Network msg -> Printf.printf "Network error: %s\n" msg
5454+ | Error.Matrix { errcode; error; _ } ->
5555+ Printf.printf "Matrix error %s: %s\n"
5656+ (Matrix_client.Error.errcode_to_string errcode) error
5757+ | _ -> Printf.printf "Other error\n"
5858+ ]}
5959+6060+ {2 Cancellation}
6161+6262+ Operations can be cancelled by releasing the switch:
6363+6464+ {[
6565+ let cancelled = ref false in
6666+ Eio.Fiber.both
6767+ (fun () ->
6868+ Eio.Switch.run (fun sw ->
6969+ Matrix_eio.Sync.sync_forever ~sw ~clock client
7070+ ~on_sync:(fun _ -> Continue) ()))
7171+ (fun () ->
7272+ Eio.Time.sleep clock 60.0;
7373+ (* Sync will be cancelled when the switch releases *)
7474+ cancelled := true)
7575+ ]}
7676+7777+ {1 Modules}
7878+7979+ {2 Core}
8080+8181+ - {!module:Error} - Error types and handling
8282+ - {!module:Client} - Client creation and session management
8383+ - {!module:Auth} - Authentication (login, logout, registration)
8484+ - {!module:Sync} - Sync loop with Eio patterns
8585+8686+ {2 Room Operations}
8787+8888+ - {!module:Rooms} - Room creation, joining, leaving
8989+ - {!module:Messages} - Sending and receiving messages
9090+ - {!module:State} - Room state management
9191+ - {!module:Relations} - Reactions, edits, threads, replies
9292+9393+ {2 User Features}
9494+9595+ - {!module:Profile} - User profile management
9696+ - {!module:Presence} - Online/offline status
9797+ - {!module:Typing} - Typing indicators
9898+ - {!module:Receipts} - Read receipts
9999+ - {!module:Account} - Account settings
100100+101101+ {2 Media & Devices}
102102+103103+ - {!module:Media} - Upload/download media
104104+ - {!module:Devices} - Device management
105105+106106+ {2 Discovery}
107107+108108+ - {!module:Directory} - Room directory
109109+110110+ {2 End-to-End Encryption}
111111+112112+ - {!module:Keys} - Key management for E2EE *)
113113+114114+(** {1 Core} *)
115115+116116+module Error = Error
117117+module Client = Client
118118+module Auth = Auth
119119+module Sync = Sync
120120+121121+(** {1 Rooms} *)
122122+123123+module Rooms = Rooms
124124+module Messages = Messages
125125+module State = State
126126+module Relations = Relations
127127+128128+(** {1 User Features} *)
129129+130130+module Profile = Profile
131131+module Typing = Typing
132132+module Receipts = Receipts
133133+module Account = Account
134134+module Presence = Presence
135135+136136+(** {1 Media & Devices} *)
137137+138138+module Media = Media
139139+module Devices = Devices
140140+141141+(** {1 Discovery} *)
142142+143143+module Directory = Directory
144144+145145+(** {1 End-to-End Encryption} *)
146146+147147+module Keys = Keys
148148+module Verification = Verification
149149+module Backup = Backup
150150+151151+(** {1 Offline Support} *)
152152+153153+module Send_queue = Send_queue
154154+155155+(** {1 Convenience Functions} *)
156156+157157+(** Create a Matrix client connected to a homeserver.
158158+159159+ This is a convenience function that wraps {!Client.create}.
160160+161161+ @param sw The Eio switch for resource management
162162+ @param env The Eio environment
163163+ @param homeserver The Matrix homeserver URL
164164+ @param user_agent Optional user agent string *)
165165+let connect ~sw ~env ~homeserver ?user_agent () =
166166+ Client.create ~sw ~env ~homeserver ?user_agent ()
167167+168168+(** Login to a Matrix server with password authentication.
169169+170170+ This is a convenience function that creates a client and logs in.
171171+172172+ @param sw The Eio switch
173173+ @param env The Eio environment
174174+ @param homeserver The Matrix homeserver URL
175175+ @param user The username
176176+ @param password The password
177177+ @return The logged-in client *)
178178+let login_password ~sw ~env ~homeserver ~user ~password () =
179179+ let client = connect ~sw ~env ~homeserver () in
180180+ Auth.login_password client ~user ~password ()
181181+182182+(** Run a sync loop with a callback.
183183+184184+ This is a convenience function for common sync patterns.
185185+186186+ @param sw The Eio switch
187187+ @param env The Eio environment (provides clock)
188188+ @param client The logged-in client
189189+ @param on_sync Callback for each sync response
190190+ @param on_error Optional error handler *)
191191+let run_sync ~sw ~env client ~on_sync ?on_error () =
192192+ let clock = Eio.Stdenv.clock env in
193193+ Sync.sync_forever ~sw ~clock client ~on_sync ?on_error ()
+64
lib/matrix_eio/media.ml
···11+(** Eio-idiomatic media operations.
22+33+ Note: Many media operations require direct HTTP access for binary data.
44+ These functions may return errors indicating that full implementation
55+ requires using the Requests library directly. *)
66+77+(** Upload media content.
88+99+ Note: This requires binary upload which may not be fully implemented.
1010+1111+ @param content_type MIME type of the content
1212+ @param filename Optional filename
1313+ @param data The media content as string
1414+ @return The mxc:// URI for the uploaded content
1515+ @raise Eio.Io on failure *)
1616+let upload client ~content_type ~data ?filename () =
1717+ Error.unwrap (Matrix_client.Media.upload (Client.base client)
1818+ ~content_type ~data ?filename ())
1919+2020+(** Download media content.
2121+2222+ Note: This requires binary download which may not be fully implemented.
2323+2424+ @param server_name The server hosting the content
2525+ @param media_id The media ID
2626+ @return The media content as string
2727+ @raise Eio.Io on failure *)
2828+let download client ~server_name ~media_id =
2929+ Error.unwrap (Matrix_client.Media.download (Client.base client)
3030+ ~server_name ~media_id)
3131+3232+(** Download a thumbnail of media.
3333+3434+ Note: This requires binary download which may not be fully implemented.
3535+3636+ @param server_name The server hosting the content
3737+ @param media_id The media ID
3838+ @param width Desired width
3939+ @param height Desired height
4040+ @param method_ Optional resize method ("crop" or "scale")
4141+ @return The thumbnail content as string
4242+ @raise Eio.Io on failure *)
4343+let thumbnail client ~server_name ~media_id ~width ~height ?method_ () =
4444+ Error.unwrap (Matrix_client.Media.thumbnail (Client.base client)
4545+ ~server_name ~media_id ~width ~height ?method_ ())
4646+4747+(** Parse an mxc:// URI.
4848+ @return (server_name, media_id) tuple option *)
4949+let parse_mxc = Matrix_client.Media.parse_mxc
5050+5151+(** Convert mxc:// to HTTP URL.
5252+ @return HTTP URL option *)
5353+let mxc_to_http client ~mxc ?width ?height () =
5454+ Matrix_client.Media.mxc_to_http (Client.base client) ~mxc ?width ?height ()
5555+5656+(** Media configuration from server *)
5757+type config = Matrix_client.Media.config = {
5858+ upload_size : int option;
5959+}
6060+6161+(** Get media upload configuration.
6262+ @raise Eio.Io on failure *)
6363+let get_config client =
6464+ Error.unwrap (Matrix_client.Media.get_config (Client.base client))
+96
lib/matrix_eio/messages.ml
···11+(** Eio-idiomatic message operations.
22+33+ All functions raise [Eio.Io] exceptions on error instead of
44+ returning Result types. *)
55+66+(** Send a text message to a room.
77+88+ @param room_id The room to send to
99+ @param body The message text
1010+ @param format Optional format (e.g., "org.matrix.custom.html")
1111+ @param formatted_body Optional HTML-formatted body
1212+ @return The event ID of the sent message
1313+ @raise Eio.Io on failure *)
1414+let send_text client ~room_id ~body ?format ?formatted_body () =
1515+ Error.unwrap (Matrix_client.Messages.send_text (Client.base client)
1616+ ~room_id ~body ?format ?formatted_body ())
1717+1818+(** Send an emote message (/me action).
1919+ @raise Eio.Io on failure *)
2020+let send_emote client ~room_id ~body () =
2121+ Error.unwrap (Matrix_client.Messages.send_emote (Client.base client)
2222+ ~room_id ~body ())
2323+2424+(** Send a notice message (bot message).
2525+ @raise Eio.Io on failure *)
2626+let send_notice client ~room_id ~body () =
2727+ Error.unwrap (Matrix_client.Messages.send_notice (Client.base client)
2828+ ~room_id ~body ())
2929+3030+(** Send an image message.
3131+ @raise Eio.Io on failure *)
3232+let send_image client ~room_id ~body ~url ?info () =
3333+ Error.unwrap (Matrix_client.Messages.send_image (Client.base client)
3434+ ~room_id ~body ~url ?info ())
3535+3636+(** Send a file message.
3737+ @raise Eio.Io on failure *)
3838+let send_file client ~room_id ~body ~url ?info () =
3939+ Error.unwrap (Matrix_client.Messages.send_file (Client.base client)
4040+ ~room_id ~body ~url ?info ())
4141+4242+(** Redact (delete) an event.
4343+ @raise Eio.Io on failure *)
4444+let redact client ~room_id ~event_id ?reason () =
4545+ Error.unwrap (Matrix_client.Messages.redact (Client.base client)
4646+ ~room_id ~event_id ?reason ())
4747+4848+(** Pagination direction *)
4949+type direction = Matrix_client.Messages.direction = Forward | Backward
5050+5151+(** Messages response from pagination *)
5252+type messages_response = Matrix_client.Messages.messages_response = {
5353+ start : string;
5454+ end_ : string option;
5555+ chunk : Matrix_proto.Event.Raw_event.t list;
5656+ state : Matrix_proto.Event.Raw_event.t list;
5757+}
5858+5959+(** Get messages from a room with pagination.
6060+6161+ @param room_id The room ID
6262+ @param from Pagination token to start from
6363+ @param dir Direction (Forward or Backward)
6464+ @param limit Maximum number of events to return
6565+ @param filter Optional filter JSON string
6666+ @return Messages response with events and pagination tokens
6767+ @raise Eio.Io on failure *)
6868+let get_messages client ~room_id ~from ~dir ?limit ?filter () =
6969+ Error.unwrap (Matrix_client.Messages.get_messages (Client.base client)
7070+ ~room_id ~from ~dir ?limit ?filter ())
7171+7272+(** Event context result *)
7373+type context = Matrix_client.Messages.context = {
7474+ start : string;
7575+ end_ : string;
7676+ event : Matrix_proto.Event.Raw_event.t;
7777+ events_before : Matrix_proto.Event.Raw_event.t list;
7878+ events_after : Matrix_proto.Event.Raw_event.t list;
7979+ state : Matrix_proto.Event.Raw_event.t list;
8080+}
8181+8282+(** Get context around an event.
8383+8484+ @param room_id The room ID
8585+ @param event_id The event to get context for
8686+ @param limit Number of events before/after to include
8787+ @return Context with events before and after
8888+ @raise Eio.Io on failure *)
8989+let get_context client ~room_id ~event_id ?limit () =
9090+ Error.unwrap (Matrix_client.Messages.get_context (Client.base client)
9191+ ~room_id ~event_id ?limit ())
9292+9393+(** Get a single event by ID.
9494+ @raise Eio.Io on failure *)
9595+let get_event client ~room_id ~event_id =
9696+ Error.unwrap (Matrix_client.Messages.get_event (Client.base client) ~room_id ~event_id)
+41
lib/matrix_eio/presence.ml
···11+(** Eio-idiomatic presence operations. *)
22+33+(** Presence state *)
44+type presence_state = Matrix_client.Presence.presence_state =
55+ | Online
66+ | Offline
77+ | Unavailable
88+99+(** Presence status *)
1010+type presence = Matrix_client.Presence.presence = {
1111+ presence : presence_state;
1212+ status_msg : string option;
1313+ last_active_ago : int option;
1414+ currently_active : bool option;
1515+}
1616+1717+(** Get a user's presence status.
1818+ @raise Eio.Io on failure *)
1919+let get_presence client ~user_id =
2020+ Error.unwrap (Matrix_client.Presence.get_presence (Client.base client) ~user_id)
2121+2222+(** Set the current user's presence status.
2323+ @raise Eio.Io on failure *)
2424+let set_presence client ~presence ?status_msg () =
2525+ Error.unwrap (Matrix_client.Presence.set_presence (Client.base client)
2626+ ~presence ?status_msg ())
2727+2828+(** Set the current user as online.
2929+ @raise Eio.Io on failure *)
3030+let set_online client ?status_msg () =
3131+ set_presence client ~presence:Online ?status_msg ()
3232+3333+(** Set the current user as offline.
3434+ @raise Eio.Io on failure *)
3535+let set_offline client () =
3636+ set_presence client ~presence:Offline ()
3737+3838+(** Set the current user as unavailable/away.
3939+ @raise Eio.Io on failure *)
4040+let set_unavailable client ?status_msg () =
4141+ set_presence client ~presence:Unavailable ?status_msg ()
+35
lib/matrix_eio/profile.ml
···11+(** Eio-idiomatic profile operations.
22+33+ All functions raise [Eio.Io] exceptions on error instead of
44+ returning Result types. *)
55+66+(** Profile data *)
77+type profile = Matrix_client.Profile.profile = {
88+ displayname : string option;
99+ avatar_url : string option;
1010+}
1111+1212+(** Get a user's profile.
1313+ @raise Eio.Io on failure *)
1414+let get_profile client ~user_id =
1515+ Error.unwrap (Matrix_client.Profile.get_profile (Client.base client) ~user_id)
1616+1717+(** Get a user's display name.
1818+ @raise Eio.Io on failure *)
1919+let get_displayname client ~user_id =
2020+ Error.unwrap (Matrix_client.Profile.get_displayname (Client.base client) ~user_id)
2121+2222+(** Set the current user's display name.
2323+ @raise Eio.Io on failure *)
2424+let set_displayname client ~displayname =
2525+ Error.unwrap (Matrix_client.Profile.set_displayname (Client.base client) ~displayname)
2626+2727+(** Get a user's avatar URL.
2828+ @raise Eio.Io on failure *)
2929+let get_avatar_url client ~user_id =
3030+ Error.unwrap (Matrix_client.Profile.get_avatar_url (Client.base client) ~user_id)
3131+3232+(** Set the current user's avatar URL.
3333+ @raise Eio.Io on failure *)
3434+let set_avatar_url client ~avatar_url =
3535+ Error.unwrap (Matrix_client.Profile.set_avatar_url (Client.base client) ~avatar_url)
+33
lib/matrix_eio/receipts.ml
···11+(** Eio-idiomatic receipt operations. *)
22+33+(** Send a read receipt.
44+55+ @param room_id The room containing the event
66+ @param event_id The event to mark as read
77+ @param receipt_type Receipt type (default "m.read", also "m.read.private")
88+ @raise Eio.Io on failure *)
99+let send_receipt client ~room_id ~event_id ?receipt_type () =
1010+ Error.unwrap (Matrix_client.Receipts.send_receipt (Client.base client)
1111+ ~room_id ~event_id ?receipt_type ())
1212+1313+(** Mark a room as read up to an event.
1414+ Convenience function that sends a public read receipt.
1515+ @raise Eio.Io on failure *)
1616+let mark_read client ~room_id ~event_id =
1717+ send_receipt client ~room_id ~event_id ~receipt_type:"m.read" ()
1818+1919+(** Mark a room as read privately.
2020+ The read status is not visible to other users.
2121+ @raise Eio.Io on failure *)
2222+let mark_read_private client ~room_id ~event_id =
2323+ send_receipt client ~room_id ~event_id ~receipt_type:"m.read.private" ()
2424+2525+(** Set the fully read marker for a room.
2626+2727+ @param room_id The room to update
2828+ @param fully_read The event to mark as fully read
2929+ @param read Optional event to also mark as read
3030+ @raise Eio.Io on failure *)
3131+let set_read_marker client ~room_id ~fully_read ?read () =
3232+ Error.unwrap (Matrix_client.Receipts.set_read_marker (Client.base client)
3333+ ~room_id ~fully_read ?read ())
+85
lib/matrix_eio/relations.ml
···11+(** Eio-idiomatic relation operations (reactions, edits, threads, replies). *)
22+33+(** Send a reaction to an event.
44+55+ @param room_id The room containing the event
66+ @param event_id The event to react to
77+ @param key The reaction key (usually an emoji)
88+ @return The reaction event ID
99+ @raise Eio.Io on failure *)
1010+let send_reaction client ~room_id ~event_id ~key =
1111+ Error.unwrap (Matrix_client.Relations.send_reaction (Client.base client)
1212+ ~room_id ~event_id ~key)
1313+1414+(** Send an edit to a message.
1515+1616+ @param room_id The room containing the event
1717+ @param event_id The event to edit
1818+ @param new_body The new message body
1919+ @return The edit event ID
2020+ @raise Eio.Io on failure *)
2121+let edit_message client ~room_id ~event_id ~new_body =
2222+ Error.unwrap (Matrix_client.Relations.edit_message (Client.base client)
2323+ ~room_id ~event_id ~new_body)
2424+2525+(** Send a reply to a message.
2626+2727+ @param room_id The room containing the event
2828+ @param event_id The event to reply to
2929+ @param body The reply body
3030+ @return The reply event ID
3131+ @raise Eio.Io on failure *)
3232+let send_reply client ~room_id ~event_id ~body =
3333+ Error.unwrap (Matrix_client.Relations.send_reply (Client.base client)
3434+ ~room_id ~event_id ~body)
3535+3636+(** Send a message in a thread.
3737+3838+ @param room_id The room containing the thread
3939+ @param thread_root_id The root event of the thread
4040+ @param body The message body
4141+ @param reply_to_id Optional event to reply to within the thread
4242+ @return The message event ID
4343+ @raise Eio.Io on failure *)
4444+let send_in_thread client ~room_id ~thread_root_id ?reply_to_id ~body () =
4545+ Error.unwrap (Matrix_client.Relations.send_in_thread (Client.base client)
4646+ ~room_id ~thread_root_id ?reply_to_id ~body ())
4747+4848+(** Relation type *)
4949+type relation_type = Matrix_client.Relations.relation_type =
5050+ | Annotation (** m.annotation - reactions *)
5151+ | Reference (** m.reference - generic reference *)
5252+ | Replace (** m.replace - edits *)
5353+ | Thread (** m.thread - threads *)
5454+5555+(** Aggregation result *)
5656+type aggregation = Matrix_client.Relations.aggregation = {
5757+ event_id : Matrix_proto.Id.Event_id.t;
5858+ origin_server_ts : int64;
5959+ sender : Matrix_proto.Id.User_id.t;
6060+}
6161+6262+(** Relations response *)
6363+type relations_response = Matrix_client.Relations.relations_response = {
6464+ chunk : aggregation list;
6565+ next_batch : string option;
6666+ prev_batch : string option;
6767+}
6868+6969+(** Get events related to a given event.
7070+7171+ @param room_id The room containing the event
7272+ @param event_id The event to get relations for
7373+ @param rel_type Optional relation type filter
7474+ @param event_type Optional event type filter
7575+ @param limit Maximum number of events
7676+ @param from Pagination token
7777+ @raise Eio.Io on failure *)
7878+let get_relations client ~room_id ~event_id ?rel_type ?event_type ?limit ?from () =
7979+ Error.unwrap (Matrix_client.Relations.get_relations (Client.base client)
8080+ ~room_id ~event_id ?rel_type ?event_type ?limit ?from ())
8181+8282+(** Get reactions for an event.
8383+ @raise Eio.Io on failure *)
8484+let get_reactions client ~room_id ~event_id =
8585+ Error.unwrap (Matrix_client.Relations.get_reactions (Client.base client) ~room_id ~event_id)
+140
lib/matrix_eio/rooms.ml
···11+(** Eio-idiomatic room operations.
22+33+ All functions raise [Eio.Io] exceptions on error instead of
44+ returning Result types. *)
55+66+(** Room visibility *)
77+type visibility = Matrix_client.Rooms.visibility
88+99+(** Room creation preset *)
1010+type preset = Matrix_client.Rooms.preset =
1111+ | Private_chat
1212+ | Public_chat
1313+ | Trusted_private_chat
1414+1515+(** Create a new room.
1616+1717+ @param name Optional room name
1818+ @param topic Optional room topic
1919+ @param preset Room preset (affects default permissions)
2020+ @param is_direct Mark as a direct message room
2121+ @param invite List of user IDs to invite
2222+ @param room_alias_local_part Local part of a room alias
2323+ @param visibility Public or Private
2424+ @param room_type Optional room type (e.g., "m.space")
2525+ @return The new room ID
2626+ @raise Eio.Io on failure *)
2727+let create client ?name ?topic ?visibility ?preset ?room_alias_local_part ?invite ?is_direct ?room_type () =
2828+ Error.unwrap (Matrix_client.Rooms.create (Client.base client)
2929+ ?name ?topic ?visibility ?preset ?room_alias_local_part ?invite ?is_direct ?room_type ())
3030+3131+(** Join a room by ID or alias.
3232+ @raise Eio.Io on failure *)
3333+let join client ~room_id_or_alias ?via ?reason () =
3434+ Error.unwrap (Matrix_client.Rooms.join (Client.base client) ~room_id_or_alias ?via ?reason ())
3535+3636+(** Leave a room.
3737+ @raise Eio.Io on failure *)
3838+let leave client ~room_id ?reason () =
3939+ Error.unwrap (Matrix_client.Rooms.leave (Client.base client) ~room_id ?reason ())
4040+4141+(** Forget a room (remove from room list).
4242+ @raise Eio.Io on failure *)
4343+let forget client ~room_id =
4444+ Error.unwrap (Matrix_client.Rooms.forget (Client.base client) ~room_id)
4545+4646+(** Invite a user to a room.
4747+ @raise Eio.Io on failure *)
4848+let invite client ~room_id ~user_id ?reason () =
4949+ Error.unwrap (Matrix_client.Rooms.invite (Client.base client) ~room_id ~user_id ?reason ())
5050+5151+(** Kick a user from a room.
5252+ @raise Eio.Io on failure *)
5353+let kick client ~room_id ~user_id ?reason () =
5454+ Error.unwrap (Matrix_client.Rooms.kick (Client.base client) ~room_id ~user_id ?reason ())
5555+5656+(** Ban a user from a room.
5757+ @raise Eio.Io on failure *)
5858+let ban client ~room_id ~user_id ?reason () =
5959+ Error.unwrap (Matrix_client.Rooms.ban (Client.base client) ~room_id ~user_id ?reason ())
6060+6161+(** Unban a user from a room.
6262+ @raise Eio.Io on failure *)
6363+let unban client ~room_id ~user_id ?reason () =
6464+ Error.unwrap (Matrix_client.Rooms.unban (Client.base client) ~room_id ~user_id ?reason ())
6565+6666+(** Get the list of joined rooms.
6767+ @raise Eio.Io on failure *)
6868+let get_joined_rooms client =
6969+ Error.unwrap (Matrix_client.Rooms.get_joined_rooms (Client.base client))
7070+7171+(** Member info *)
7272+type member = Matrix_client.Rooms.member = {
7373+ user_id : Matrix_proto.Id.User_id.t;
7474+ display_name : string option;
7575+ avatar_url : string option;
7676+ membership : string;
7777+}
7878+7979+(** Get members of a room.
8080+ @raise Eio.Io on failure *)
8181+let get_members client ~room_id ?membership ?not_membership () =
8282+ Error.unwrap (Matrix_client.Rooms.get_members (Client.base client) ~room_id ?membership ?not_membership ())
8383+8484+(** Public room info *)
8585+type public_room = Matrix_client.Rooms.public_room = {
8686+ room_id : Matrix_proto.Id.Room_id.t;
8787+ name : string option;
8888+ topic : string option;
8989+ num_joined_members : int;
9090+ world_readable : bool;
9191+ guest_can_join : bool;
9292+ avatar_url : string option;
9393+ canonical_alias : string option;
9494+}
9595+9696+(** Public rooms response *)
9797+type public_rooms_response = Matrix_client.Rooms.public_rooms_response = {
9898+ chunk : public_room list;
9999+ next_batch : string option;
100100+ prev_batch : string option;
101101+ total_room_count_estimate : int option;
102102+}
103103+104104+(** Get public rooms.
105105+ @raise Eio.Io on failure *)
106106+let get_public_rooms client ?limit ?since ?server () =
107107+ Error.unwrap (Matrix_client.Rooms.get_public_rooms (Client.base client) ?limit ?since ?server ())
108108+109109+(** Power levels type *)
110110+type power_levels = Matrix_client.Rooms.power_levels = {
111111+ ban : int;
112112+ events : (string * int) list;
113113+ events_default : int;
114114+ invite : int;
115115+ kick : int;
116116+ redact : int;
117117+ state_default : int;
118118+ users : (string * int) list;
119119+ users_default : int;
120120+ notifications : (string * int) list;
121121+}
122122+123123+(** Get power levels for a room.
124124+ @raise Eio.Io on failure *)
125125+let get_power_levels client ~room_id =
126126+ Error.unwrap (Matrix_client.Rooms.get_power_levels (Client.base client) ~room_id)
127127+128128+(** Set power levels for a room.
129129+ @raise Eio.Io on failure *)
130130+let set_power_levels client ~room_id ~power_levels =
131131+ Error.unwrap (Matrix_client.Rooms.set_power_levels (Client.base client) ~room_id ~power_levels)
132132+133133+(** Get a user's power level from power levels state. *)
134134+let get_user_power_level = Matrix_client.Rooms.get_user_power_level
135135+136136+(** Set a user's power level in a room.
137137+ @raise Eio.Io on failure *)
138138+let set_user_power_level client ~room_id ~user_id ~level =
139139+ Error.unwrap (Matrix_client.Rooms.set_user_power_level (Client.base client)
140140+ ~room_id ~user_id ~level)
+242
lib/matrix_eio/send_queue.ml
···11+(** Eio-idiomatic send queue for offline message queueing.
22+33+ This module provides Eio wrappers for the send queue with
44+ fibre-based processing support. *)
55+66+(** Re-export send queue types *)
77+type request_kind = Matrix_client.Send_queue.request_kind =
88+ | Event of {
99+ event_type : string;
1010+ content : Jsont.json;
1111+ txn_id : string;
1212+ }
1313+ | MediaUpload of {
1414+ content_type : string;
1515+ data_size : int;
1616+ local_path : string option;
1717+ txn_id : string;
1818+ }
1919+ | Reaction of {
2020+ relates_to : Matrix_proto.Id.Event_id.t;
2121+ key : string;
2222+ txn_id : string;
2323+ }
2424+ | Redaction of {
2525+ event_id : Matrix_proto.Id.Event_id.t;
2626+ reason : string option;
2727+ txn_id : string;
2828+ }
2929+3030+type request_state = Matrix_client.Send_queue.request_state =
3131+ | Pending
3232+ | Sending
3333+ | Sent
3434+ | Failed of string
3535+ | Cancelled
3636+3737+type send_result = Matrix_client.Send_queue.send_result =
3838+ | Sent_ok of { event_id : Matrix_proto.Id.Event_id.t option }
3939+ | Send_failed of { error : string; retryable : bool }
4040+ | Send_cancelled
4141+4242+type send_handle = Matrix_client.Send_queue.send_handle
4343+type room_send_queue = Matrix_client.Send_queue.room_send_queue
4444+type t = Matrix_client.Send_queue.t
4545+4646+(** {1 Queue Creation} *)
4747+4848+(** Create a new global send queue manager *)
4949+let create = Matrix_client.Send_queue.create
5050+5151+(** Get or create a room queue *)
5252+let get_room_queue = Matrix_client.Send_queue.get_room_queue
5353+5454+(** Create a new room send queue *)
5555+let create_room_queue = Matrix_client.Send_queue.create_room_queue
5656+5757+(** {1 Enqueueing Requests} *)
5858+5959+(** Send a message event *)
6060+let send_message = Matrix_client.Send_queue.send_message
6161+6262+(** Send a text message *)
6363+let send_text = Matrix_client.Send_queue.send_text
6464+6565+(** Send a reaction *)
6666+let send_reaction = Matrix_client.Send_queue.send_reaction
6767+6868+(** Send a redaction *)
6969+let send_redaction = Matrix_client.Send_queue.send_redaction
7070+7171+(** Send media with dependent event *)
7272+let send_media = Matrix_client.Send_queue.send_media
7373+7474+(** {1 Handle Operations} *)
7575+7676+(** Cancel a queued request *)
7777+let cancel = Matrix_client.Send_queue.cancel
7878+7979+(** Abort a request (cancel and remove) *)
8080+let abort = Matrix_client.Send_queue.abort
8181+8282+(** Get request by handle *)
8383+let get_request = Matrix_client.Send_queue.get_request
8484+8585+(** Check if request is still pending *)
8686+let is_pending = Matrix_client.Send_queue.is_pending
8787+8888+(** Check if request was sent *)
8989+let is_sent = Matrix_client.Send_queue.is_sent
9090+9191+(** {1 Queue Control} *)
9292+9393+(** Enable/disable a room queue *)
9494+let set_room_enabled = Matrix_client.Send_queue.set_room_enabled
9595+9696+(** Enable/disable all queues globally *)
9797+let set_enabled = Matrix_client.Send_queue.set_enabled
9898+9999+(** Check if globally enabled *)
100100+let is_enabled = Matrix_client.Send_queue.is_enabled
101101+102102+(** Check if a room queue is enabled *)
103103+let is_room_enabled = Matrix_client.Send_queue.is_room_enabled
104104+105105+(** {1 Queue Statistics} *)
106106+107107+(** Count of pending requests in a room queue *)
108108+let pending_count = Matrix_client.Send_queue.pending_count
109109+110110+(** Count of all pending requests across all rooms *)
111111+let total_pending = Matrix_client.Send_queue.total_pending
112112+113113+(** Get all pending requests for a room *)
114114+let pending_requests = Matrix_client.Send_queue.pending_requests
115115+116116+(** Get all failed requests for a room *)
117117+let failed_requests = Matrix_client.Send_queue.failed_requests
118118+119119+(** {1 Queue Processing} *)
120120+121121+(** Get next sendable request from queue *)
122122+let next_sendable = Matrix_client.Send_queue.next_sendable
123123+124124+(** Mark request as being sent *)
125125+let mark_sending = Matrix_client.Send_queue.mark_sending
126126+127127+(** Mark request as successfully sent *)
128128+let mark_sent = Matrix_client.Send_queue.mark_sent
129129+130130+(** Mark request as failed with optional retry *)
131131+let mark_failed = Matrix_client.Send_queue.mark_failed
132132+133133+(** Remove completed/cancelled/failed requests *)
134134+let cleanup_queue = Matrix_client.Send_queue.cleanup_queue
135135+136136+(** {1 Persistence} *)
137137+138138+(** Get all pending requests for persistence *)
139139+let requests_to_persist = Matrix_client.Send_queue.requests_to_persist
140140+141141+(** Restore requests from persistence *)
142142+let restore_requests = Matrix_client.Send_queue.restore_requests
143143+144144+(** {1 Local Echo} *)
145145+146146+(** Create a local echo event from a queued request *)
147147+let local_echo_event = Matrix_client.Send_queue.local_echo_event
148148+149149+(** {1 Dependencies} *)
150150+151151+(** Add a dependency between requests *)
152152+let add_dependency = Matrix_client.Send_queue.add_dependency
153153+154154+(** {1 Callbacks} *)
155155+156156+(** Set callback for state changes *)
157157+let on_state_change = Matrix_client.Send_queue.on_state_change
158158+159159+(** Set global error callback *)
160160+let on_error = Matrix_client.Send_queue.on_error
161161+162162+(** {1 Retry Logic} *)
163163+164164+(** Calculate delay for next retry (exponential backoff) *)
165165+let retry_delay = Matrix_client.Send_queue.retry_delay
166166+167167+(** Check if a request should be retried *)
168168+let should_retry = Matrix_client.Send_queue.should_retry
169169+170170+(** {1 Eio-Specific Queue Processing} *)
171171+172172+(** Process the send queue in a fibre.
173173+174174+ This function runs in a loop, processing queued requests and
175175+ sending them to the server. It respects the Eio switch for
176176+ cancellation.
177177+178178+ @param sw The Eio switch
179179+ @param clock The Eio clock for retry delays
180180+ @param send_fn The function to actually send a request *)
181181+let process_queue ~sw ~clock queue ~send_fn =
182182+ let rec loop () =
183183+ Eio.Switch.check sw;
184184+ match next_sendable queue with
185185+ | None ->
186186+ (* Wait a bit before checking again *)
187187+ Eio.Time.sleep clock 0.1;
188188+ loop ()
189189+ | Some request ->
190190+ mark_sending queue request;
191191+ (try
192192+ let result = send_fn request in
193193+ (match result with
194194+ | Sent_ok _ -> mark_sent queue request
195195+ | Send_failed { error; retryable } ->
196196+ mark_failed queue request error ~retryable
197197+ | Send_cancelled ->
198198+ Matrix_client.Send_queue.update_state queue request Cancelled)
199199+ with exn ->
200200+ let error = Printexc.to_string exn in
201201+ mark_failed queue request error ~retryable:true);
202202+ (* Small delay between sends *)
203203+ Eio.Time.sleep clock 0.05;
204204+ loop ()
205205+ in
206206+ loop ()
207207+208208+(** Start processing the send queue in a background fibre.
209209+210210+ Returns immediately after spawning the processing fibre.
211211+212212+ @param sw The Eio switch
213213+ @param clock The Eio clock
214214+ @param send_fn The function to actually send a request *)
215215+let start_processing ~sw ~clock queue ~send_fn =
216216+ Eio.Fiber.fork ~sw (fun () ->
217217+ process_queue ~sw ~clock queue ~send_fn)
218218+219219+(** Process pending requests once (non-blocking).
220220+221221+ Processes all currently sendable requests and returns.
222222+ Useful for manual queue processing. *)
223223+let process_pending queue ~send_fn =
224224+ let rec process () =
225225+ match next_sendable queue with
226226+ | None -> ()
227227+ | Some request ->
228228+ mark_sending queue request;
229229+ (try
230230+ let result = send_fn request in
231231+ (match result with
232232+ | Sent_ok _ -> mark_sent queue request
233233+ | Send_failed { error; retryable } ->
234234+ mark_failed queue request error ~retryable
235235+ | Send_cancelled ->
236236+ Matrix_client.Send_queue.update_state queue request Cancelled)
237237+ with exn ->
238238+ let error = Printexc.to_string exn in
239239+ mark_failed queue request error ~retryable:true);
240240+ process ()
241241+ in
242242+ process ()
+63
lib/matrix_eio/state.ml
···11+(** Eio-idiomatic room state operations.
22+33+ All functions raise [Eio.Io] exceptions on error instead of
44+ returning Result types. *)
55+66+(** Get a room state event.
77+88+ @param room_id The room ID
99+ @param event_type The state event type
1010+ @param state_key The state key (empty string for events without a key)
1111+ @return The state event content as JSON
1212+ @raise Eio.Io on failure *)
1313+let get_state_event client ~room_id ~event_type ?state_key () =
1414+ Error.unwrap (Matrix_client.State.get_state_event (Client.base client)
1515+ ~room_id ~event_type ?state_key ())
1616+1717+(** Set a room state event.
1818+1919+ @param room_id The room ID
2020+ @param event_type The state event type
2121+ @param state_key The state key (empty string for events without a key)
2222+ @param content The event content as JSON
2323+ @return The event ID
2424+ @raise Eio.Io on failure *)
2525+let set_state client ~room_id ~event_type ?state_key ~content () =
2626+ Error.unwrap (Matrix_client.State.set_state (Client.base client)
2727+ ~room_id ~event_type ?state_key ~content ())
2828+2929+(** Get all state events for a room.
3030+ @return List of state events as JSON
3131+ @raise Eio.Io on failure *)
3232+let get_state client ~room_id =
3333+ Error.unwrap (Matrix_client.State.get_state (Client.base client) ~room_id)
3434+3535+(** Get the room name.
3636+ @raise Eio.Io on failure *)
3737+let get_name client ~room_id =
3838+ Error.unwrap (Matrix_client.State.get_name (Client.base client) ~room_id)
3939+4040+(** Set the room name.
4141+ @raise Eio.Io on failure *)
4242+let set_name client ~room_id ~name =
4343+ Error.unwrap (Matrix_client.State.set_name (Client.base client) ~room_id ~name)
4444+4545+(** Get the room topic.
4646+ @raise Eio.Io on failure *)
4747+let get_topic client ~room_id =
4848+ Error.unwrap (Matrix_client.State.get_topic (Client.base client) ~room_id)
4949+5050+(** Set the room topic.
5151+ @raise Eio.Io on failure *)
5252+let set_topic client ~room_id ~topic =
5353+ Error.unwrap (Matrix_client.State.set_topic (Client.base client) ~room_id ~topic)
5454+5555+(** Get the room avatar URL.
5656+ @raise Eio.Io on failure *)
5757+let get_avatar client ~room_id =
5858+ Error.unwrap (Matrix_client.State.get_avatar (Client.base client) ~room_id)
5959+6060+(** Set the room avatar URL.
6161+ @raise Eio.Io on failure *)
6262+let set_avatar client ~room_id ~url =
6363+ Error.unwrap (Matrix_client.State.set_avatar (Client.base client) ~room_id ~url)
+159
lib/matrix_eio/sync.ml
···11+(** Eio-idiomatic sync operations with structured concurrency.
22+33+ This module provides sync functionality using Eio patterns:
44+ - Fibre-based sync loops with cancellation
55+ - Event streams using Eio.Stream
66+ - Proper resource cleanup via switches *)
77+88+(** Sync parameters *)
99+type params = Matrix_client.Sync.params = {
1010+ filter : string option;
1111+ since : string option;
1212+ full_state : bool;
1313+ set_presence : [ `Online | `Offline | `Unavailable ] option;
1414+ timeout : int;
1515+}
1616+1717+let default_params = Matrix_client.Sync.default_params
1818+1919+(** Sync response type *)
2020+type response = Matrix_proto.Sync.Response.t
2121+2222+(** Perform a single sync request.
2323+2424+ @param params Sync parameters
2525+ @raise Eio.Io on network or protocol error *)
2626+let sync client ?(params = default_params) () =
2727+ Error.unwrap (Matrix_client.Sync.sync (Client.base client) ~params ())
2828+2929+(** Event handler callback type *)
3030+type 'a handler = response -> 'a
3131+3232+(** Sync loop control *)
3333+type control =
3434+ | Continue (** Continue syncing *)
3535+ | Stop (** Stop the sync loop *)
3636+ | Retry_after of float (** Retry after given seconds *)
3737+3838+(** Run a sync loop in a dedicated fibre.
3939+4040+ The sync loop runs until:
4141+ - The handler returns [Stop]
4242+ - The switch is cancelled
4343+ - An unrecoverable error occurs
4444+4545+ @param sw The switch controlling this fibre
4646+ @param clock The Eio clock for timing
4747+ @param initial_since Optional initial sync token (for resuming)
4848+ @param params Sync parameters
4949+ @param on_sync Callback for each sync response
5050+ @param on_error Callback for errors (returns control action)
5151+ @raise Eio.Io on unrecoverable error *)
5252+let sync_forever ~sw ~clock client
5353+ ?initial_since
5454+ ?(params = default_params)
5555+ ~on_sync
5656+ ?(on_error = fun _ -> Retry_after 5.0)
5757+ () =
5858+ let rec loop since =
5959+ (* Check if we should stop due to switch cancellation *)
6060+ Eio.Fiber.check ();
6161+ let params = { params with since } in
6262+ match Matrix_client.Sync.sync (Client.base client) ~params () with
6363+ | Error e ->
6464+ (match on_error (Error.of_client_error e) with
6565+ | Continue -> loop since
6666+ | Stop -> ()
6767+ | Retry_after delay ->
6868+ Eio.Time.sleep clock delay;
6969+ loop since)
7070+ | Ok response ->
7171+ (match on_sync response with
7272+ | Continue -> loop (Some response.next_batch)
7373+ | Stop -> ()
7474+ | Retry_after delay ->
7575+ Eio.Time.sleep clock delay;
7676+ loop (Some response.next_batch))
7777+ in
7878+ Eio.Fiber.fork ~sw (fun () -> loop initial_since)
7979+8080+(** Run a sync loop that pushes events to a stream.
8181+8282+ This is useful for decoupling sync from event processing.
8383+ Events are pushed to the stream as they arrive.
8484+8585+ @param sw The switch controlling this fibre
8686+ @param clock The Eio clock for timing
8787+ @param stream The stream to push events to
8888+ @param initial_since Optional initial sync token
8989+ @param params Sync parameters
9090+ @param on_error Error handler (returns control action) *)
9191+let sync_to_stream ~sw ~clock client ~stream
9292+ ?initial_since
9393+ ?(params = default_params)
9494+ ?(on_error = fun _ -> Retry_after 5.0)
9595+ () =
9696+ sync_forever ~sw ~clock client
9797+ ?initial_since ~params
9898+ ~on_sync:(fun response ->
9999+ Eio.Stream.add stream response;
100100+ Continue)
101101+ ~on_error
102102+ ()
103103+104104+(** Create a sync stream and start syncing.
105105+106106+ Returns a stream that yields sync responses.
107107+ The sync loop runs in a background fibre.
108108+109109+ @param sw The switch controlling the sync fibre
110110+ @param clock The Eio clock
111111+ @param capacity Stream buffer capacity
112112+ @param initial_since Optional initial sync token
113113+ @param params Sync parameters *)
114114+let create_sync_stream ~sw ~clock client
115115+ ?(capacity = 10)
116116+ ?initial_since
117117+ ?(params = default_params)
118118+ () =
119119+ let stream = Eio.Stream.create capacity in
120120+ sync_to_stream ~sw ~clock client ~stream ?initial_since ~params ();
121121+ stream
122122+123123+(** Iterate over sync responses.
124124+125125+ Runs the sync loop and calls [f] for each response.
126126+ Stops when [f] returns [Stop] or the switch is cancelled.
127127+128128+ @param sw The switch for cancellation
129129+ @param clock The Eio clock
130130+ @param initial_since Optional initial sync token
131131+ @param params Sync parameters
132132+ @param f Callback for each response *)
133133+let iter ~sw ~clock client ?initial_since ?(params = default_params) f =
134134+ sync_forever ~sw ~clock client
135135+ ?initial_since ~params
136136+ ~on_sync:(fun response -> f response; Continue)
137137+ ()
138138+139139+(** Filter types and creation *)
140140+type filter = Matrix_client.Sync.filter
141141+type room_filter = Matrix_client.Sync.room_filter
142142+type event_filter = Matrix_client.Sync.event_filter
143143+type room_event_filter = Matrix_client.Sync.room_event_filter
144144+145145+let default_filter = Matrix_client.Sync.default_filter
146146+let default_room_filter = Matrix_client.Sync.default_room_filter
147147+let default_event_filter = Matrix_client.Sync.default_event_filter
148148+let default_room_event_filter = Matrix_client.Sync.default_room_event_filter
149149+150150+(** Upload a filter to the server.
151151+ @return The filter ID
152152+ @raise Eio.Io on failure *)
153153+let create_filter client ~filter =
154154+ Error.unwrap (Matrix_client.Sync.create_filter (Client.base client) ~filter)
155155+156156+(** Get a previously uploaded filter.
157157+ @raise Eio.Io on failure *)
158158+let get_filter client ~filter_id =
159159+ Error.unwrap (Matrix_client.Sync.get_filter (Client.base client) ~filter_id)
+23
lib/matrix_eio/typing.ml
···11+(** Eio-idiomatic typing indicator operations. *)
22+33+(** Send a typing notification.
44+55+ @param room_id The room to send the typing indicator to
66+ @param typing Whether the user is typing
77+ @param timeout Typing timeout in milliseconds (default 30000)
88+ @raise Eio.Io on failure *)
99+let set_typing client ~room_id ~typing ?timeout () =
1010+ Error.unwrap (Matrix_client.Typing.set_typing (Client.base client)
1111+ ~room_id ~typing ?timeout ())
1212+1313+(** Start typing in a room.
1414+ Convenience function that sets typing to true.
1515+ @raise Eio.Io on failure *)
1616+let start_typing client ~room_id ?timeout () =
1717+ set_typing client ~room_id ~typing:true ?timeout ()
1818+1919+(** Stop typing in a room.
2020+ Convenience function that sets typing to false.
2121+ @raise Eio.Io on failure *)
2222+let stop_typing client ~room_id =
2323+ set_typing client ~room_id ~typing:false ()
+127
lib/matrix_eio/verification.ml
···11+(** Eio-idiomatic device verification operations.
22+33+ All verification functions are designed to work with Eio's
44+ structured concurrency and exception-based error handling. *)
55+66+(** Re-export verification types *)
77+type local_trust = Matrix_client.Verification.local_trust =
88+ | Verified
99+ | BlackListed
1010+ | Ignored
1111+ | Unset
1212+1313+type key_usage = Matrix_client.Verification.key_usage =
1414+ | Master
1515+ | Self_signing
1616+ | User_signing
1717+1818+type sas_state = Matrix_client.Verification.sas_state =
1919+ | Sas_created
2020+ | Sas_started
2121+ | Sas_accepted
2222+ | Sas_keys_exchanged
2323+ | Sas_confirmed
2424+ | Sas_mac_received
2525+ | Sas_done
2626+ | Sas_cancelled of string
2727+2828+type sas_output = Matrix_client.Verification.sas_output =
2929+ | Decimal of int * int * int
3030+ | Emoji of (int * string) list
3131+3232+type sas_method = Matrix_client.Verification.sas_method =
3333+ | Decimal_method
3434+ | Emoji_method
3535+3636+type qr_mode = Matrix_client.Verification.qr_mode =
3737+ | Self_verifying_master_key_trusts_device
3838+ | Self_verifying_device_trusts_master_key
3939+ | Verifying_another_user
4040+4141+(** {1 Cross-Signing Key Management} *)
4242+4343+(** Generate new cross-signing keys for the user *)
4444+let generate_cross_signing_keys identity =
4545+ Matrix_client.Verification.generate_cross_signing_keys identity
4646+4747+(** Build upload data for cross-signing keys *)
4848+let build_cross_signing_upload identity =
4949+ Matrix_client.Verification.build_cross_signing_upload identity
5050+5151+(** Create a private cross-signing identity *)
5252+let create_private_identity = Matrix_client.Verification.create_private_identity
5353+5454+(** {1 Device Trust} *)
5555+5656+(** Check if a device is verified (locally or via cross-signing) *)
5757+let is_device_verified = Matrix_client.Verification.is_device_verified
5858+5959+(** Set local trust state for a device *)
6060+let set_device_local_trust = Matrix_client.Verification.set_device_local_trust
6161+6262+(** Create a verified device record *)
6363+let create_verified_device = Matrix_client.Verification.create_verified_device
6464+6565+(** {1 User Identity} *)
6666+6767+(** Check if own identity is verified *)
6868+let is_own_identity_verified = Matrix_client.Verification.is_own_identity_verified
6969+7070+(** Check if other identity is verified by us *)
7171+let is_other_identity_verified = Matrix_client.Verification.is_other_identity_verified
7272+7373+(** Check if user identity has changed *)
7474+let has_identity_changed = Matrix_client.Verification.has_identity_changed
7575+7676+(** Pin the current master key for change detection *)
7777+let pin_master_key = Matrix_client.Verification.pin_master_key
7878+7979+(** {1 SAS Verification} *)
8080+8181+(** Create a new SAS verification session *)
8282+let create_sas_session = Matrix_client.Verification.create_sas_session
8383+8484+(** Get SAS output for display to user *)
8585+let get_sas_output = Matrix_client.Verification.get_sas_output
8686+8787+(** Confirm SAS match *)
8888+let confirm_sas = Matrix_client.Verification.confirm_sas
8989+9090+(** Cancel SAS verification *)
9191+let cancel_sas = Matrix_client.Verification.cancel_sas
9292+9393+(** Check if SAS verification is complete *)
9494+let is_sas_done = Matrix_client.Verification.is_sas_done
9595+9696+(** {1 QR Code Verification} *)
9797+9898+(** Create QR verification for self-verification *)
9999+let create_self_qr_verification = Matrix_client.Verification.create_self_qr_verification
100100+101101+(** Create QR verification for verifying another user *)
102102+let create_user_qr_verification = Matrix_client.Verification.create_user_qr_verification
103103+104104+(** {1 Verification Requests} *)
105105+106106+(** Create a verification request *)
107107+let create_verification_request = Matrix_client.Verification.create_verification_request
108108+109109+(** Accept a verification request *)
110110+let accept_verification_request = Matrix_client.Verification.accept_verification_request
111111+112112+(** Cancel a verification request *)
113113+let cancel_verification_request = Matrix_client.Verification.cancel_verification_request
114114+115115+(** {1 Signature Verification} *)
116116+117117+(** Verify an Ed25519 signature *)
118118+let verify_signature = Matrix_client.Verification.verify_signature
119119+120120+(** Verify cross-signing signature *)
121121+let verify_cross_signing_signature = Matrix_client.Verification.verify_cross_signing_signature
122122+123123+(** Sign data with a private key *)
124124+let sign_with_key private_key data =
125125+ match Matrix_client.Verification.sign_with_key private_key data with
126126+ | Ok sig_ -> sig_
127127+ | Error msg -> raise (Eio.Exn.create (Error.E (Error.Network msg)))
···11+(** Matrix event types with JSON codecs.
22+33+ Events are the fundamental unit of data in Matrix. All communication
44+ in Matrix happens through events, which are JSON objects with a
55+ standardized structure.
66+77+ This module provides types and codecs for:
88+ - {b State Events}: Persistent room configuration (create, member, name, etc.)
99+ - {b Message Events}: Transient room messages (text, image, file, etc.)
1010+ - {b Ephemeral Events}: Non-persistent data (typing indicators, receipts)
1111+ - {b To-Device Events}: Direct device-to-device messages (for E2EE)
1212+ - {b Account Data}: Per-user configuration (tags, read markers)
1313+1414+ All types include bidirectional JSON codecs using the [jsont] library,
1515+ enabling both parsing and serialization.
1616+1717+ @see <https://spec.matrix.org/v1.11/client-server-api/#events> Events
1818+ @see <https://spec.matrix.org/v1.11/client-server-api/#room-events> Room Events
1919+ @see <https://spec.matrix.org/v1.11/client-server-api/#state-events> State Events *)
2020+2121+open Matrix_id
2222+2323+(** {1 Timestamps}
2424+2525+ Matrix uses millisecond timestamps since the Unix epoch (1970-01-01 00:00:00 UTC).
2626+ These are used for event ordering and timing.
2727+2828+ @see <https://spec.matrix.org/v1.11/client-server-api/#events> Event timestamps *)
2929+3030+module Timestamp = struct
3131+ (** Server timestamps in milliseconds since Unix epoch.
3232+3333+ All Matrix events include an [origin_server_ts] field with the
3434+ timestamp when the event was received by the originating server. *)
3535+ type t = int64
3636+3737+ let of_ptime pt =
3838+ let span = Ptime.to_span pt in
3939+ let d, ps = Ptime.Span.to_d_ps span in
4040+ let days_ms = Int64.mul (Int64.of_int d) 86_400_000L in
4141+ let ps_ms = Int64.div ps 1_000_000_000L in
4242+ Int64.add days_ms ps_ms
4343+4444+ let to_ptime_opt t =
4545+ let days = Int64.div t 86_400_000L |> Int64.to_int in
4646+ let rem_ms = Int64.rem t 86_400_000L in
4747+ let ps = Int64.mul rem_ms 1_000_000_000L in
4848+ Ptime.Span.of_d_ps (days, ps) |> Option.map Ptime.of_span |> Option.join
4949+5050+ let jsont = Jsont.int64
5151+end
5252+5353+(** {1 Unsigned Event Data} *)
5454+5555+module Unsigned = struct
5656+ (** Unsigned data added by the homeserver. *)
5757+5858+ type t = {
5959+ age : int64 option;
6060+ prev_content : Jsont.json option;
6161+ prev_sender : User_id.t option;
6262+ redacted_because : Jsont.json option;
6363+ transaction_id : Transaction_id.t option;
6464+ }
6565+6666+ let empty = {
6767+ age = None;
6868+ prev_content = None;
6969+ prev_sender = None;
7070+ redacted_because = None;
7171+ transaction_id = None;
7272+ }
7373+7474+ let jsont =
7575+ Jsont.Object.(
7676+ map (fun age prev_content prev_sender redacted_because transaction_id ->
7777+ { age; prev_content; prev_sender; redacted_because; transaction_id })
7878+ |> opt_mem "age" Jsont.int64 ~enc:(fun t -> t.age)
7979+ |> opt_mem "prev_content" Jsont.json ~enc:(fun t -> t.prev_content)
8080+ |> opt_mem "prev_sender" User_id.jsont ~enc:(fun t -> t.prev_sender)
8181+ |> opt_mem "redacted_because" Jsont.json ~enc:(fun t -> t.redacted_because)
8282+ |> opt_mem "transaction_id" Transaction_id.jsont ~enc:(fun t -> t.transaction_id)
8383+ |> finish)
8484+end
8585+8686+(** {1 Room Membership} *)
8787+8888+module Membership = struct
8989+ type t =
9090+ | Join
9191+ | Invite
9292+ | Leave
9393+ | Ban
9494+ | Knock
9595+9696+ let to_string = function
9797+ | Join -> "join"
9898+ | Invite -> "invite"
9999+ | Leave -> "leave"
100100+ | Ban -> "ban"
101101+ | Knock -> "knock"
102102+103103+ let of_string = function
104104+ | "join" -> Ok Join
105105+ | "invite" -> Ok Invite
106106+ | "leave" -> Ok Leave
107107+ | "ban" -> Ok Ban
108108+ | "knock" -> Ok Knock
109109+ | s -> Error (`Unknown_membership s)
110110+111111+ let jsont =
112112+ Jsont.enum [
113113+ ("join", Join);
114114+ ("invite", Invite);
115115+ ("leave", Leave);
116116+ ("ban", Ban);
117117+ ("knock", Knock);
118118+ ]
119119+end
120120+121121+(** {1 Join Rules} *)
122122+123123+module Join_rule = struct
124124+ type t =
125125+ | Public
126126+ | Invite
127127+ | Knock
128128+ | Restricted
129129+ | Knock_restricted
130130+ | Private
131131+132132+ let jsont =
133133+ Jsont.enum [
134134+ ("public", Public);
135135+ ("invite", Invite);
136136+ ("knock", Knock);
137137+ ("restricted", Restricted);
138138+ ("knock_restricted", Knock_restricted);
139139+ ("private", Private);
140140+ ]
141141+end
142142+143143+(** {1 History Visibility} *)
144144+145145+module History_visibility = struct
146146+ type t =
147147+ | Invited
148148+ | Joined
149149+ | Shared
150150+ | World_readable
151151+152152+ let jsont =
153153+ Jsont.enum [
154154+ ("invited", Invited);
155155+ ("joined", Joined);
156156+ ("shared", Shared);
157157+ ("world_readable", World_readable);
158158+ ]
159159+end
160160+161161+(** {1 Room State Event Contents} *)
162162+163163+module Room_create_content = struct
164164+ type t = {
165165+ creator : User_id.t option; (* Deprecated in v11, optional *)
166166+ room_version : string option;
167167+ predecessor : predecessor option;
168168+ type_ : string option; (* m.space for spaces *)
169169+ }
170170+ and predecessor = {
171171+ room_id : Room_id.t;
172172+ event_id : Event_id.t;
173173+ }
174174+175175+ let predecessor_jsont =
176176+ Jsont.Object.(
177177+ map (fun room_id event_id -> { room_id; event_id })
178178+ |> mem "room_id" Room_id.jsont ~enc:(fun p -> p.room_id)
179179+ |> mem "event_id" Event_id.jsont ~enc:(fun p -> p.event_id)
180180+ |> finish)
181181+182182+ let jsont =
183183+ Jsont.Object.(
184184+ map (fun creator room_version predecessor type_ ->
185185+ { creator; room_version; predecessor; type_ })
186186+ |> opt_mem "creator" User_id.jsont ~enc:(fun t -> t.creator)
187187+ |> opt_mem "room_version" Jsont.string ~enc:(fun t -> t.room_version)
188188+ |> opt_mem "predecessor" predecessor_jsont ~enc:(fun t -> t.predecessor)
189189+ |> opt_mem "type" Jsont.string ~enc:(fun t -> t.type_)
190190+ |> finish)
191191+end
192192+193193+module Room_name_content = struct
194194+ type t = { name : string }
195195+196196+ let jsont =
197197+ Jsont.Object.(
198198+ map (fun name -> { name })
199199+ |> mem "name" Jsont.string ~enc:(fun t -> t.name)
200200+ |> finish)
201201+end
202202+203203+module Room_topic_content = struct
204204+ type t = { topic : string }
205205+206206+ let jsont =
207207+ Jsont.Object.(
208208+ map (fun topic -> { topic })
209209+ |> mem "topic" Jsont.string ~enc:(fun t -> t.topic)
210210+ |> finish)
211211+end
212212+213213+module Room_avatar_content = struct
214214+ type t = {
215215+ url : string option;
216216+ info : image_info option;
217217+ }
218218+ and image_info = {
219219+ h : int option;
220220+ w : int option;
221221+ mimetype : string option;
222222+ size : int option;
223223+ }
224224+225225+ let image_info_jsont =
226226+ Jsont.Object.(
227227+ map (fun h w mimetype size -> { h; w; mimetype; size })
228228+ |> opt_mem "h" Jsont.int ~enc:(fun t -> t.h)
229229+ |> opt_mem "w" Jsont.int ~enc:(fun t -> t.w)
230230+ |> opt_mem "mimetype" Jsont.string ~enc:(fun t -> t.mimetype)
231231+ |> opt_mem "size" Jsont.int ~enc:(fun t -> t.size)
232232+ |> finish)
233233+234234+ let jsont =
235235+ Jsont.Object.(
236236+ map (fun url info -> { url; info })
237237+ |> opt_mem "url" Jsont.string ~enc:(fun t -> t.url)
238238+ |> opt_mem "info" image_info_jsont ~enc:(fun t -> t.info)
239239+ |> finish)
240240+end
241241+242242+module Room_member_content = struct
243243+ type t = {
244244+ membership : Membership.t;
245245+ displayname : string option;
246246+ avatar_url : string option;
247247+ is_direct : bool option;
248248+ reason : string option;
249249+ }
250250+251251+ let jsont =
252252+ Jsont.Object.(
253253+ map (fun membership displayname avatar_url is_direct reason ->
254254+ { membership; displayname; avatar_url; is_direct; reason })
255255+ |> mem "membership" Membership.jsont ~enc:(fun t -> t.membership)
256256+ |> opt_mem "displayname" Jsont.string ~enc:(fun t -> t.displayname)
257257+ |> opt_mem "avatar_url" Jsont.string ~enc:(fun t -> t.avatar_url)
258258+ |> opt_mem "is_direct" Jsont.bool ~enc:(fun t -> t.is_direct)
259259+ |> opt_mem "reason" Jsont.string ~enc:(fun t -> t.reason)
260260+ |> finish)
261261+end
262262+263263+module Room_join_rules_content = struct
264264+ type t = {
265265+ join_rule : Join_rule.t;
266266+ allow : allow_condition list option;
267267+ }
268268+ and allow_condition = {
269269+ type_ : string;
270270+ room_id : Room_id.t option;
271271+ }
272272+273273+ let allow_condition_jsont =
274274+ Jsont.Object.(
275275+ map (fun type_ room_id -> { type_; room_id })
276276+ |> mem "type" Jsont.string ~enc:(fun c -> c.type_)
277277+ |> opt_mem "room_id" Room_id.jsont ~enc:(fun c -> c.room_id)
278278+ |> finish)
279279+280280+ let jsont =
281281+ Jsont.Object.(
282282+ map (fun join_rule allow -> { join_rule; allow })
283283+ |> mem "join_rule" Join_rule.jsont ~enc:(fun t -> t.join_rule)
284284+ |> opt_mem "allow" (Jsont.list allow_condition_jsont) ~enc:(fun t -> t.allow)
285285+ |> finish)
286286+end
287287+288288+module Room_history_visibility_content = struct
289289+ type t = { history_visibility : History_visibility.t }
290290+291291+ let jsont =
292292+ Jsont.Object.(
293293+ map (fun history_visibility -> { history_visibility })
294294+ |> mem "history_visibility" History_visibility.jsont
295295+ ~enc:(fun t -> t.history_visibility)
296296+ |> finish)
297297+end
298298+299299+module Room_canonical_alias_content = struct
300300+ type t = {
301301+ alias : Room_alias.t option;
302302+ alt_aliases : Room_alias.t list option;
303303+ }
304304+305305+ let jsont =
306306+ Jsont.Object.(
307307+ map (fun alias alt_aliases -> { alias; alt_aliases })
308308+ |> opt_mem "alias" Room_alias.jsont ~enc:(fun t -> t.alias)
309309+ |> opt_mem "alt_aliases" (Jsont.list Room_alias.jsont) ~enc:(fun t -> t.alt_aliases)
310310+ |> finish)
311311+end
312312+313313+module Room_power_levels_content = struct
314314+ type t = {
315315+ ban : int option;
316316+ events : (string * int) list option;
317317+ events_default : int option;
318318+ invite : int option;
319319+ kick : int option;
320320+ redact : int option;
321321+ state_default : int option;
322322+ users : (string * int) list option;
323323+ users_default : int option;
324324+ }
325325+326326+ (* Helper for string -> int maps encoded as objects *)
327327+ module StringMap = Map.Make(String)
328328+329329+ let int_map_jsont =
330330+ Jsont.Object.as_string_map Jsont.int
331331+ |> Jsont.map
332332+ ~dec:(fun m -> StringMap.bindings m)
333333+ ~enc:(fun l -> List.to_seq l |> StringMap.of_seq)
334334+335335+ let jsont =
336336+ Jsont.Object.(
337337+ map (fun ban events events_default invite kick redact
338338+ state_default users users_default ->
339339+ { ban; events; events_default; invite; kick; redact;
340340+ state_default; users; users_default })
341341+ |> opt_mem "ban" Jsont.int ~enc:(fun t -> t.ban)
342342+ |> opt_mem "events" int_map_jsont ~enc:(fun t -> t.events)
343343+ |> opt_mem "events_default" Jsont.int ~enc:(fun t -> t.events_default)
344344+ |> opt_mem "invite" Jsont.int ~enc:(fun t -> t.invite)
345345+ |> opt_mem "kick" Jsont.int ~enc:(fun t -> t.kick)
346346+ |> opt_mem "redact" Jsont.int ~enc:(fun t -> t.redact)
347347+ |> opt_mem "state_default" Jsont.int ~enc:(fun t -> t.state_default)
348348+ |> opt_mem "users" int_map_jsont ~enc:(fun t -> t.users)
349349+ |> opt_mem "users_default" Jsont.int ~enc:(fun t -> t.users_default)
350350+ |> finish)
351351+end
352352+353353+module Room_encryption_content = struct
354354+ type t = {
355355+ algorithm : string;
356356+ rotation_period_ms : int64 option;
357357+ rotation_period_msgs : int option;
358358+ }
359359+360360+ let jsont =
361361+ Jsont.Object.(
362362+ map (fun algorithm rotation_period_ms rotation_period_msgs ->
363363+ { algorithm; rotation_period_ms; rotation_period_msgs })
364364+ |> mem "algorithm" Jsont.string ~enc:(fun t -> t.algorithm)
365365+ |> opt_mem "rotation_period_ms" Jsont.int64 ~enc:(fun t -> t.rotation_period_ms)
366366+ |> opt_mem "rotation_period_msgs" Jsont.int ~enc:(fun t -> t.rotation_period_msgs)
367367+ |> finish)
368368+end
369369+370370+module Room_pinned_events_content = struct
371371+ type t = {
372372+ pinned : string list;
373373+ }
374374+375375+ let jsont =
376376+ Jsont.Object.(
377377+ map (fun pinned -> { pinned })
378378+ |> mem "pinned" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.pinned)
379379+ |> finish)
380380+end
381381+382382+module Room_server_acl_content = struct
383383+ type t = {
384384+ allow : string list;
385385+ allow_ip_literals : bool;
386386+ deny : string list;
387387+ }
388388+389389+ let jsont =
390390+ Jsont.Object.(
391391+ map (fun allow allow_ip_literals deny ->
392392+ { allow; allow_ip_literals; deny })
393393+ |> mem "allow" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.allow)
394394+ |> mem "allow_ip_literals" Jsont.bool ~dec_absent:true ~enc:(fun t -> t.allow_ip_literals)
395395+ |> mem "deny" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.deny)
396396+ |> finish)
397397+end
398398+399399+module Room_tombstone_content = struct
400400+ type t = {
401401+ body : string;
402402+ replacement_room : Room_id.t;
403403+ }
404404+405405+ let jsont =
406406+ Jsont.Object.(
407407+ map (fun body replacement_room -> { body; replacement_room })
408408+ |> mem "body" Jsont.string ~enc:(fun t -> t.body)
409409+ |> mem "replacement_room" Room_id.jsont ~enc:(fun t -> t.replacement_room)
410410+ |> finish)
411411+end
412412+413413+module Room_guest_access_content = struct
414414+ type access =
415415+ | Can_join
416416+ | Forbidden
417417+418418+ let access_jsont =
419419+ Jsont.enum [
420420+ ("can_join", Can_join);
421421+ ("forbidden", Forbidden);
422422+ ]
423423+424424+ type t = {
425425+ guest_access : access;
426426+ }
427427+428428+ let jsont =
429429+ Jsont.Object.(
430430+ map (fun guest_access -> { guest_access })
431431+ |> mem "guest_access" access_jsont ~enc:(fun t -> t.guest_access)
432432+ |> finish)
433433+end
434434+435435+(** {1 Space Event Contents} *)
436436+437437+module Space_child_content = struct
438438+ type t = {
439439+ via : string list option;
440440+ order : string option;
441441+ suggested : bool option;
442442+ }
443443+444444+ let jsont =
445445+ Jsont.Object.(
446446+ map (fun via order suggested -> { via; order; suggested })
447447+ |> opt_mem "via" (Jsont.list Jsont.string) ~enc:(fun t -> t.via)
448448+ |> opt_mem "order" Jsont.string ~enc:(fun t -> t.order)
449449+ |> opt_mem "suggested" Jsont.bool ~enc:(fun t -> t.suggested)
450450+ |> finish)
451451+end
452452+453453+module Space_parent_content = struct
454454+ type t = {
455455+ via : string list option;
456456+ canonical : bool option;
457457+ }
458458+459459+ let jsont =
460460+ Jsont.Object.(
461461+ map (fun via canonical -> { via; canonical })
462462+ |> opt_mem "via" (Jsont.list Jsont.string) ~enc:(fun t -> t.via)
463463+ |> opt_mem "canonical" Jsont.bool ~enc:(fun t -> t.canonical)
464464+ |> finish)
465465+end
466466+467467+(** {1 Call Event Contents} *)
468468+469469+module Call_invite_content = struct
470470+ type t = {
471471+ call_id : string;
472472+ party_id : string option;
473473+ version : int;
474474+ lifetime : int;
475475+ offer : sdp_content;
476476+ invitee : string option;
477477+ }
478478+ and sdp_content = {
479479+ type_ : string;
480480+ sdp : string;
481481+ }
482482+483483+ let sdp_content_jsont =
484484+ Jsont.Object.(
485485+ map (fun type_ sdp -> { type_; sdp })
486486+ |> mem "type" Jsont.string ~enc:(fun t -> t.type_)
487487+ |> mem "sdp" Jsont.string ~enc:(fun t -> t.sdp)
488488+ |> finish)
489489+490490+ let jsont =
491491+ Jsont.Object.(
492492+ map (fun call_id party_id version lifetime offer invitee ->
493493+ { call_id; party_id; version; lifetime; offer; invitee })
494494+ |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id)
495495+ |> opt_mem "party_id" Jsont.string ~enc:(fun t -> t.party_id)
496496+ |> mem "version" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.version)
497497+ |> mem "lifetime" Jsont.int ~enc:(fun t -> t.lifetime)
498498+ |> mem "offer" sdp_content_jsont ~enc:(fun t -> t.offer)
499499+ |> opt_mem "invitee" Jsont.string ~enc:(fun t -> t.invitee)
500500+ |> finish)
501501+end
502502+503503+module Call_answer_content = struct
504504+ type sdp_content = {
505505+ type_ : string;
506506+ sdp : string;
507507+ }
508508+509509+ type t = {
510510+ call_id : string;
511511+ party_id : string option;
512512+ version : int;
513513+ answer : sdp_content;
514514+ }
515515+516516+ let sdp_content_jsont =
517517+ Jsont.Object.(
518518+ map (fun type_ sdp -> { type_; sdp })
519519+ |> mem "type" Jsont.string ~enc:(fun t -> t.type_)
520520+ |> mem "sdp" Jsont.string ~enc:(fun t -> t.sdp)
521521+ |> finish)
522522+523523+ let jsont =
524524+ Jsont.Object.(
525525+ map (fun call_id party_id version answer ->
526526+ { call_id; party_id; version; answer })
527527+ |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id)
528528+ |> opt_mem "party_id" Jsont.string ~enc:(fun t -> t.party_id)
529529+ |> mem "version" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.version)
530530+ |> mem "answer" sdp_content_jsont ~enc:(fun t -> t.answer)
531531+ |> finish)
532532+end
533533+534534+module Call_hangup_content = struct
535535+ type reason =
536536+ | Ice_failed
537537+ | Invite_timeout
538538+ | User_hangup
539539+ | User_media_failed
540540+ | User_busy
541541+ | Unknown_error
542542+543543+ let reason_jsont =
544544+ Jsont.enum [
545545+ ("ice_failed", Ice_failed);
546546+ ("invite_timeout", Invite_timeout);
547547+ ("user_hangup", User_hangup);
548548+ ("user_media_failed", User_media_failed);
549549+ ("user_busy", User_busy);
550550+ ("unknown_error", Unknown_error);
551551+ ]
552552+553553+ type t = {
554554+ call_id : string;
555555+ party_id : string option;
556556+ version : int;
557557+ reason : reason option;
558558+ }
559559+560560+ let jsont =
561561+ Jsont.Object.(
562562+ map (fun call_id party_id version reason ->
563563+ { call_id; party_id; version; reason })
564564+ |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id)
565565+ |> opt_mem "party_id" Jsont.string ~enc:(fun t -> t.party_id)
566566+ |> mem "version" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.version)
567567+ |> opt_mem "reason" reason_jsont ~enc:(fun t -> t.reason)
568568+ |> finish)
569569+end
570570+571571+module Call_candidates_content = struct
572572+ type candidate = {
573573+ candidate : string;
574574+ sdp_mid : string;
575575+ sdp_m_line_index : int;
576576+ }
577577+578578+ let candidate_jsont =
579579+ Jsont.Object.(
580580+ map (fun candidate sdp_mid sdp_m_line_index ->
581581+ { candidate; sdp_mid; sdp_m_line_index })
582582+ |> mem "candidate" Jsont.string ~enc:(fun t -> t.candidate)
583583+ |> mem "sdpMid" Jsont.string ~enc:(fun t -> t.sdp_mid)
584584+ |> mem "sdpMLineIndex" Jsont.int ~enc:(fun t -> t.sdp_m_line_index)
585585+ |> finish)
586586+587587+ type t = {
588588+ call_id : string;
589589+ party_id : string option;
590590+ version : int;
591591+ candidates : candidate list;
592592+ }
593593+594594+ let jsont =
595595+ Jsont.Object.(
596596+ map (fun call_id party_id version candidates ->
597597+ { call_id; party_id; version; candidates })
598598+ |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id)
599599+ |> opt_mem "party_id" Jsont.string ~enc:(fun t -> t.party_id)
600600+ |> mem "version" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.version)
601601+ |> mem "candidates" (Jsont.list candidate_jsont) ~dec_absent:[]
602602+ ~enc:(fun t -> t.candidates)
603603+ |> finish)
604604+end
605605+606606+(** {1 Call Member Content (m.call.member)} *)
607607+608608+module Call_member_content = struct
609609+ (** Focus type for MatrixRTC *)
610610+ type focus = {
611611+ type_ : string;
612612+ livekit_service_url : string option;
613613+ livekit_alias : string option;
614614+ }
615615+616616+ let focus_jsont =
617617+ Jsont.Object.(
618618+ map (fun type_ livekit_service_url livekit_alias ->
619619+ { type_; livekit_service_url; livekit_alias })
620620+ |> mem "type" Jsont.string ~enc:(fun t -> t.type_)
621621+ |> opt_mem "livekit_service_url" Jsont.string ~enc:(fun t -> t.livekit_service_url)
622622+ |> opt_mem "livekit_alias" Jsont.string ~enc:(fun t -> t.livekit_alias)
623623+ |> finish)
624624+625625+ type membership = {
626626+ call_id : string;
627627+ scope : string;
628628+ application : string;
629629+ device_id : string;
630630+ expires : int64;
631631+ foci_active : focus list option;
632632+ membership_id : string option;
633633+ }
634634+635635+ let membership_jsont =
636636+ Jsont.Object.(
637637+ map (fun call_id scope application device_id expires foci_active membership_id ->
638638+ { call_id; scope; application; device_id; expires; foci_active; membership_id })
639639+ |> mem "call_id" Jsont.string ~enc:(fun t -> t.call_id)
640640+ |> mem "scope" Jsont.string ~dec_absent:"m.room" ~enc:(fun t -> t.scope)
641641+ |> mem "application" Jsont.string ~enc:(fun t -> t.application)
642642+ |> mem "device_id" Jsont.string ~enc:(fun t -> t.device_id)
643643+ |> mem "expires" Jsont.int64 ~enc:(fun t -> t.expires)
644644+ |> opt_mem "foci_active" (Jsont.list focus_jsont) ~enc:(fun t -> t.foci_active)
645645+ |> opt_mem "membership_id" Jsont.string ~enc:(fun t -> t.membership_id)
646646+ |> finish)
647647+648648+ type t = {
649649+ memberships : membership list;
650650+ }
651651+652652+ let jsont =
653653+ Jsont.Object.(
654654+ map (fun memberships -> { memberships })
655655+ |> mem "memberships" (Jsont.list membership_jsont) ~dec_absent:[]
656656+ ~enc:(fun t -> t.memberships)
657657+ |> finish)
658658+end
659659+660660+(** {1 Key Verification Event Contents}
661661+662662+ Key verification events are used to verify the identity of users
663663+ and their devices through interactive verification protocols like
664664+ SAS (Short Authentication String) or QR codes.
665665+666666+ @see <https://spec.matrix.org/v1.11/client-server-api/#key-verification-framework> Key Verification Framework
667667+ @see <https://spec.matrix.org/v1.11/client-server-api/#short-authentication-string-sas-verification> SAS Verification *)
668668+669669+module Key_verification_ready_content = struct
670670+ (** Content for [m.key.verification.ready] events.
671671+672672+ Sent by a user to indicate they are ready to start verification
673673+ and which methods they support. *)
674674+ type t = {
675675+ from_device : string;
676676+ methods : string list;
677677+ transaction_id : string option;
678678+ }
679679+680680+ let jsont =
681681+ Jsont.Object.(
682682+ map (fun from_device methods transaction_id ->
683683+ { from_device; methods; transaction_id })
684684+ |> mem "from_device" Jsont.string ~enc:(fun t -> t.from_device)
685685+ |> mem "methods" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun t -> t.methods)
686686+ |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id)
687687+ |> finish)
688688+end
689689+690690+module Key_verification_start_content = struct
691691+ type t = {
692692+ from_device : string;
693693+ method_ : string;
694694+ transaction_id : string option;
695695+ key_agreement_protocols : string list option;
696696+ hashes : string list option;
697697+ message_authentication_codes : string list option;
698698+ short_authentication_string : string list option;
699699+ }
700700+701701+ let jsont =
702702+ Jsont.Object.(
703703+ map (fun from_device method_ transaction_id key_agreement_protocols hashes
704704+ message_authentication_codes short_authentication_string ->
705705+ { from_device; method_; transaction_id; key_agreement_protocols; hashes;
706706+ message_authentication_codes; short_authentication_string })
707707+ |> mem "from_device" Jsont.string ~enc:(fun t -> t.from_device)
708708+ |> mem "method" Jsont.string ~enc:(fun t -> t.method_)
709709+ |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id)
710710+ |> opt_mem "key_agreement_protocols" (Jsont.list Jsont.string)
711711+ ~enc:(fun t -> t.key_agreement_protocols)
712712+ |> opt_mem "hashes" (Jsont.list Jsont.string) ~enc:(fun t -> t.hashes)
713713+ |> opt_mem "message_authentication_codes" (Jsont.list Jsont.string)
714714+ ~enc:(fun t -> t.message_authentication_codes)
715715+ |> opt_mem "short_authentication_string" (Jsont.list Jsont.string)
716716+ ~enc:(fun t -> t.short_authentication_string)
717717+ |> finish)
718718+end
719719+720720+module Key_verification_accept_content = struct
721721+ type t = {
722722+ transaction_id : string option;
723723+ method_ : string;
724724+ key_agreement_protocol : string;
725725+ hash : string;
726726+ message_authentication_code : string;
727727+ short_authentication_string : string list;
728728+ commitment : string;
729729+ }
730730+731731+ let jsont =
732732+ Jsont.Object.(
733733+ map (fun transaction_id method_ key_agreement_protocol hash
734734+ message_authentication_code short_authentication_string commitment ->
735735+ { transaction_id; method_; key_agreement_protocol; hash;
736736+ message_authentication_code; short_authentication_string; commitment })
737737+ |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id)
738738+ |> mem "method" Jsont.string ~enc:(fun t -> t.method_)
739739+ |> mem "key_agreement_protocol" Jsont.string ~enc:(fun t -> t.key_agreement_protocol)
740740+ |> mem "hash" Jsont.string ~enc:(fun t -> t.hash)
741741+ |> mem "message_authentication_code" Jsont.string
742742+ ~enc:(fun t -> t.message_authentication_code)
743743+ |> mem "short_authentication_string" (Jsont.list Jsont.string)
744744+ ~enc:(fun t -> t.short_authentication_string)
745745+ |> mem "commitment" Jsont.string ~enc:(fun t -> t.commitment)
746746+ |> finish)
747747+end
748748+749749+module Key_verification_key_content = struct
750750+ type t = {
751751+ transaction_id : string option;
752752+ key : string;
753753+ }
754754+755755+ let jsont =
756756+ Jsont.Object.(
757757+ map (fun transaction_id key -> { transaction_id; key })
758758+ |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id)
759759+ |> mem "key" Jsont.string ~enc:(fun t -> t.key)
760760+ |> finish)
761761+end
762762+763763+module Key_verification_mac_content = struct
764764+ module StringMap = Map.Make(String)
765765+766766+ let string_map_jsont =
767767+ Jsont.Object.as_string_map Jsont.string
768768+ |> Jsont.map
769769+ ~dec:(fun m -> StringMap.bindings m)
770770+ ~enc:(fun l -> List.to_seq l |> StringMap.of_seq)
771771+772772+ type t = {
773773+ transaction_id : string option;
774774+ mac : (string * string) list;
775775+ keys : string;
776776+ }
777777+778778+ let jsont =
779779+ Jsont.Object.(
780780+ map (fun transaction_id mac keys -> { transaction_id; mac; keys })
781781+ |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id)
782782+ |> mem "mac" string_map_jsont ~enc:(fun t -> t.mac)
783783+ |> mem "keys" Jsont.string ~enc:(fun t -> t.keys)
784784+ |> finish)
785785+end
786786+787787+module Key_verification_cancel_content = struct
788788+ type t = {
789789+ transaction_id : string option;
790790+ code : string;
791791+ reason : string;
792792+ }
793793+794794+ let jsont =
795795+ Jsont.Object.(
796796+ map (fun transaction_id code reason -> { transaction_id; code; reason })
797797+ |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id)
798798+ |> mem "code" Jsont.string ~enc:(fun t -> t.code)
799799+ |> mem "reason" Jsont.string ~enc:(fun t -> t.reason)
800800+ |> finish)
801801+end
802802+803803+module Key_verification_done_content = struct
804804+ type t = {
805805+ transaction_id : string option;
806806+ }
807807+808808+ let jsont =
809809+ Jsont.Object.(
810810+ map (fun transaction_id -> { transaction_id })
811811+ |> opt_mem "transaction_id" Jsont.string ~enc:(fun t -> t.transaction_id)
812812+ |> finish)
813813+end
814814+815815+(** {1 Policy Rule Event Contents}
816816+817817+ Policy rules allow servers and rooms to define moderation policies.
818818+ These events describe entities (users, rooms, servers) that should
819819+ be banned or otherwise restricted.
820820+821821+ @see <https://spec.matrix.org/v1.11/client-server-api/#moderation-policy-lists> Moderation Policy Lists *)
822822+823823+module Policy_rule_content = struct
824824+ (** Recommendation for how to handle a policy rule match.
825825+826826+ Currently only [m.ban] is specified, but servers may define
827827+ custom recommendations. *)
828828+ type recommendation =
829829+ | Ban (** The entity should be banned *)
830830+ | Unknown of string (** Custom or unrecognized recommendation *)
831831+832832+ let recommendation_to_string = function
833833+ | Ban -> "m.ban"
834834+ | Unknown s -> s
835835+836836+ let recommendation_of_string = function
837837+ | "m.ban" -> Ban
838838+ | s -> Unknown s
839839+840840+ let recommendation_jsont =
841841+ Jsont.of_of_string ~kind:"recommendation"
842842+ ~enc:recommendation_to_string
843843+ (fun s -> Ok (recommendation_of_string s))
844844+845845+ type t = {
846846+ entity : string;
847847+ reason : string;
848848+ recommendation : recommendation;
849849+ }
850850+851851+ let jsont =
852852+ Jsont.Object.(
853853+ map (fun entity reason recommendation -> { entity; reason; recommendation })
854854+ |> mem "entity" Jsont.string ~enc:(fun t -> t.entity)
855855+ |> mem "reason" Jsont.string ~enc:(fun t -> t.reason)
856856+ |> mem "recommendation" recommendation_jsont ~enc:(fun t -> t.recommendation)
857857+ |> finish)
858858+end
859859+860860+(** {1 Account Data Contents}
861861+862862+ Account data events store per-user, per-room configuration such as
863863+ read markers, tags, and notification settings.
864864+865865+ @see <https://spec.matrix.org/v1.11/client-server-api/#client-config> Client Config *)
866866+867867+module Marked_unread_content = struct
868868+ (** Content for [m.marked_unread] room account data.
869869+870870+ Allows users to manually mark a room as unread regardless of
871871+ actual read status. *)
872872+ type t = {
873873+ unread : bool; (** Whether the room should be shown as unread *)
874874+ }
875875+876876+ let jsont =
877877+ Jsont.Object.(
878878+ map (fun unread -> { unread })
879879+ |> mem "unread" Jsont.bool ~dec_absent:false ~enc:(fun t -> t.unread)
880880+ |> finish)
881881+end
882882+883883+(** {1 Encrypted Event Content} *)
884884+885885+module Encrypted_content = struct
886886+ type algorithm =
887887+ | Olm_v1_curve25519_aes_sha2_256
888888+ | Megolm_v1_aes_sha2_256
889889+ | Unknown of string
890890+891891+ let algorithm_to_string = function
892892+ | Olm_v1_curve25519_aes_sha2_256 -> "m.olm.v1.curve25519-aes-sha2-256"
893893+ | Megolm_v1_aes_sha2_256 -> "m.megolm.v1.aes-sha2-256"
894894+ | Unknown s -> s
895895+896896+ let algorithm_of_string = function
897897+ | "m.olm.v1.curve25519-aes-sha2-256" -> Olm_v1_curve25519_aes_sha2_256
898898+ | "m.megolm.v1.aes-sha2-256" -> Megolm_v1_aes_sha2_256
899899+ | s -> Unknown s
900900+901901+ let algorithm_jsont =
902902+ Jsont.of_of_string ~kind:"algorithm"
903903+ ~enc:algorithm_to_string
904904+ (fun s -> Ok (algorithm_of_string s))
905905+906906+ type t = {
907907+ algorithm : algorithm;
908908+ sender_key : string;
909909+ ciphertext : Jsont.json; (* Can be string or object depending on algorithm *)
910910+ session_id : string option; (* Megolm only *)
911911+ device_id : string option;
912912+ }
913913+914914+ let jsont =
915915+ Jsont.Object.(
916916+ map (fun algorithm sender_key ciphertext session_id device_id ->
917917+ { algorithm; sender_key; ciphertext; session_id; device_id })
918918+ |> mem "algorithm" algorithm_jsont ~enc:(fun t -> t.algorithm)
919919+ |> mem "sender_key" Jsont.string ~enc:(fun t -> t.sender_key)
920920+ |> mem "ciphertext" Jsont.json ~enc:(fun t -> t.ciphertext)
921921+ |> opt_mem "session_id" Jsont.string ~enc:(fun t -> t.session_id)
922922+ |> opt_mem "device_id" Jsont.string ~enc:(fun t -> t.device_id)
923923+ |> finish)
924924+end
925925+926926+(** {1 Reaction Content} *)
927927+928928+module Reaction_content = struct
929929+ type relates_to = {
930930+ rel_type : string;
931931+ event_id : Event_id.t;
932932+ key : string;
933933+ }
934934+935935+ let relates_to_jsont =
936936+ Jsont.Object.(
937937+ map (fun rel_type event_id key -> { rel_type; event_id; key })
938938+ |> mem "rel_type" Jsont.string ~enc:(fun t -> t.rel_type)
939939+ |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id)
940940+ |> mem "key" Jsont.string ~enc:(fun t -> t.key)
941941+ |> finish)
942942+943943+ type t = {
944944+ relates_to : relates_to;
945945+ }
946946+947947+ let jsont =
948948+ Jsont.Object.(
949949+ map (fun relates_to -> { relates_to })
950950+ |> mem "m.relates_to" relates_to_jsont ~enc:(fun t -> t.relates_to)
951951+ |> finish)
952952+end
953953+954954+(** {1 Beacon/Live Location Content} *)
955955+956956+module Beacon_info_content = struct
957957+ type t = {
958958+ description : string option;
959959+ live : bool;
960960+ timeout : int64;
961961+ asset_type : string option;
962962+ }
963963+964964+ let jsont =
965965+ Jsont.Object.(
966966+ map (fun description live timeout asset_type ->
967967+ { description; live; timeout; asset_type })
968968+ |> opt_mem "description" Jsont.string ~enc:(fun t -> t.description)
969969+ |> mem "live" Jsont.bool ~dec_absent:true ~enc:(fun t -> t.live)
970970+ |> mem "timeout" Jsont.int64 ~enc:(fun t -> t.timeout)
971971+ |> opt_mem "org.matrix.msc3488.asset" Jsont.string ~enc:(fun t -> t.asset_type)
972972+ |> finish)
973973+end
974974+975975+module Beacon_content = struct
976976+ type location = {
977977+ uri : string;
978978+ description : string option;
979979+ }
980980+981981+ let location_jsont =
982982+ Jsont.Object.(
983983+ map (fun uri description -> { uri; description })
984984+ |> mem "uri" Jsont.string ~enc:(fun t -> t.uri)
985985+ |> opt_mem "description" Jsont.string ~enc:(fun t -> t.description)
986986+ |> finish)
987987+988988+ type relates_to = {
989989+ rel_type : string;
990990+ event_id : Event_id.t;
991991+ }
992992+993993+ let relates_to_jsont =
994994+ Jsont.Object.(
995995+ map (fun rel_type event_id -> { rel_type; event_id })
996996+ |> mem "rel_type" Jsont.string ~enc:(fun t -> t.rel_type)
997997+ |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id)
998998+ |> finish)
999999+10001000+ type t = {
10011001+ location : location;
10021002+ timestamp : int64;
10031003+ relates_to : relates_to;
10041004+ }
10051005+10061006+ let jsont =
10071007+ Jsont.Object.(
10081008+ map (fun location timestamp relates_to ->
10091009+ { location; timestamp; relates_to })
10101010+ |> mem "org.matrix.msc3488.location" location_jsont ~enc:(fun t -> t.location)
10111011+ |> mem "org.matrix.msc3488.ts" Jsont.int64 ~enc:(fun t -> t.timestamp)
10121012+ |> mem "m.relates_to" relates_to_jsont ~enc:(fun t -> t.relates_to)
10131013+ |> finish)
10141014+end
10151015+10161016+(** {1 Poll Event Contents} *)
10171017+10181018+module Poll_start_content = struct
10191019+ type poll_answer = {
10201020+ id : string;
10211021+ text : string;
10221022+ }
10231023+10241024+ let poll_answer_jsont =
10251025+ Jsont.Object.(
10261026+ map (fun id text -> { id; text })
10271027+ |> mem "id" Jsont.string ~enc:(fun t -> t.id)
10281028+ |> mem "org.matrix.msc1767.text" Jsont.string ~enc:(fun t -> t.text)
10291029+ |> finish)
10301030+10311031+ type poll_kind =
10321032+ | Disclosed
10331033+ | Undisclosed
10341034+10351035+ let poll_kind_jsont =
10361036+ Jsont.enum [
10371037+ ("org.matrix.msc3381.poll.disclosed", Disclosed);
10381038+ ("org.matrix.msc3381.poll.undisclosed", Undisclosed);
10391039+ ]
10401040+10411041+ type poll_start = {
10421042+ question : string;
10431043+ kind : poll_kind;
10441044+ max_selections : int;
10451045+ answers : poll_answer list;
10461046+ }
10471047+10481048+ let poll_start_jsont =
10491049+ Jsont.Object.(
10501050+ map (fun question kind max_selections answers ->
10511051+ { question; kind; max_selections; answers })
10521052+ |> mem "question" Jsont.string ~enc:(fun t -> t.question)
10531053+ |> mem "kind" poll_kind_jsont ~dec_absent:Disclosed ~enc:(fun t -> t.kind)
10541054+ |> mem "max_selections" Jsont.int ~dec_absent:1 ~enc:(fun t -> t.max_selections)
10551055+ |> mem "answers" (Jsont.list poll_answer_jsont) ~enc:(fun t -> t.answers)
10561056+ |> finish)
10571057+10581058+ type t = {
10591059+ poll_start : poll_start;
10601060+ text : string;
10611061+ }
10621062+10631063+ let jsont =
10641064+ Jsont.Object.(
10651065+ map (fun poll_start text -> { poll_start; text })
10661066+ |> mem "org.matrix.msc3381.poll.start" poll_start_jsont ~enc:(fun t -> t.poll_start)
10671067+ |> mem "org.matrix.msc1767.text" Jsont.string ~enc:(fun t -> t.text)
10681068+ |> finish)
10691069+end
10701070+10711071+module Poll_response_content = struct
10721072+ type relates_to = {
10731073+ rel_type : string;
10741074+ event_id : Event_id.t;
10751075+ }
10761076+10771077+ let relates_to_jsont =
10781078+ Jsont.Object.(
10791079+ map (fun rel_type event_id -> { rel_type; event_id })
10801080+ |> mem "rel_type" Jsont.string ~enc:(fun t -> t.rel_type)
10811081+ |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id)
10821082+ |> finish)
10831083+10841084+ type t = {
10851085+ relates_to : relates_to;
10861086+ answers : string list;
10871087+ }
10881088+10891089+ let jsont =
10901090+ Jsont.Object.(
10911091+ map (fun relates_to answers -> { relates_to; answers })
10921092+ |> mem "m.relates_to" relates_to_jsont ~enc:(fun t -> t.relates_to)
10931093+ |> mem "org.matrix.msc3381.poll.response" (Jsont.list Jsont.string)
10941094+ ~dec_absent:[] ~enc:(fun t -> t.answers)
10951095+ |> finish)
10961096+end
10971097+10981098+module Poll_end_content = struct
10991099+ type relates_to = {
11001100+ rel_type : string;
11011101+ event_id : Event_id.t;
11021102+ }
11031103+11041104+ let relates_to_jsont =
11051105+ Jsont.Object.(
11061106+ map (fun rel_type event_id -> { rel_type; event_id })
11071107+ |> mem "rel_type" Jsont.string ~enc:(fun t -> t.rel_type)
11081108+ |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id)
11091109+ |> finish)
11101110+11111111+ type t = {
11121112+ relates_to : relates_to;
11131113+ text : string;
11141114+ }
11151115+11161116+ let jsont =
11171117+ Jsont.Object.(
11181118+ map (fun relates_to text -> { relates_to; text })
11191119+ |> mem "m.relates_to" relates_to_jsont ~enc:(fun t -> t.relates_to)
11201120+ |> mem "org.matrix.msc1767.text" Jsont.string ~enc:(fun t -> t.text)
11211121+ |> finish)
11221122+end
11231123+11241124+(** {1 Message Event Contents} *)
11251125+11261126+module Msgtype = struct
11271127+ type t =
11281128+ | Text
11291129+ | Emote
11301130+ | Notice
11311131+ | Image
11321132+ | File
11331133+ | Audio
11341134+ | Video
11351135+ | Location
11361136+ | Custom of string
11371137+11381138+ let to_string = function
11391139+ | Text -> "m.text"
11401140+ | Emote -> "m.emote"
11411141+ | Notice -> "m.notice"
11421142+ | Image -> "m.image"
11431143+ | File -> "m.file"
11441144+ | Audio -> "m.audio"
11451145+ | Video -> "m.video"
11461146+ | Location -> "m.location"
11471147+ | Custom s -> s
11481148+11491149+ let of_string = function
11501150+ | "m.text" -> Text
11511151+ | "m.emote" -> Emote
11521152+ | "m.notice" -> Notice
11531153+ | "m.image" -> Image
11541154+ | "m.file" -> File
11551155+ | "m.audio" -> Audio
11561156+ | "m.video" -> Video
11571157+ | "m.location" -> Location
11581158+ | s -> Custom s
11591159+11601160+ let jsont =
11611161+ Jsont.of_of_string ~kind:"msgtype"
11621162+ ~enc:to_string
11631163+ (fun s -> Ok (of_string s))
11641164+end
11651165+11661166+module Text_message_content = struct
11671167+ type t = {
11681168+ body : string;
11691169+ msgtype : Msgtype.t;
11701170+ format : string option;
11711171+ formatted_body : string option;
11721172+ }
11731173+11741174+ let jsont =
11751175+ Jsont.Object.(
11761176+ map (fun body msgtype format formatted_body ->
11771177+ { body; msgtype; format; formatted_body })
11781178+ |> mem "body" Jsont.string ~enc:(fun t -> t.body)
11791179+ |> mem "msgtype" Msgtype.jsont ~enc:(fun t -> t.msgtype)
11801180+ |> opt_mem "format" Jsont.string ~enc:(fun t -> t.format)
11811181+ |> opt_mem "formatted_body" Jsont.string ~enc:(fun t -> t.formatted_body)
11821182+ |> finish)
11831183+11841184+ let make ?(format = "org.matrix.custom.html") ?formatted_body body =
11851185+ let msgtype = Msgtype.Text in
11861186+ match formatted_body with
11871187+ | None -> { body; msgtype; format = None; formatted_body = None }
11881188+ | Some fb -> { body; msgtype; format = Some format; formatted_body = Some fb }
11891189+end
11901190+11911191+module Media_info = struct
11921192+ type t = {
11931193+ mimetype : string option;
11941194+ size : int option;
11951195+ duration : int option;
11961196+ h : int option;
11971197+ w : int option;
11981198+ thumbnail_url : string option;
11991199+ thumbnail_info : thumbnail_info option;
12001200+ }
12011201+ and thumbnail_info = {
12021202+ mimetype : string option;
12031203+ size : int option;
12041204+ h : int option;
12051205+ w : int option;
12061206+ }
12071207+12081208+ let thumbnail_info_jsont : thumbnail_info Jsont.t =
12091209+ Jsont.Object.(
12101210+ map (fun mimetype size h w -> { mimetype; size; h; w })
12111211+ |> opt_mem "mimetype" Jsont.string ~enc:(fun (t : thumbnail_info) -> t.mimetype)
12121212+ |> opt_mem "size" Jsont.int ~enc:(fun (t : thumbnail_info) -> t.size)
12131213+ |> opt_mem "h" Jsont.int ~enc:(fun (t : thumbnail_info) -> t.h)
12141214+ |> opt_mem "w" Jsont.int ~enc:(fun (t : thumbnail_info) -> t.w)
12151215+ |> finish)
12161216+12171217+ let jsont =
12181218+ Jsont.Object.(
12191219+ map (fun mimetype size duration h w thumbnail_url thumbnail_info ->
12201220+ { mimetype; size; duration; h; w; thumbnail_url; thumbnail_info })
12211221+ |> opt_mem "mimetype" Jsont.string ~enc:(fun t -> t.mimetype)
12221222+ |> opt_mem "size" Jsont.int ~enc:(fun t -> t.size)
12231223+ |> opt_mem "duration" Jsont.int ~enc:(fun t -> t.duration)
12241224+ |> opt_mem "h" Jsont.int ~enc:(fun t -> t.h)
12251225+ |> opt_mem "w" Jsont.int ~enc:(fun t -> t.w)
12261226+ |> opt_mem "thumbnail_url" Jsont.string ~enc:(fun t -> t.thumbnail_url)
12271227+ |> opt_mem "thumbnail_info" thumbnail_info_jsont ~enc:(fun t -> t.thumbnail_info)
12281228+ |> finish)
12291229+end
12301230+12311231+module Media_message_content = struct
12321232+ type t = {
12331233+ body : string;
12341234+ msgtype : Msgtype.t;
12351235+ url : string option;
12361236+ info : Media_info.t option;
12371237+ file : encrypted_file option;
12381238+ }
12391239+ and encrypted_file = {
12401240+ url : string;
12411241+ key : Jsont.json; (* JWK *)
12421242+ iv : string;
12431243+ hashes : (string * string) list;
12441244+ v : string;
12451245+ }
12461246+12471247+ module StringMap = Map.Make(String)
12481248+12491249+ let string_map_jsont =
12501250+ Jsont.Object.as_string_map Jsont.string
12511251+ |> Jsont.map
12521252+ ~dec:(fun m -> StringMap.bindings m)
12531253+ ~enc:(fun l -> List.to_seq l |> StringMap.of_seq)
12541254+12551255+ let encrypted_file_jsont : encrypted_file Jsont.t =
12561256+ Jsont.Object.(
12571257+ map (fun url key iv hashes v -> { url; key; iv; hashes; v })
12581258+ |> mem "url" Jsont.string ~enc:(fun (f : encrypted_file) -> f.url)
12591259+ |> mem "key" Jsont.json ~enc:(fun (f : encrypted_file) -> f.key)
12601260+ |> mem "iv" Jsont.string ~enc:(fun (f : encrypted_file) -> f.iv)
12611261+ |> mem "hashes" string_map_jsont ~enc:(fun (f : encrypted_file) -> f.hashes)
12621262+ |> mem "v" Jsont.string ~enc:(fun (f : encrypted_file) -> f.v)
12631263+ |> finish)
12641264+12651265+ let jsont =
12661266+ Jsont.Object.(
12671267+ map (fun body msgtype url info file ->
12681268+ { body; msgtype; url; info; file })
12691269+ |> mem "body" Jsont.string ~enc:(fun t -> t.body)
12701270+ |> mem "msgtype" Msgtype.jsont ~enc:(fun t -> t.msgtype)
12711271+ |> opt_mem "url" Jsont.string ~enc:(fun t -> t.url)
12721272+ |> opt_mem "info" Media_info.jsont ~enc:(fun t -> t.info)
12731273+ |> opt_mem "file" encrypted_file_jsont ~enc:(fun t -> t.file)
12741274+ |> finish)
12751275+end
12761276+12771277+(** {1 Sticker Content} *)
12781278+12791279+module Sticker_content = struct
12801280+ type t = {
12811281+ body : string;
12821282+ info : Media_info.t option;
12831283+ url : string;
12841284+ }
12851285+12861286+ let jsont =
12871287+ Jsont.Object.(
12881288+ map (fun body info url -> { body; info; url })
12891289+ |> mem "body" Jsont.string ~enc:(fun t -> t.body)
12901290+ |> opt_mem "info" Media_info.jsont ~enc:(fun t -> t.info)
12911291+ |> mem "url" Jsont.string ~enc:(fun t -> t.url)
12921292+ |> finish)
12931293+end
12941294+12951295+(** {1 Location Content} *)
12961296+12971297+module Location_message_content = struct
12981298+ type location_info = {
12991299+ uri : string;
13001300+ description : string option;
13011301+ }
13021302+13031303+ let location_info_jsont =
13041304+ Jsont.Object.(
13051305+ map (fun uri description -> { uri; description })
13061306+ |> mem "uri" Jsont.string ~enc:(fun t -> t.uri)
13071307+ |> opt_mem "description" Jsont.string ~enc:(fun t -> t.description)
13081308+ |> finish)
13091309+13101310+ type t = {
13111311+ body : string;
13121312+ msgtype : Msgtype.t;
13131313+ geo_uri : string;
13141314+ info : location_info option;
13151315+ }
13161316+13171317+ let jsont =
13181318+ Jsont.Object.(
13191319+ map (fun body msgtype geo_uri info ->
13201320+ { body; msgtype; geo_uri; info })
13211321+ |> mem "body" Jsont.string ~enc:(fun t -> t.body)
13221322+ |> mem "msgtype" Msgtype.jsont ~enc:(fun t -> t.msgtype)
13231323+ |> mem "geo_uri" Jsont.string ~enc:(fun t -> t.geo_uri)
13241324+ |> opt_mem "info" location_info_jsont ~enc:(fun t -> t.info)
13251325+ |> finish)
13261326+end
13271327+13281328+(** {1 Event Types} *)
13291329+13301330+module Event_type = struct
13311331+ type t =
13321332+ (* Room state events *)
13331333+ | Room_create
13341334+ | Room_name
13351335+ | Room_topic
13361336+ | Room_avatar
13371337+ | Room_member
13381338+ | Room_join_rules
13391339+ | Room_history_visibility
13401340+ | Room_canonical_alias
13411341+ | Room_power_levels
13421342+ | Room_encryption
13431343+ | Room_pinned_events
13441344+ | Room_server_acl
13451345+ | Room_tombstone
13461346+ | Room_guest_access
13471347+ (* Space events *)
13481348+ | Space_child
13491349+ | Space_parent
13501350+ (* Message events *)
13511351+ | Room_message
13521352+ | Room_message_encrypted
13531353+ | Room_redaction
13541354+ | Reaction
13551355+ | Sticker
13561356+ (* Call events *)
13571357+ | Call_invite
13581358+ | Call_candidates
13591359+ | Call_answer
13601360+ | Call_hangup
13611361+ | Call_reject
13621362+ | Call_select_answer
13631363+ | Call_negotiate
13641364+ | Call_member
13651365+ (* Key verification events *)
13661366+ | Key_verification_ready
13671367+ | Key_verification_start
13681368+ | Key_verification_accept
13691369+ | Key_verification_key
13701370+ | Key_verification_mac
13711371+ | Key_verification_cancel
13721372+ | Key_verification_done
13731373+ (* Policy rule events *)
13741374+ | Policy_rule_room
13751375+ | Policy_rule_server
13761376+ | Policy_rule_user
13771377+ (* To-device events *)
13781378+ | Room_key
13791379+ | Room_key_request
13801380+ | Forwarded_room_key
13811381+ | Dummy
13821382+ (* Ephemeral *)
13831383+ | Typing
13841384+ | Receipt
13851385+ | Presence
13861386+ (* Account data *)
13871387+ | Direct
13881388+ | Ignored_user_list
13891389+ | Fully_read
13901390+ | Marked_unread
13911391+ | Tag
13921392+ | Push_rules
13931393+ | Secret_storage_default_key
13941394+ | Secret_storage_key
13951395+ | Cross_signing_keys
13961396+ (* Beacon/location events *)
13971397+ | Beacon_info
13981398+ | Beacon
13991399+ (* Polls *)
14001400+ | Poll_start
14011401+ | Poll_response
14021402+ | Poll_end
14031403+ (* Custom *)
14041404+ | Custom of string
14051405+14061406+ let to_string = function
14071407+ | Room_create -> "m.room.create"
14081408+ | Room_name -> "m.room.name"
14091409+ | Room_topic -> "m.room.topic"
14101410+ | Room_avatar -> "m.room.avatar"
14111411+ | Room_member -> "m.room.member"
14121412+ | Room_join_rules -> "m.room.join_rules"
14131413+ | Room_history_visibility -> "m.room.history_visibility"
14141414+ | Room_canonical_alias -> "m.room.canonical_alias"
14151415+ | Room_power_levels -> "m.room.power_levels"
14161416+ | Room_encryption -> "m.room.encryption"
14171417+ | Room_pinned_events -> "m.room.pinned_events"
14181418+ | Room_server_acl -> "m.room.server_acl"
14191419+ | Room_tombstone -> "m.room.tombstone"
14201420+ | Room_guest_access -> "m.room.guest_access"
14211421+ | Space_child -> "m.space.child"
14221422+ | Space_parent -> "m.space.parent"
14231423+ | Room_message -> "m.room.message"
14241424+ | Room_message_encrypted -> "m.room.encrypted"
14251425+ | Room_redaction -> "m.room.redaction"
14261426+ | Reaction -> "m.reaction"
14271427+ | Sticker -> "m.sticker"
14281428+ | Call_invite -> "m.call.invite"
14291429+ | Call_candidates -> "m.call.candidates"
14301430+ | Call_answer -> "m.call.answer"
14311431+ | Call_hangup -> "m.call.hangup"
14321432+ | Call_reject -> "m.call.reject"
14331433+ | Call_select_answer -> "m.call.select_answer"
14341434+ | Call_negotiate -> "m.call.negotiate"
14351435+ | Call_member -> "m.call.member"
14361436+ | Key_verification_ready -> "m.key.verification.ready"
14371437+ | Key_verification_start -> "m.key.verification.start"
14381438+ | Key_verification_accept -> "m.key.verification.accept"
14391439+ | Key_verification_key -> "m.key.verification.key"
14401440+ | Key_verification_mac -> "m.key.verification.mac"
14411441+ | Key_verification_cancel -> "m.key.verification.cancel"
14421442+ | Key_verification_done -> "m.key.verification.done"
14431443+ | Policy_rule_room -> "m.policy.rule.room"
14441444+ | Policy_rule_server -> "m.policy.rule.server"
14451445+ | Policy_rule_user -> "m.policy.rule.user"
14461446+ | Room_key -> "m.room_key"
14471447+ | Room_key_request -> "m.room_key_request"
14481448+ | Forwarded_room_key -> "m.forwarded_room_key"
14491449+ | Dummy -> "m.dummy"
14501450+ | Typing -> "m.typing"
14511451+ | Receipt -> "m.receipt"
14521452+ | Presence -> "m.presence"
14531453+ | Direct -> "m.direct"
14541454+ | Ignored_user_list -> "m.ignored_user_list"
14551455+ | Fully_read -> "m.fully_read"
14561456+ | Marked_unread -> "m.marked_unread"
14571457+ | Tag -> "m.tag"
14581458+ | Push_rules -> "m.push_rules"
14591459+ | Secret_storage_default_key -> "m.secret_storage.default_key"
14601460+ | Secret_storage_key -> "m.secret_storage.key"
14611461+ | Cross_signing_keys -> "m.cross_signing.keys"
14621462+ | Beacon_info -> "org.matrix.msc3672.beacon_info"
14631463+ | Beacon -> "org.matrix.msc3672.beacon"
14641464+ | Poll_start -> "m.poll.start"
14651465+ | Poll_response -> "m.poll.response"
14661466+ | Poll_end -> "m.poll.end"
14671467+ | Custom s -> s
14681468+14691469+ let of_string = function
14701470+ | "m.room.create" -> Room_create
14711471+ | "m.room.name" -> Room_name
14721472+ | "m.room.topic" -> Room_topic
14731473+ | "m.room.avatar" -> Room_avatar
14741474+ | "m.room.member" -> Room_member
14751475+ | "m.room.join_rules" -> Room_join_rules
14761476+ | "m.room.history_visibility" -> Room_history_visibility
14771477+ | "m.room.canonical_alias" -> Room_canonical_alias
14781478+ | "m.room.power_levels" -> Room_power_levels
14791479+ | "m.room.encryption" -> Room_encryption
14801480+ | "m.room.pinned_events" -> Room_pinned_events
14811481+ | "m.room.server_acl" -> Room_server_acl
14821482+ | "m.room.tombstone" -> Room_tombstone
14831483+ | "m.room.guest_access" -> Room_guest_access
14841484+ | "m.space.child" -> Space_child
14851485+ | "m.space.parent" -> Space_parent
14861486+ | "m.room.message" -> Room_message
14871487+ | "m.room.encrypted" -> Room_message_encrypted
14881488+ | "m.room.redaction" -> Room_redaction
14891489+ | "m.reaction" -> Reaction
14901490+ | "m.sticker" -> Sticker
14911491+ | "m.call.invite" -> Call_invite
14921492+ | "m.call.candidates" -> Call_candidates
14931493+ | "m.call.answer" -> Call_answer
14941494+ | "m.call.hangup" -> Call_hangup
14951495+ | "m.call.reject" -> Call_reject
14961496+ | "m.call.select_answer" -> Call_select_answer
14971497+ | "m.call.negotiate" -> Call_negotiate
14981498+ | "m.call.member" -> Call_member
14991499+ | "m.key.verification.ready" -> Key_verification_ready
15001500+ | "m.key.verification.start" -> Key_verification_start
15011501+ | "m.key.verification.accept" -> Key_verification_accept
15021502+ | "m.key.verification.key" -> Key_verification_key
15031503+ | "m.key.verification.mac" -> Key_verification_mac
15041504+ | "m.key.verification.cancel" -> Key_verification_cancel
15051505+ | "m.key.verification.done" -> Key_verification_done
15061506+ | "m.policy.rule.room" -> Policy_rule_room
15071507+ | "m.policy.rule.server" -> Policy_rule_server
15081508+ | "m.policy.rule.user" -> Policy_rule_user
15091509+ | "m.room_key" -> Room_key
15101510+ | "m.room_key_request" -> Room_key_request
15111511+ | "m.forwarded_room_key" -> Forwarded_room_key
15121512+ | "m.dummy" -> Dummy
15131513+ | "m.typing" -> Typing
15141514+ | "m.receipt" -> Receipt
15151515+ | "m.presence" -> Presence
15161516+ | "m.direct" -> Direct
15171517+ | "m.ignored_user_list" -> Ignored_user_list
15181518+ | "m.fully_read" -> Fully_read
15191519+ | "m.marked_unread" -> Marked_unread
15201520+ | "m.tag" -> Tag
15211521+ | "m.push_rules" -> Push_rules
15221522+ | "m.secret_storage.default_key" -> Secret_storage_default_key
15231523+ | "m.secret_storage.key" -> Secret_storage_key
15241524+ | "m.cross_signing.keys" -> Cross_signing_keys
15251525+ | "org.matrix.msc3672.beacon_info" -> Beacon_info
15261526+ | "org.matrix.msc3672.beacon" -> Beacon
15271527+ | "m.poll.start" -> Poll_start
15281528+ | "m.poll.response" -> Poll_response
15291529+ | "m.poll.end" -> Poll_end
15301530+ | s -> Custom s
15311531+15321532+ let jsont =
15331533+ Jsont.of_of_string ~kind:"event_type"
15341534+ ~enc:to_string
15351535+ (fun s -> Ok (of_string s))
15361536+end
15371537+15381538+(** {1 State Events} *)
15391539+15401540+module State_event = struct
15411541+ (** A room state event with typed content. *)
15421542+15431543+ type 'content t = {
15441544+ event_id : Event_id.t;
15451545+ sender : User_id.t;
15461546+ origin_server_ts : Timestamp.t;
15471547+ state_key : string;
15481548+ type_ : Event_type.t;
15491549+ content : 'content;
15501550+ unsigned : Unsigned.t option;
15511551+ }
15521552+15531553+ let make_jsont content_jsont =
15541554+ Jsont.Object.(
15551555+ map (fun event_id sender origin_server_ts state_key type_ content unsigned ->
15561556+ { event_id; sender; origin_server_ts; state_key; type_; content; unsigned })
15571557+ |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id)
15581558+ |> mem "sender" User_id.jsont ~enc:(fun t -> t.sender)
15591559+ |> mem "origin_server_ts" Timestamp.jsont ~enc:(fun t -> t.origin_server_ts)
15601560+ |> mem "state_key" Jsont.string ~enc:(fun t -> t.state_key)
15611561+ |> mem "type" Event_type.jsont ~enc:(fun t -> t.type_)
15621562+ |> mem "content" content_jsont ~enc:(fun t -> t.content)
15631563+ |> opt_mem "unsigned" Unsigned.jsont ~enc:(fun t -> t.unsigned)
15641564+ |> finish)
15651565+end
15661566+15671567+(** {1 Room Events} *)
15681568+15691569+module Room_event = struct
15701570+ (** A room event (timeline event) with typed content. *)
15711571+15721572+ type 'content t = {
15731573+ event_id : Event_id.t;
15741574+ sender : User_id.t;
15751575+ origin_server_ts : Timestamp.t;
15761576+ type_ : Event_type.t;
15771577+ content : 'content;
15781578+ unsigned : Unsigned.t option;
15791579+ }
15801580+15811581+ let make_jsont content_jsont =
15821582+ Jsont.Object.(
15831583+ map (fun event_id sender origin_server_ts type_ content unsigned ->
15841584+ { event_id; sender; origin_server_ts; type_; content; unsigned })
15851585+ |> mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id)
15861586+ |> mem "sender" User_id.jsont ~enc:(fun t -> t.sender)
15871587+ |> mem "origin_server_ts" Timestamp.jsont ~enc:(fun t -> t.origin_server_ts)
15881588+ |> mem "type" Event_type.jsont ~enc:(fun t -> t.type_)
15891589+ |> mem "content" content_jsont ~enc:(fun t -> t.content)
15901590+ |> opt_mem "unsigned" Unsigned.jsont ~enc:(fun t -> t.unsigned)
15911591+ |> finish)
15921592+end
15931593+15941594+(** {1 Raw/Untyped Events}
15951595+15961596+ For events where we don't know the content type ahead of time. *)
15971597+15981598+module Raw_event = struct
15991599+ type t = {
16001600+ event_id : Event_id.t option;
16011601+ sender : User_id.t;
16021602+ origin_server_ts : Timestamp.t;
16031603+ type_ : Event_type.t;
16041604+ state_key : string option;
16051605+ content : Jsont.json;
16061606+ unsigned : Unsigned.t option;
16071607+ room_id : Room_id.t option;
16081608+ }
16091609+16101610+ let jsont =
16111611+ Jsont.Object.(
16121612+ map (fun event_id sender origin_server_ts type_ state_key content unsigned room_id ->
16131613+ { event_id; sender; origin_server_ts; type_; state_key; content; unsigned; room_id })
16141614+ |> opt_mem "event_id" Event_id.jsont ~enc:(fun t -> t.event_id)
16151615+ |> mem "sender" User_id.jsont ~enc:(fun t -> t.sender)
16161616+ |> mem "origin_server_ts" Timestamp.jsont ~enc:(fun t -> t.origin_server_ts)
16171617+ |> mem "type" Event_type.jsont ~enc:(fun t -> t.type_)
16181618+ |> opt_mem "state_key" Jsont.string ~enc:(fun t -> t.state_key)
16191619+ |> mem "content" Jsont.json ~enc:(fun t -> t.content)
16201620+ |> opt_mem "unsigned" Unsigned.jsont ~enc:(fun t -> t.unsigned)
16211621+ |> opt_mem "room_id" Room_id.jsont ~enc:(fun t -> t.room_id)
16221622+ |> finish)
16231623+end
+611
lib/matrix_proto/matrix_event.mli
···11+(** Matrix event types with JSON codecs. *)
22+33+open Matrix_id
44+55+(** {1 Timestamps} *)
66+77+module Timestamp : sig
88+ type t = int64
99+ val of_ptime : Ptime.t -> t
1010+ val to_ptime_opt : t -> Ptime.t option
1111+ val jsont : t Jsont.t
1212+end
1313+1414+(** {1 Unsigned Event Data} *)
1515+1616+module Unsigned : sig
1717+ type t = {
1818+ age : int64 option;
1919+ prev_content : Jsont.json option;
2020+ prev_sender : User_id.t option;
2121+ redacted_because : Jsont.json option;
2222+ transaction_id : Transaction_id.t option;
2323+ }
2424+ val empty : t
2525+ val jsont : t Jsont.t
2626+end
2727+2828+(** {1 Room Membership} *)
2929+3030+module Membership : sig
3131+ type t = Join | Invite | Leave | Ban | Knock
3232+ val to_string : t -> string
3333+ val of_string : string -> (t, [> `Unknown_membership of string ]) result
3434+ val jsont : t Jsont.t
3535+end
3636+3737+(** {1 Join Rules} *)
3838+3939+module Join_rule : sig
4040+ type t = Public | Invite | Knock | Restricted | Knock_restricted | Private
4141+ val jsont : t Jsont.t
4242+end
4343+4444+(** {1 History Visibility} *)
4545+4646+module History_visibility : sig
4747+ type t = Invited | Joined | Shared | World_readable
4848+ val jsont : t Jsont.t
4949+end
5050+5151+(** {1 Room State Event Contents} *)
5252+5353+module Room_create_content : sig
5454+ type t = {
5555+ creator : User_id.t option;
5656+ room_version : string option;
5757+ predecessor : predecessor option;
5858+ type_ : string option;
5959+ }
6060+ and predecessor = {
6161+ room_id : Room_id.t;
6262+ event_id : Event_id.t;
6363+ }
6464+ val jsont : t Jsont.t
6565+end
6666+6767+module Room_name_content : sig
6868+ type t = { name : string }
6969+ val jsont : t Jsont.t
7070+end
7171+7272+module Room_topic_content : sig
7373+ type t = { topic : string }
7474+ val jsont : t Jsont.t
7575+end
7676+7777+module Room_avatar_content : sig
7878+ type t = {
7979+ url : string option;
8080+ info : image_info option;
8181+ }
8282+ and image_info = {
8383+ h : int option;
8484+ w : int option;
8585+ mimetype : string option;
8686+ size : int option;
8787+ }
8888+ val jsont : t Jsont.t
8989+end
9090+9191+module Room_member_content : sig
9292+ type t = {
9393+ membership : Membership.t;
9494+ displayname : string option;
9595+ avatar_url : string option;
9696+ is_direct : bool option;
9797+ reason : string option;
9898+ }
9999+ val jsont : t Jsont.t
100100+end
101101+102102+module Room_join_rules_content : sig
103103+ type t = {
104104+ join_rule : Join_rule.t;
105105+ allow : allow_condition list option;
106106+ }
107107+ and allow_condition = {
108108+ type_ : string;
109109+ room_id : Room_id.t option;
110110+ }
111111+ val jsont : t Jsont.t
112112+end
113113+114114+module Room_history_visibility_content : sig
115115+ type t = { history_visibility : History_visibility.t }
116116+ val jsont : t Jsont.t
117117+end
118118+119119+module Room_canonical_alias_content : sig
120120+ type t = {
121121+ alias : Room_alias.t option;
122122+ alt_aliases : Room_alias.t list option;
123123+ }
124124+ val jsont : t Jsont.t
125125+end
126126+127127+module Room_power_levels_content : sig
128128+ type t = {
129129+ ban : int option;
130130+ events : (string * int) list option;
131131+ events_default : int option;
132132+ invite : int option;
133133+ kick : int option;
134134+ redact : int option;
135135+ state_default : int option;
136136+ users : (string * int) list option;
137137+ users_default : int option;
138138+ }
139139+ val jsont : t Jsont.t
140140+end
141141+142142+module Room_encryption_content : sig
143143+ type t = {
144144+ algorithm : string;
145145+ rotation_period_ms : int64 option;
146146+ rotation_period_msgs : int option;
147147+ }
148148+ val jsont : t Jsont.t
149149+end
150150+151151+module Room_pinned_events_content : sig
152152+ type t = { pinned : string list }
153153+ val jsont : t Jsont.t
154154+end
155155+156156+module Room_server_acl_content : sig
157157+ type t = {
158158+ allow : string list;
159159+ allow_ip_literals : bool;
160160+ deny : string list;
161161+ }
162162+ val jsont : t Jsont.t
163163+end
164164+165165+module Room_tombstone_content : sig
166166+ type t = {
167167+ body : string;
168168+ replacement_room : Room_id.t;
169169+ }
170170+ val jsont : t Jsont.t
171171+end
172172+173173+module Room_guest_access_content : sig
174174+ type access = Can_join | Forbidden
175175+ val access_jsont : access Jsont.t
176176+ type t = { guest_access : access }
177177+ val jsont : t Jsont.t
178178+end
179179+180180+(** {1 Space Event Contents} *)
181181+182182+module Space_child_content : sig
183183+ type t = {
184184+ via : string list option;
185185+ order : string option;
186186+ suggested : bool option;
187187+ }
188188+ val jsont : t Jsont.t
189189+end
190190+191191+module Space_parent_content : sig
192192+ type t = {
193193+ via : string list option;
194194+ canonical : bool option;
195195+ }
196196+ val jsont : t Jsont.t
197197+end
198198+199199+(** {1 Call Event Contents} *)
200200+201201+module Call_invite_content : sig
202202+ type sdp_content = { type_ : string; sdp : string }
203203+ type t = {
204204+ call_id : string;
205205+ party_id : string option;
206206+ version : int;
207207+ lifetime : int;
208208+ offer : sdp_content;
209209+ invitee : string option;
210210+ }
211211+ val jsont : t Jsont.t
212212+end
213213+214214+module Call_answer_content : sig
215215+ type sdp_content = { type_ : string; sdp : string }
216216+ type t = {
217217+ call_id : string;
218218+ party_id : string option;
219219+ version : int;
220220+ answer : sdp_content;
221221+ }
222222+ val jsont : t Jsont.t
223223+end
224224+225225+module Call_hangup_content : sig
226226+ type reason =
227227+ | Ice_failed | Invite_timeout | User_hangup
228228+ | User_media_failed | User_busy | Unknown_error
229229+ type t = {
230230+ call_id : string;
231231+ party_id : string option;
232232+ version : int;
233233+ reason : reason option;
234234+ }
235235+ val jsont : t Jsont.t
236236+end
237237+238238+module Call_candidates_content : sig
239239+ type candidate = {
240240+ candidate : string;
241241+ sdp_mid : string;
242242+ sdp_m_line_index : int;
243243+ }
244244+ type t = {
245245+ call_id : string;
246246+ party_id : string option;
247247+ version : int;
248248+ candidates : candidate list;
249249+ }
250250+ val jsont : t Jsont.t
251251+end
252252+253253+(** {1 Call Member Content (m.call.member)} *)
254254+255255+module Call_member_content : sig
256256+ type focus = {
257257+ type_ : string;
258258+ livekit_service_url : string option;
259259+ livekit_alias : string option;
260260+ }
261261+ type membership = {
262262+ call_id : string;
263263+ scope : string;
264264+ application : string;
265265+ device_id : string;
266266+ expires : int64;
267267+ foci_active : focus list option;
268268+ membership_id : string option;
269269+ }
270270+ type t = { memberships : membership list }
271271+ val jsont : t Jsont.t
272272+end
273273+274274+(** {1 Key Verification Event Contents} *)
275275+276276+module Key_verification_ready_content : sig
277277+ type t = {
278278+ from_device : string;
279279+ methods : string list;
280280+ transaction_id : string option;
281281+ }
282282+ val jsont : t Jsont.t
283283+end
284284+285285+module Key_verification_start_content : sig
286286+ type t = {
287287+ from_device : string;
288288+ method_ : string;
289289+ transaction_id : string option;
290290+ key_agreement_protocols : string list option;
291291+ hashes : string list option;
292292+ message_authentication_codes : string list option;
293293+ short_authentication_string : string list option;
294294+ }
295295+ val jsont : t Jsont.t
296296+end
297297+298298+module Key_verification_accept_content : sig
299299+ type t = {
300300+ transaction_id : string option;
301301+ method_ : string;
302302+ key_agreement_protocol : string;
303303+ hash : string;
304304+ message_authentication_code : string;
305305+ short_authentication_string : string list;
306306+ commitment : string;
307307+ }
308308+ val jsont : t Jsont.t
309309+end
310310+311311+module Key_verification_key_content : sig
312312+ type t = {
313313+ transaction_id : string option;
314314+ key : string;
315315+ }
316316+ val jsont : t Jsont.t
317317+end
318318+319319+module Key_verification_mac_content : sig
320320+ type t = {
321321+ transaction_id : string option;
322322+ mac : (string * string) list;
323323+ keys : string;
324324+ }
325325+ val jsont : t Jsont.t
326326+end
327327+328328+module Key_verification_cancel_content : sig
329329+ type t = {
330330+ transaction_id : string option;
331331+ code : string;
332332+ reason : string;
333333+ }
334334+ val jsont : t Jsont.t
335335+end
336336+337337+module Key_verification_done_content : sig
338338+ type t = { transaction_id : string option }
339339+ val jsont : t Jsont.t
340340+end
341341+342342+(** {1 Policy Rule Event Contents} *)
343343+344344+module Policy_rule_content : sig
345345+ type recommendation = Ban | Unknown of string
346346+ val recommendation_to_string : recommendation -> string
347347+ val recommendation_of_string : string -> recommendation
348348+ type t = {
349349+ entity : string;
350350+ reason : string;
351351+ recommendation : recommendation;
352352+ }
353353+ val jsont : t Jsont.t
354354+end
355355+356356+(** {1 Account Data Contents} *)
357357+358358+module Marked_unread_content : sig
359359+ type t = { unread : bool }
360360+ val jsont : t Jsont.t
361361+end
362362+363363+(** {1 Encrypted Event Content} *)
364364+365365+module Encrypted_content : sig
366366+ type algorithm =
367367+ | Olm_v1_curve25519_aes_sha2_256
368368+ | Megolm_v1_aes_sha2_256
369369+ | Unknown of string
370370+ val algorithm_to_string : algorithm -> string
371371+ val algorithm_of_string : string -> algorithm
372372+ type t = {
373373+ algorithm : algorithm;
374374+ sender_key : string;
375375+ ciphertext : Jsont.json;
376376+ session_id : string option;
377377+ device_id : string option;
378378+ }
379379+ val jsont : t Jsont.t
380380+end
381381+382382+(** {1 Reaction Content} *)
383383+384384+module Reaction_content : sig
385385+ type relates_to = {
386386+ rel_type : string;
387387+ event_id : Event_id.t;
388388+ key : string;
389389+ }
390390+ type t = { relates_to : relates_to }
391391+ val jsont : t Jsont.t
392392+end
393393+394394+(** {1 Beacon/Live Location Content} *)
395395+396396+module Beacon_info_content : sig
397397+ type t = {
398398+ description : string option;
399399+ live : bool;
400400+ timeout : int64;
401401+ asset_type : string option;
402402+ }
403403+ val jsont : t Jsont.t
404404+end
405405+406406+module Beacon_content : sig
407407+ type location = { uri : string; description : string option }
408408+ type relates_to = { rel_type : string; event_id : Event_id.t }
409409+ type t = {
410410+ location : location;
411411+ timestamp : int64;
412412+ relates_to : relates_to;
413413+ }
414414+ val jsont : t Jsont.t
415415+end
416416+417417+(** {1 Poll Event Contents} *)
418418+419419+module Poll_start_content : sig
420420+ type poll_answer = { id : string; text : string }
421421+ type poll_kind = Disclosed | Undisclosed
422422+ type poll_start = {
423423+ question : string;
424424+ kind : poll_kind;
425425+ max_selections : int;
426426+ answers : poll_answer list;
427427+ }
428428+ type t = { poll_start : poll_start; text : string }
429429+ val jsont : t Jsont.t
430430+end
431431+432432+module Poll_response_content : sig
433433+ type relates_to = { rel_type : string; event_id : Event_id.t }
434434+ type t = { relates_to : relates_to; answers : string list }
435435+ val jsont : t Jsont.t
436436+end
437437+438438+module Poll_end_content : sig
439439+ type relates_to = { rel_type : string; event_id : Event_id.t }
440440+ type t = { relates_to : relates_to; text : string }
441441+ val jsont : t Jsont.t
442442+end
443443+444444+(** {1 Message Event Contents} *)
445445+446446+module Msgtype : sig
447447+ type t =
448448+ | Text | Emote | Notice | Image | File | Audio | Video | Location
449449+ | Custom of string
450450+ val to_string : t -> string
451451+ val of_string : string -> t
452452+ val jsont : t Jsont.t
453453+end
454454+455455+module Text_message_content : sig
456456+ type t = {
457457+ body : string;
458458+ msgtype : Msgtype.t;
459459+ format : string option;
460460+ formatted_body : string option;
461461+ }
462462+ val jsont : t Jsont.t
463463+ val make : ?format:string -> ?formatted_body:string -> string -> t
464464+end
465465+466466+module Media_info : sig
467467+ type t = {
468468+ mimetype : string option;
469469+ size : int option;
470470+ duration : int option;
471471+ h : int option;
472472+ w : int option;
473473+ thumbnail_url : string option;
474474+ thumbnail_info : thumbnail_info option;
475475+ }
476476+ and thumbnail_info = {
477477+ mimetype : string option;
478478+ size : int option;
479479+ h : int option;
480480+ w : int option;
481481+ }
482482+ val jsont : t Jsont.t
483483+end
484484+485485+module Media_message_content : sig
486486+ type t = {
487487+ body : string;
488488+ msgtype : Msgtype.t;
489489+ url : string option;
490490+ info : Media_info.t option;
491491+ file : encrypted_file option;
492492+ }
493493+ and encrypted_file = {
494494+ url : string;
495495+ key : Jsont.json;
496496+ iv : string;
497497+ hashes : (string * string) list;
498498+ v : string;
499499+ }
500500+ val jsont : t Jsont.t
501501+end
502502+503503+(** {1 Sticker Content} *)
504504+505505+module Sticker_content : sig
506506+ type t = {
507507+ body : string;
508508+ info : Media_info.t option;
509509+ url : string;
510510+ }
511511+ val jsont : t Jsont.t
512512+end
513513+514514+(** {1 Location Content} *)
515515+516516+module Location_message_content : sig
517517+ type location_info = { uri : string; description : string option }
518518+ type t = {
519519+ body : string;
520520+ msgtype : Msgtype.t;
521521+ geo_uri : string;
522522+ info : location_info option;
523523+ }
524524+ val jsont : t Jsont.t
525525+end
526526+527527+(** {1 Event Types} *)
528528+529529+module Event_type : sig
530530+ type t =
531531+ (* Room state events *)
532532+ | Room_create | Room_name | Room_topic | Room_avatar | Room_member
533533+ | Room_join_rules | Room_history_visibility | Room_canonical_alias
534534+ | Room_power_levels | Room_encryption | Room_pinned_events
535535+ | Room_server_acl | Room_tombstone | Room_guest_access
536536+ (* Space events *)
537537+ | Space_child | Space_parent
538538+ (* Message events *)
539539+ | Room_message | Room_message_encrypted | Room_redaction
540540+ | Reaction | Sticker
541541+ (* Call events *)
542542+ | Call_invite | Call_candidates | Call_answer | Call_hangup
543543+ | Call_reject | Call_select_answer | Call_negotiate | Call_member
544544+ (* Key verification events *)
545545+ | Key_verification_ready | Key_verification_start | Key_verification_accept
546546+ | Key_verification_key | Key_verification_mac | Key_verification_cancel
547547+ | Key_verification_done
548548+ (* Policy rule events *)
549549+ | Policy_rule_room | Policy_rule_server | Policy_rule_user
550550+ (* To-device events *)
551551+ | Room_key | Room_key_request | Forwarded_room_key | Dummy
552552+ (* Ephemeral *)
553553+ | Typing | Receipt | Presence
554554+ (* Account data *)
555555+ | Direct | Ignored_user_list | Fully_read | Marked_unread | Tag | Push_rules
556556+ | Secret_storage_default_key | Secret_storage_key | Cross_signing_keys
557557+ (* Beacon/location events *)
558558+ | Beacon_info | Beacon
559559+ (* Polls *)
560560+ | Poll_start | Poll_response | Poll_end
561561+ (* Custom *)
562562+ | Custom of string
563563+ val to_string : t -> string
564564+ val of_string : string -> t
565565+ val jsont : t Jsont.t
566566+end
567567+568568+(** {1 State Events} *)
569569+570570+module State_event : sig
571571+ type 'content t = {
572572+ event_id : Event_id.t;
573573+ sender : User_id.t;
574574+ origin_server_ts : Timestamp.t;
575575+ state_key : string;
576576+ type_ : Event_type.t;
577577+ content : 'content;
578578+ unsigned : Unsigned.t option;
579579+ }
580580+ val make_jsont : 'content Jsont.t -> 'content t Jsont.t
581581+end
582582+583583+(** {1 Room Events} *)
584584+585585+module Room_event : sig
586586+ type 'content t = {
587587+ event_id : Event_id.t;
588588+ sender : User_id.t;
589589+ origin_server_ts : Timestamp.t;
590590+ type_ : Event_type.t;
591591+ content : 'content;
592592+ unsigned : Unsigned.t option;
593593+ }
594594+ val make_jsont : 'content Jsont.t -> 'content t Jsont.t
595595+end
596596+597597+(** {1 Raw Events} *)
598598+599599+module Raw_event : sig
600600+ type t = {
601601+ event_id : Event_id.t option;
602602+ sender : User_id.t;
603603+ origin_server_ts : Timestamp.t;
604604+ type_ : Event_type.t;
605605+ state_key : string option;
606606+ content : Jsont.json;
607607+ unsigned : Unsigned.t option;
608608+ room_id : Room_id.t option;
609609+ }
610610+ val jsont : t Jsont.t
611611+end
+421
lib/matrix_proto/matrix_id.ml
···11+(** Matrix identifiers with validation and JSON codecs.
22+33+ Matrix uses several types of identifiers that follow specific formats
44+ as defined in the Matrix specification:
55+66+ - {b User IDs}: [@localpart:server_name] - Identify users uniquely
77+ - {b Room IDs}: [!opaque_id:server_name] - Identify rooms uniquely
88+ - {b Event IDs}: [$opaque_id] or [$opaque_id:server_name] - Identify events
99+ - {b Room Aliases}: [#alias:server_name] - Human-readable room references
1010+ - {b Device IDs}: Opaque strings identifying client devices
1111+1212+ All identifiers are case-sensitive. Server names follow DNS syntax
1313+ with optional port numbers.
1414+1515+ @see <https://spec.matrix.org/v1.11/appendices/#identifier-grammar> Identifier Grammar
1616+ @see <https://spec.matrix.org/v1.11/appendices/#common-identifier-format> Common Identifier Format *)
1717+1818+(** {1 Server Names}
1919+2020+ Server names identify Matrix homeservers and follow a DNS-like format.
2121+ They consist of a hostname and optional port, e.g., [matrix.org] or
2222+ [matrix.org:8448].
2323+2424+ @see <https://spec.matrix.org/v1.11/appendices/#server-name> Server Name *)
2525+2626+module Server_name = struct
2727+ (** A Matrix server name (hostname with optional port).
2828+2929+ Server names are used as the domain part of user IDs, room IDs, and
3030+ other identifiers. They follow DNS hostname rules with an optional
3131+ port number suffix. *)
3232+ type t = string
3333+3434+ (** Check if a character is valid in a server name. *)
3535+ let is_valid_char = function
3636+ | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '-' | '.' | ':' | '[' | ']' -> true
3737+ | _ -> false
3838+3939+ (** Check if a string is a valid server name. *)
4040+ let is_valid s =
4141+ String.length s > 0 && String.for_all is_valid_char s
4242+4343+ (** Parse a server name from a string.
4444+ @return [Ok t] if valid, [Error `Invalid_server_name] otherwise *)
4545+ let of_string s =
4646+ if is_valid s then Ok s
4747+ else Error (`Invalid_server_name s)
4848+4949+ (** Parse a server name, raising [Invalid_argument] on failure. *)
5050+ let of_string_exn s =
5151+ match of_string s with
5252+ | Ok t -> t
5353+ | Error (`Invalid_server_name s) ->
5454+ invalid_arg (Printf.sprintf "Invalid server name: %s" s)
5555+5656+ (** Convert to string representation. *)
5757+ let to_string t = t
5858+5959+ (** JSON codec for server names. *)
6060+ let jsont =
6161+ Jsont.of_of_string ~kind:"server_name"
6262+ ~enc:to_string
6363+ (fun s -> Result.map_error (fun (`Invalid_server_name s) -> s) (of_string s))
6464+end
6565+6666+(** {1 User IDs}
6767+6868+ User IDs uniquely identify Matrix users. They have the format
6969+ [@localpart:server_name], where the localpart consists of lowercase
7070+ letters, digits, and certain special characters.
7171+7272+ @see <https://spec.matrix.org/v1.11/appendices/#user-identifiers> User Identifiers *)
7373+7474+module User_id = struct
7575+ (** A Matrix user ID (e.g., [@alice:matrix.org]).
7676+7777+ User IDs consist of a sigil [@], a localpart, a colon, and a server name.
7878+ The localpart is case-sensitive and may contain: [a-z0-9._=\-/+] *)
7979+ type t = { localpart : string; server_name : Server_name.t }
8080+8181+ (** The user ID sigil character. *)
8282+ let sigil = '@'
8383+8484+ (** Check if a character is valid in a user ID localpart.
8585+ Valid characters are lowercase letters, digits, and: [._=\-/+] *)
8686+ let is_valid_localpart_char = function
8787+ | 'a' .. 'z' | '0' .. '9' | '.' | '_' | '=' | '-' | '/' | '+' -> true
8888+ | _ -> false
8989+9090+ (** Parse a user ID from its string representation.
9191+ @return [Ok t] if valid, [Error `Invalid_user_id reason] otherwise *)
9292+ let of_string s =
9393+ if String.length s < 2 then
9494+ Error (`Invalid_user_id "too short")
9595+ else if s.[0] <> sigil then
9696+ Error (`Invalid_user_id "must start with @")
9797+ else
9898+ match String.index_opt s ':' with
9999+ | None -> Error (`Invalid_user_id "missing colon separator")
100100+ | Some colon_pos ->
101101+ let localpart = String.sub s 1 (colon_pos - 1) in
102102+ let server_part = String.sub s (colon_pos + 1) (String.length s - colon_pos - 1) in
103103+ if String.length localpart = 0 then
104104+ Error (`Invalid_user_id "empty localpart")
105105+ else if not (String.for_all is_valid_localpart_char localpart) then
106106+ Error (`Invalid_user_id "invalid characters in localpart")
107107+ else
108108+ match Server_name.of_string server_part with
109109+ | Error _ -> Error (`Invalid_user_id "invalid server name")
110110+ | Ok server_name -> Ok { localpart; server_name }
111111+112112+ (** Parse a user ID, raising [Invalid_argument] on failure. *)
113113+ let of_string_exn s =
114114+ match of_string s with
115115+ | Ok t -> t
116116+ | Error (`Invalid_user_id msg) ->
117117+ invalid_arg (Printf.sprintf "Invalid user ID '%s': %s" s msg)
118118+119119+ (** Convert to the canonical string representation. *)
120120+ let to_string { localpart; server_name } =
121121+ Printf.sprintf "%c%s:%s" sigil localpart (Server_name.to_string server_name)
122122+123123+ (** Get the localpart (the part before the colon, without the sigil). *)
124124+ let localpart t = t.localpart
125125+126126+ (** Get the server name (the part after the colon). *)
127127+ let server_name t = t.server_name
128128+129129+ (** JSON codec for user IDs. *)
130130+ let jsont =
131131+ Jsont.of_of_string ~kind:"user_id"
132132+ ~enc:to_string
133133+ (fun s -> Result.map_error (fun (`Invalid_user_id msg) -> msg) (of_string s))
134134+end
135135+136136+(** {1 Room IDs}
137137+138138+ Room IDs uniquely identify Matrix rooms. They have the format
139139+ [!opaque_id:server_name], where the opaque_id is generated by
140140+ the creating server.
141141+142142+ @see <https://spec.matrix.org/v1.11/appendices/#room-ids> Room IDs *)
143143+144144+module Room_id = struct
145145+ (** A Matrix room ID (e.g., [!abcdef:matrix.org]).
146146+147147+ Room IDs are assigned by the server that creates the room and
148148+ are globally unique. The opaque_id portion is server-generated. *)
149149+ type t = { opaque_id : string; server_name : Server_name.t }
150150+151151+ (** The room ID sigil character. *)
152152+ let sigil = '!'
153153+154154+ (** Parse a room ID from its string representation.
155155+ @return [Ok t] if valid, [Error `Invalid_room_id reason] otherwise *)
156156+ let of_string s =
157157+ if String.length s < 2 then
158158+ Error (`Invalid_room_id "too short")
159159+ else if s.[0] <> sigil then
160160+ Error (`Invalid_room_id "must start with !")
161161+ else
162162+ match String.index_opt s ':' with
163163+ | None -> Error (`Invalid_room_id "missing colon separator")
164164+ | Some colon_pos ->
165165+ let opaque_id = String.sub s 1 (colon_pos - 1) in
166166+ let server_part = String.sub s (colon_pos + 1) (String.length s - colon_pos - 1) in
167167+ match Server_name.of_string server_part with
168168+ | Error _ -> Error (`Invalid_room_id "invalid server name")
169169+ | Ok server_name -> Ok { opaque_id; server_name }
170170+171171+ (** Parse a room ID, raising [Invalid_argument] on failure. *)
172172+ let of_string_exn s =
173173+ match of_string s with
174174+ | Ok t -> t
175175+ | Error (`Invalid_room_id msg) ->
176176+ invalid_arg (Printf.sprintf "Invalid room ID '%s': %s" s msg)
177177+178178+ (** Convert to the canonical string representation. *)
179179+ let to_string { opaque_id; server_name } =
180180+ Printf.sprintf "%c%s:%s" sigil opaque_id (Server_name.to_string server_name)
181181+182182+ (** Get the opaque ID portion (server-generated, without sigil). *)
183183+ let opaque_id t = t.opaque_id
184184+185185+ (** Get the server name of the room's creating server. *)
186186+ let server_name t = t.server_name
187187+188188+ (** JSON codec for room IDs. *)
189189+ let jsont =
190190+ Jsont.of_of_string ~kind:"room_id"
191191+ ~enc:to_string
192192+ (fun s -> Result.map_error (fun (`Invalid_room_id msg) -> msg) (of_string s))
193193+end
194194+195195+(** {1 Event IDs}
196196+197197+ Event IDs uniquely identify events within Matrix. The format varies
198198+ by room version:
199199+ - Versions 1-3: [$opaque_id:server_name]
200200+ - Version 4+: [$base64_opaque_id] (no server name, uses reference hashing)
201201+202202+ @see <https://spec.matrix.org/v1.11/appendices/#event-ids> Event IDs
203203+ @see <https://spec.matrix.org/v1.11/rooms/> Room Versions *)
204204+205205+module Event_id = struct
206206+ (** A Matrix event ID.
207207+208208+ Event IDs can be in two formats depending on the room version:
209209+ - {b V1}: [$opaque_id:server_name] (room versions 1-3)
210210+ - {b V4}: [$base64_opaque_id] (room version 4+, using reference hashing) *)
211211+ type t =
212212+ | V1 of { opaque_id : string; server_name : Server_name.t }
213213+ | V4 of { opaque_id : string }
214214+215215+ (** The event ID sigil character. *)
216216+ let sigil = '$'
217217+218218+ (** Parse an event ID from its string representation.
219219+ Automatically detects V1 vs V4 format based on presence of colon.
220220+ @return [Ok t] if valid, [Error `Invalid_event_id reason] otherwise *)
221221+ let of_string s =
222222+ if String.length s < 2 then
223223+ Error (`Invalid_event_id "too short")
224224+ else if s.[0] <> sigil then
225225+ Error (`Invalid_event_id "must start with $")
226226+ else
227227+ let rest = String.sub s 1 (String.length s - 1) in
228228+ match String.index_opt rest ':' with
229229+ | None ->
230230+ (* V4+ format: no server name *)
231231+ Ok (V4 { opaque_id = rest })
232232+ | Some colon_pos ->
233233+ let opaque_id = String.sub rest 0 colon_pos in
234234+ let server_part = String.sub rest (colon_pos + 1) (String.length rest - colon_pos - 1) in
235235+ match Server_name.of_string server_part with
236236+ | Error _ -> Error (`Invalid_event_id "invalid server name")
237237+ | Ok server_name -> Ok (V1 { opaque_id; server_name })
238238+239239+ (** Parse an event ID, raising [Invalid_argument] on failure. *)
240240+ let of_string_exn s =
241241+ match of_string s with
242242+ | Ok t -> t
243243+ | Error (`Invalid_event_id msg) ->
244244+ invalid_arg (Printf.sprintf "Invalid event ID '%s': %s" s msg)
245245+246246+ (** Convert to the canonical string representation. *)
247247+ let to_string = function
248248+ | V1 { opaque_id; server_name } ->
249249+ Printf.sprintf "%c%s:%s" sigil opaque_id (Server_name.to_string server_name)
250250+ | V4 { opaque_id } ->
251251+ Printf.sprintf "%c%s" sigil opaque_id
252252+253253+ (** JSON codec for event IDs. *)
254254+ let jsont =
255255+ Jsont.of_of_string ~kind:"event_id"
256256+ ~enc:to_string
257257+ (fun s -> Result.map_error (fun (`Invalid_event_id msg) -> msg) (of_string s))
258258+end
259259+260260+(** {1 Room Aliases}
261261+262262+ Room aliases are human-readable identifiers that point to room IDs.
263263+ They have the format [#alias:server_name]. Unlike room IDs, aliases
264264+ can be created and deleted, and multiple aliases can point to the
265265+ same room.
266266+267267+ @see <https://spec.matrix.org/v1.11/appendices/#room-aliases> Room Aliases *)
268268+269269+module Room_alias = struct
270270+ (** A Matrix room alias (e.g., [#general:matrix.org]).
271271+272272+ Room aliases provide human-readable names for rooms. They are
273273+ managed by the server in the alias's domain and can be changed
274274+ over time. *)
275275+ type t = { alias : string; server_name : Server_name.t }
276276+277277+ (** The room alias sigil character. *)
278278+ let sigil = '#'
279279+280280+ (** Parse a room alias from its string representation.
281281+ @return [Ok t] if valid, [Error `Invalid_room_alias reason] otherwise *)
282282+ let of_string s =
283283+ if String.length s < 2 then
284284+ Error (`Invalid_room_alias "too short")
285285+ else if s.[0] <> sigil then
286286+ Error (`Invalid_room_alias "must start with #")
287287+ else
288288+ match String.index_opt s ':' with
289289+ | None -> Error (`Invalid_room_alias "missing colon separator")
290290+ | Some colon_pos ->
291291+ let alias = String.sub s 1 (colon_pos - 1) in
292292+ let server_part = String.sub s (colon_pos + 1) (String.length s - colon_pos - 1) in
293293+ match Server_name.of_string server_part with
294294+ | Error _ -> Error (`Invalid_room_alias "invalid server name")
295295+ | Ok server_name -> Ok { alias; server_name }
296296+297297+ (** Parse a room alias, raising [Invalid_argument] on failure. *)
298298+ let of_string_exn s =
299299+ match of_string s with
300300+ | Ok t -> t
301301+ | Error (`Invalid_room_alias msg) ->
302302+ invalid_arg (Printf.sprintf "Invalid room alias '%s': %s" s msg)
303303+304304+ (** Convert to the canonical string representation. *)
305305+ let to_string { alias; server_name } =
306306+ Printf.sprintf "%c%s:%s" sigil alias (Server_name.to_string server_name)
307307+308308+ (** Get the alias portion (without the sigil). *)
309309+ let alias t = t.alias
310310+311311+ (** Get the server that manages this alias. *)
312312+ let server_name t = t.server_name
313313+314314+ (** JSON codec for room aliases. *)
315315+ let jsont =
316316+ Jsont.of_of_string ~kind:"room_alias"
317317+ ~enc:to_string
318318+ (fun s -> Result.map_error (fun (`Invalid_room_alias msg) -> msg) (of_string s))
319319+end
320320+321321+(** {1 Device IDs}
322322+323323+ Device IDs identify client devices for end-to-end encryption and
324324+ session management. They are opaque strings assigned by the server
325325+ or provided by the client during login.
326326+327327+ @see <https://spec.matrix.org/v1.11/client-server-api/#device-management> Device Management *)
328328+329329+module Device_id = struct
330330+ (** A Matrix device ID.
331331+332332+ Device IDs are opaque strings that identify a specific client
333333+ device. They are used for E2EE key management and session tracking. *)
334334+ type t = string
335335+336336+ (** Parse a device ID from a string. Device IDs cannot be empty. *)
337337+ let of_string s =
338338+ if String.length s = 0 then
339339+ Error (`Invalid_device_id "empty")
340340+ else
341341+ Ok s
342342+343343+ (** Parse a device ID, raising [Invalid_argument] on failure. *)
344344+ let of_string_exn s =
345345+ match of_string s with
346346+ | Ok t -> t
347347+ | Error (`Invalid_device_id msg) ->
348348+ invalid_arg (Printf.sprintf "Invalid device ID: %s" msg)
349349+350350+ (** Convert to string representation. *)
351351+ let to_string t = t
352352+353353+ (** JSON codec for device IDs. *)
354354+ let jsont = Jsont.string
355355+end
356356+357357+(** {1 Session IDs}
358358+359359+ Session IDs identify Megolm encryption sessions used for
360360+ encrypted group messaging.
361361+362362+ @see <https://spec.matrix.org/v1.11/client-server-api/#messaging-algorithm-megolm> Megolm *)
363363+364364+module Session_id = struct
365365+ (** A Megolm session ID.
366366+367367+ Session IDs identify outbound Megolm sessions used to encrypt
368368+ messages in encrypted rooms. *)
369369+ type t = string
370370+371371+ (** Parse a session ID from a string. Session IDs cannot be empty. *)
372372+ let of_string s =
373373+ if String.length s = 0 then
374374+ Error (`Invalid_session_id "empty")
375375+ else
376376+ Ok s
377377+378378+ (** Convert to string representation. *)
379379+ let to_string t = t
380380+381381+ (** JSON codec for session IDs. *)
382382+ let jsont = Jsont.string
383383+end
384384+385385+(** {1 Transaction IDs}
386386+387387+ Transaction IDs are client-generated identifiers used for
388388+ idempotent request handling. If a request with the same transaction
389389+ ID is sent multiple times, the server will only process it once.
390390+391391+ @see <https://spec.matrix.org/v1.11/client-server-api/#transaction-identifiers> Transaction Identifiers *)
392392+393393+module Transaction_id = struct
394394+ (** A client-generated transaction ID.
395395+396396+ Transaction IDs ensure idempotency for state-changing requests.
397397+ The same transaction ID should be used when retrying a request
398398+ to prevent duplicate actions. *)
399399+ type t = string
400400+401401+ (** Generate a new random transaction ID.
402402+403403+ Uses 16 random bytes encoded as hexadecimal. *)
404404+ let generate () =
405405+ let random_bytes = Bytes.create 16 in
406406+ for i = 0 to 15 do
407407+ Bytes.set random_bytes i (Char.chr (Random.int 256))
408408+ done;
409409+ let buf = Buffer.create 32 in
410410+ Bytes.iter (fun c -> Buffer.add_string buf (Printf.sprintf "%02x" (Char.code c))) random_bytes;
411411+ Buffer.contents buf
412412+413413+ (** Create a transaction ID from an existing string. *)
414414+ let of_string s = s
415415+416416+ (** Convert to string representation. *)
417417+ let to_string t = t
418418+419419+ (** JSON codec for transaction IDs. *)
420420+ let jsont = Jsont.string
421421+end
+113
lib/matrix_proto/matrix_id.mli
···11+(** Matrix identifiers with validation and JSON codecs.
22+33+ Matrix uses several types of identifiers that follow specific formats.
44+ All identifiers are case-sensitive. *)
55+66+(** {1 Server Names} *)
77+88+module Server_name : sig
99+ type t
1010+1111+ val of_string : string -> (t, [> `Invalid_server_name of string ]) result
1212+ val of_string_exn : string -> t
1313+ val to_string : t -> string
1414+ val jsont : t Jsont.t
1515+end
1616+1717+(** {1 User IDs}
1818+1919+ User IDs have the format [@localpart:server_name]. *)
2020+2121+module User_id : sig
2222+ type t
2323+2424+ val of_string : string -> (t, [> `Invalid_user_id of string ]) result
2525+ val of_string_exn : string -> t
2626+ val to_string : t -> string
2727+ val localpart : t -> string
2828+ val server_name : t -> Server_name.t
2929+ val jsont : t Jsont.t
3030+end
3131+3232+(** {1 Room IDs}
3333+3434+ Room IDs have the format [!opaque_id:server_name]. *)
3535+3636+module Room_id : sig
3737+ type t
3838+3939+ val of_string : string -> (t, [> `Invalid_room_id of string ]) result
4040+ val of_string_exn : string -> t
4141+ val to_string : t -> string
4242+ val opaque_id : t -> string
4343+ val server_name : t -> Server_name.t
4444+ val jsont : t Jsont.t
4545+end
4646+4747+(** {1 Event IDs}
4848+4949+ Event IDs can be either:
5050+ - Version 1-3: [$opaque_id:server_name]
5151+ - Version 4+: [$base64_opaque_id] (no server name) *)
5252+5353+module Event_id : sig
5454+ type t
5555+5656+ val of_string : string -> (t, [> `Invalid_event_id of string ]) result
5757+ val of_string_exn : string -> t
5858+ val to_string : t -> string
5959+ val jsont : t Jsont.t
6060+end
6161+6262+(** {1 Room Aliases}
6363+6464+ Room aliases have the format [#alias:server_name]. *)
6565+6666+module Room_alias : sig
6767+ type t
6868+6969+ val of_string : string -> (t, [> `Invalid_room_alias of string ]) result
7070+ val of_string_exn : string -> t
7171+ val to_string : t -> string
7272+ val alias : t -> string
7373+ val server_name : t -> Server_name.t
7474+ val jsont : t Jsont.t
7575+end
7676+7777+(** {1 Device IDs}
7878+7979+ Device IDs are opaque strings. *)
8080+8181+module Device_id : sig
8282+ type t
8383+8484+ val of_string : string -> (t, [> `Invalid_device_id of string ]) result
8585+ val of_string_exn : string -> t
8686+ val to_string : t -> string
8787+ val jsont : t Jsont.t
8888+end
8989+9090+(** {1 Session IDs}
9191+9292+ Megolm session IDs. *)
9393+9494+module Session_id : sig
9595+ type t
9696+9797+ val of_string : string -> (t, [> `Invalid_session_id of string ]) result
9898+ val to_string : t -> string
9999+ val jsont : t Jsont.t
100100+end
101101+102102+(** {1 Transaction IDs}
103103+104104+ Client-generated transaction IDs for idempotency. *)
105105+106106+module Transaction_id : sig
107107+ type t
108108+109109+ val generate : unit -> t
110110+ val of_string : string -> t
111111+ val to_string : t -> string
112112+ val jsont : t Jsont.t
113113+end
+15
lib/matrix_proto/matrix_proto.ml
···11+(** Matrix protocol types with JSON codecs.
22+33+ This library provides comprehensive OCaml types and bidirectional JSON
44+ codecs for the Matrix protocol. All codecs are built using the jsont
55+ library for type-safe encoding and decoding.
66+77+ {1 Modules}
88+99+ - {!Matrix_id}: Matrix identifiers (User_id, Room_id, Event_id, etc.)
1010+ - {!Matrix_event}: Event types and content structures
1111+ - {!Matrix_sync}: Sync API response types *)
1212+1313+module Id = Matrix_id
1414+module Event = Matrix_event
1515+module Sync = Matrix_sync
+312
lib/matrix_proto/matrix_sync.ml
···11+(** Matrix sync API response types with JSON codecs.
22+33+ The sync API is the core of Matrix client communication. This module
44+ provides types for the complete sync response structure. *)
55+66+open Matrix_id
77+open Matrix_event
88+99+(** {1 Timeline} *)
1010+1111+module Timeline = struct
1212+ type t = {
1313+ events : Raw_event.t list;
1414+ limited : bool option;
1515+ prev_batch : string option;
1616+ }
1717+1818+ let jsont =
1919+ Jsont.Object.(
2020+ map (fun events limited prev_batch -> { events; limited; prev_batch })
2121+ |> mem "events" (Jsont.list Raw_event.jsont) ~dec_absent:[]
2222+ ~enc:(fun t -> t.events)
2323+ |> opt_mem "limited" Jsont.bool ~enc:(fun t -> t.limited)
2424+ |> opt_mem "prev_batch" Jsont.string ~enc:(fun t -> t.prev_batch)
2525+ |> finish)
2626+end
2727+2828+(** {1 Ephemeral Events} *)
2929+3030+module Ephemeral = struct
3131+ type t = {
3232+ events : Jsont.json list;
3333+ }
3434+3535+ let jsont =
3636+ Jsont.Object.(
3737+ map (fun events -> { events })
3838+ |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.events)
3939+ |> finish)
4040+end
4141+4242+(** {1 Account Data} *)
4343+4444+module Account_data = struct
4545+ type t = {
4646+ events : Jsont.json list;
4747+ }
4848+4949+ let jsont =
5050+ Jsont.Object.(
5151+ map (fun events -> { events })
5252+ |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.events)
5353+ |> finish)
5454+end
5555+5656+(** {1 Room State} *)
5757+5858+module Room_state = struct
5959+ type t = {
6060+ events : Raw_event.t list;
6161+ }
6262+6363+ let jsont =
6464+ Jsont.Object.(
6565+ map (fun events -> { events })
6666+ |> mem "events" (Jsont.list Raw_event.jsont) ~dec_absent:[]
6767+ ~enc:(fun t -> t.events)
6868+ |> finish)
6969+end
7070+7171+(** {1 Unread Notification Counts} *)
7272+7373+module Unread_notification_counts = struct
7474+ type t = {
7575+ highlight_count : int option;
7676+ notification_count : int option;
7777+ }
7878+7979+ let jsont =
8080+ Jsont.Object.(
8181+ map (fun highlight_count notification_count ->
8282+ { highlight_count; notification_count })
8383+ |> opt_mem "highlight_count" Jsont.int ~enc:(fun t -> t.highlight_count)
8484+ |> opt_mem "notification_count" Jsont.int ~enc:(fun t -> t.notification_count)
8585+ |> finish)
8686+end
8787+8888+(** {1 Room Summary} *)
8989+9090+module Room_summary = struct
9191+ type t = {
9292+ heroes : User_id.t list option;
9393+ joined_member_count : int option;
9494+ invited_member_count : int option;
9595+ }
9696+9797+ let jsont =
9898+ Jsont.Object.(
9999+ map (fun heroes joined_member_count invited_member_count ->
100100+ { heroes; joined_member_count; invited_member_count })
101101+ |> opt_mem "m.heroes" (Jsont.list User_id.jsont) ~enc:(fun t -> t.heroes)
102102+ |> opt_mem "m.joined_member_count" Jsont.int ~enc:(fun t -> t.joined_member_count)
103103+ |> opt_mem "m.invited_member_count" Jsont.int ~enc:(fun t -> t.invited_member_count)
104104+ |> finish)
105105+end
106106+107107+(** {1 Joined Room} *)
108108+109109+module Joined_room = struct
110110+ type t = {
111111+ summary : Room_summary.t option;
112112+ state : Room_state.t option;
113113+ timeline : Timeline.t option;
114114+ ephemeral : Ephemeral.t option;
115115+ account_data : Account_data.t option;
116116+ unread_notifications : Unread_notification_counts.t option;
117117+ }
118118+119119+ let jsont =
120120+ Jsont.Object.(
121121+ map (fun summary state timeline ephemeral account_data unread_notifications ->
122122+ { summary; state; timeline; ephemeral; account_data; unread_notifications })
123123+ |> opt_mem "summary" Room_summary.jsont ~enc:(fun t -> t.summary)
124124+ |> opt_mem "state" Room_state.jsont ~enc:(fun t -> t.state)
125125+ |> opt_mem "timeline" Timeline.jsont ~enc:(fun t -> t.timeline)
126126+ |> opt_mem "ephemeral" Ephemeral.jsont ~enc:(fun t -> t.ephemeral)
127127+ |> opt_mem "account_data" Account_data.jsont ~enc:(fun t -> t.account_data)
128128+ |> opt_mem "unread_notifications" Unread_notification_counts.jsont ~enc:(fun t -> t.unread_notifications)
129129+ |> finish)
130130+end
131131+132132+(** {1 Invited Room} *)
133133+134134+module Invited_room = struct
135135+ type t = {
136136+ invite_state : invite_state option;
137137+ }
138138+ and invite_state = {
139139+ events : Jsont.json list;
140140+ }
141141+142142+ let invite_state_jsont =
143143+ Jsont.Object.(
144144+ map (fun events -> { events })
145145+ |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun s -> s.events)
146146+ |> finish)
147147+148148+ let jsont =
149149+ Jsont.Object.(
150150+ map (fun invite_state -> { invite_state })
151151+ |> opt_mem "invite_state" invite_state_jsont ~enc:(fun t -> t.invite_state)
152152+ |> finish)
153153+end
154154+155155+(** {1 Left Room} *)
156156+157157+module Left_room = struct
158158+ type t = {
159159+ state : Room_state.t option;
160160+ timeline : Timeline.t option;
161161+ account_data : Account_data.t option;
162162+ }
163163+164164+ let jsont =
165165+ Jsont.Object.(
166166+ map (fun state timeline account_data -> { state; timeline; account_data })
167167+ |> opt_mem "state" Room_state.jsont ~enc:(fun t -> t.state)
168168+ |> opt_mem "timeline" Timeline.jsont ~enc:(fun t -> t.timeline)
169169+ |> opt_mem "account_data" Account_data.jsont ~enc:(fun t -> t.account_data)
170170+ |> finish)
171171+end
172172+173173+(** {1 Knocked Room} *)
174174+175175+module Knocked_room = struct
176176+ type t = {
177177+ knock_state : knock_state option;
178178+ }
179179+ and knock_state = {
180180+ events : Jsont.json list;
181181+ }
182182+183183+ let knock_state_jsont =
184184+ Jsont.Object.(
185185+ map (fun events -> { events })
186186+ |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun s -> s.events)
187187+ |> finish)
188188+189189+ let jsont =
190190+ Jsont.Object.(
191191+ map (fun knock_state -> { knock_state })
192192+ |> opt_mem "knock_state" knock_state_jsont ~enc:(fun t -> t.knock_state)
193193+ |> finish)
194194+end
195195+196196+(** {1 Rooms} *)
197197+198198+module Rooms = struct
199199+ type t = {
200200+ join : (string * Joined_room.t) list;
201201+ invite : (string * Invited_room.t) list;
202202+ leave : (string * Left_room.t) list;
203203+ knock : (string * Knocked_room.t) list;
204204+ }
205205+206206+ module StringMap = Map.Make(String)
207207+208208+ let room_map_jsont jsont =
209209+ Jsont.Object.as_string_map jsont
210210+ |> Jsont.map
211211+ ~dec:(fun m -> StringMap.bindings m)
212212+ ~enc:(fun l -> List.to_seq l |> StringMap.of_seq)
213213+214214+ let jsont =
215215+ Jsont.Object.(
216216+ map (fun join invite leave knock -> { join; invite; leave; knock })
217217+ |> mem "join" (room_map_jsont Joined_room.jsont) ~dec_absent:[]
218218+ ~enc:(fun t -> t.join)
219219+ |> mem "invite" (room_map_jsont Invited_room.jsont) ~dec_absent:[]
220220+ ~enc:(fun t -> t.invite)
221221+ |> mem "leave" (room_map_jsont Left_room.jsont) ~dec_absent:[]
222222+ ~enc:(fun t -> t.leave)
223223+ |> mem "knock" (room_map_jsont Knocked_room.jsont) ~dec_absent:[]
224224+ ~enc:(fun t -> t.knock)
225225+ |> finish)
226226+end
227227+228228+(** {1 Device Lists} *)
229229+230230+module Device_lists = struct
231231+ type t = {
232232+ changed : User_id.t list;
233233+ left : User_id.t list;
234234+ }
235235+236236+ let jsont =
237237+ Jsont.Object.(
238238+ map (fun changed left -> { changed; left })
239239+ |> mem "changed" (Jsont.list User_id.jsont) ~dec_absent:[] ~enc:(fun t -> t.changed)
240240+ |> mem "left" (Jsont.list User_id.jsont) ~dec_absent:[] ~enc:(fun t -> t.left)
241241+ |> finish)
242242+end
243243+244244+(** {1 To-Device Events} *)
245245+246246+module To_device = struct
247247+ type t = {
248248+ events : Jsont.json list;
249249+ }
250250+251251+ let jsont =
252252+ Jsont.Object.(
253253+ map (fun events -> { events })
254254+ |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.events)
255255+ |> finish)
256256+end
257257+258258+(** {1 Presence} *)
259259+260260+module Presence = struct
261261+ type t = {
262262+ events : Jsont.json list;
263263+ }
264264+265265+ let jsont =
266266+ Jsont.Object.(
267267+ map (fun events -> { events })
268268+ |> mem "events" (Jsont.list Jsont.json) ~dec_absent:[] ~enc:(fun t -> t.events)
269269+ |> finish)
270270+end
271271+272272+(** {1 Sync Response} *)
273273+274274+module Response = struct
275275+ type t = {
276276+ next_batch : string;
277277+ rooms : Rooms.t option;
278278+ presence : Presence.t option;
279279+ account_data : Account_data.t option;
280280+ to_device : To_device.t option;
281281+ device_lists : Device_lists.t option;
282282+ device_one_time_keys_count : (string * int) list;
283283+ device_unused_fallback_key_types : string list option;
284284+ }
285285+286286+ module StringMap = Map.Make(String)
287287+288288+ let int_map_jsont =
289289+ Jsont.Object.as_string_map Jsont.int
290290+ |> Jsont.map
291291+ ~dec:(fun m -> StringMap.bindings m)
292292+ ~enc:(fun l -> List.to_seq l |> StringMap.of_seq)
293293+294294+ let jsont =
295295+ Jsont.Object.(
296296+ map (fun next_batch rooms presence account_data to_device
297297+ device_lists device_one_time_keys_count
298298+ device_unused_fallback_key_types ->
299299+ { next_batch; rooms; presence; account_data; to_device;
300300+ device_lists; device_one_time_keys_count;
301301+ device_unused_fallback_key_types })
302302+ |> mem "next_batch" Jsont.string ~enc:(fun t -> t.next_batch)
303303+ |> opt_mem "rooms" Rooms.jsont ~enc:(fun t -> t.rooms)
304304+ |> opt_mem "presence" Presence.jsont ~enc:(fun t -> t.presence)
305305+ |> opt_mem "account_data" Account_data.jsont ~enc:(fun t -> t.account_data)
306306+ |> opt_mem "to_device" To_device.jsont ~enc:(fun t -> t.to_device)
307307+ |> opt_mem "device_lists" Device_lists.jsont ~enc:(fun t -> t.device_lists)
308308+ |> mem "device_one_time_keys_count" int_map_jsont ~dec_absent:[]
309309+ ~enc:(fun t -> t.device_one_time_keys_count)
310310+ |> opt_mem "device_unused_fallback_key_types" (Jsont.list Jsont.string) ~enc:(fun t -> t.device_unused_fallback_key_types)
311311+ |> finish)
312312+end
+153
lib/matrix_proto/matrix_sync.mli
···11+(** Matrix sync API response types with JSON codecs.
22+33+ The sync API is the core of Matrix client communication. This module
44+ provides types for the complete sync response structure. *)
55+66+open Matrix_id
77+open Matrix_event
88+99+(** {1 Timeline} *)
1010+1111+module Timeline : sig
1212+ type t = {
1313+ events : Raw_event.t list;
1414+ limited : bool option;
1515+ prev_batch : string option;
1616+ }
1717+ val jsont : t Jsont.t
1818+end
1919+2020+(** {1 Ephemeral Events} *)
2121+2222+module Ephemeral : sig
2323+ type t = { events : Jsont.json list }
2424+ val jsont : t Jsont.t
2525+end
2626+2727+(** {1 Account Data} *)
2828+2929+module Account_data : sig
3030+ type t = { events : Jsont.json list }
3131+ val jsont : t Jsont.t
3232+end
3333+3434+(** {1 Room State} *)
3535+3636+module Room_state : sig
3737+ type t = { events : Raw_event.t list }
3838+ val jsont : t Jsont.t
3939+end
4040+4141+(** {1 Unread Notification Counts} *)
4242+4343+module Unread_notification_counts : sig
4444+ type t = {
4545+ highlight_count : int option;
4646+ notification_count : int option;
4747+ }
4848+ val jsont : t Jsont.t
4949+end
5050+5151+(** {1 Room Summary} *)
5252+5353+module Room_summary : sig
5454+ type t = {
5555+ heroes : User_id.t list option;
5656+ joined_member_count : int option;
5757+ invited_member_count : int option;
5858+ }
5959+ val jsont : t Jsont.t
6060+end
6161+6262+(** {1 Joined Room} *)
6363+6464+module Joined_room : sig
6565+ type t = {
6666+ summary : Room_summary.t option;
6767+ state : Room_state.t option;
6868+ timeline : Timeline.t option;
6969+ ephemeral : Ephemeral.t option;
7070+ account_data : Account_data.t option;
7171+ unread_notifications : Unread_notification_counts.t option;
7272+ }
7373+ val jsont : t Jsont.t
7474+end
7575+7676+(** {1 Invited Room} *)
7777+7878+module Invited_room : sig
7979+ type t = { invite_state : invite_state option }
8080+ and invite_state = { events : Jsont.json list }
8181+ val jsont : t Jsont.t
8282+end
8383+8484+(** {1 Left Room} *)
8585+8686+module Left_room : sig
8787+ type t = {
8888+ state : Room_state.t option;
8989+ timeline : Timeline.t option;
9090+ account_data : Account_data.t option;
9191+ }
9292+ val jsont : t Jsont.t
9393+end
9494+9595+(** {1 Knocked Room} *)
9696+9797+module Knocked_room : sig
9898+ type t = { knock_state : knock_state option }
9999+ and knock_state = { events : Jsont.json list }
100100+ val jsont : t Jsont.t
101101+end
102102+103103+(** {1 Rooms} *)
104104+105105+module Rooms : sig
106106+ type t = {
107107+ join : (string * Joined_room.t) list;
108108+ invite : (string * Invited_room.t) list;
109109+ leave : (string * Left_room.t) list;
110110+ knock : (string * Knocked_room.t) list;
111111+ }
112112+ val jsont : t Jsont.t
113113+end
114114+115115+(** {1 Device Lists} *)
116116+117117+module Device_lists : sig
118118+ type t = {
119119+ changed : User_id.t list;
120120+ left : User_id.t list;
121121+ }
122122+ val jsont : t Jsont.t
123123+end
124124+125125+(** {1 To-Device Events} *)
126126+127127+module To_device : sig
128128+ type t = { events : Jsont.json list }
129129+ val jsont : t Jsont.t
130130+end
131131+132132+(** {1 Presence} *)
133133+134134+module Presence : sig
135135+ type t = { events : Jsont.json list }
136136+ val jsont : t Jsont.t
137137+end
138138+139139+(** {1 Sync Response} *)
140140+141141+module Response : sig
142142+ type t = {
143143+ next_batch : string;
144144+ rooms : Rooms.t option;
145145+ presence : Presence.t option;
146146+ account_data : Account_data.t option;
147147+ to_device : To_device.t option;
148148+ device_lists : Device_lists.t option;
149149+ device_one_time_keys_count : (string * int) list;
150150+ device_unused_fallback_key_types : string list option;
151151+ }
152152+ val jsont : t Jsont.t
153153+end