let src = Logs.Src.create "matrix.client" ~doc:"Matrix client HTTP" module Log = (val Logs.src_log src : Logs.LOG) type config = { homeserver : Uri.t; user_agent : string option; } type session = { user_id : Matrix_proto.Id.User_id.t; access_token : string; device_id : Matrix_proto.Id.Device_id.t; refresh_token : string option; } type t = { http : Requests.t; config : config; session : session option; } let create ~sw ~config env = let http = Requests.create ~sw env in { http; config; session = None } let with_session t session = { t with session = Some session } let session t = t.session let homeserver t = t.config.homeserver let is_logged_in t = Option.is_some t.session let access_token t = Option.map (fun s -> s.access_token) t.session let user_id t = Option.map (fun s -> s.user_id) t.session let device_id t = Option.map (fun s -> s.device_id) t.session (* Matrix API base path *) let api_base = "/_matrix/client/v3" let make_url t path query = let base = t.config.homeserver in let path = api_base ^ path in let uri = Uri.with_path base path in match query with | None | Some [] -> uri | Some q -> Uri.with_query' uri q let auth_headers t = match t.session with | Some s -> Requests.Headers.(empty |> bearer s.access_token) | None -> Requests.Headers.empty let add_user_agent t headers = match t.config.user_agent with | Some ua -> Requests.Headers.user_agent ua headers | None -> headers let json_content_type headers = Requests.Headers.content_type Requests.Mime.json headers let handle_response response = let status = Requests.Response.status_code response in let body = Requests.Response.text response in Log.debug (fun m -> m "Response: status=%d" status); Log.debug (fun m -> m "Response body: %s" body); if status >= 200 && status < 300 then Ok body else begin Log.warn (fun m -> m "HTTP error: status=%d body=%s" status body); (* Try to parse as Matrix error *) match Jsont_bytesrw.decode_string Error.matrix_error_jsont body with | Ok matrix_err -> Error (Error.Matrix_error matrix_err) | Error _ -> Error (Error.Http_error { status; body }) end let get t ~path ?query () = try let url = make_url t path query |> Uri.to_string in Log.debug (fun m -> m "GET %s" url); let headers = auth_headers t |> add_user_agent t in let response = Requests.get t.http ~headers url in handle_response response with | exn -> Log.err (fun m -> m "GET %s failed: %s" path (Printexc.to_string exn)); Error (Error.Network_error (Printexc.to_string exn)) let post t ~path ?query ~body () = try let url = make_url t path query |> Uri.to_string in Log.debug (fun m -> m "POST %s" url); Log.debug (fun m -> m "Request body: %s" body); let headers = auth_headers t |> add_user_agent t |> json_content_type in let body = Requests.Body.of_string Requests.Mime.json body in let response = Requests.post t.http ~headers ~body url in handle_response response with | exn -> Log.err (fun m -> m "POST %s failed: %s" path (Printexc.to_string exn)); Error (Error.Network_error (Printexc.to_string exn)) let put t ~path ?query ~body () = try let url = make_url t path query |> Uri.to_string in Log.debug (fun m -> m "PUT %s" url); Log.debug (fun m -> m "Request body: %s" body); let headers = auth_headers t |> add_user_agent t |> json_content_type in let body = Requests.Body.of_string Requests.Mime.json body in let response = Requests.put t.http ~headers ~body url in handle_response response with | exn -> Log.err (fun m -> m "PUT %s failed: %s" path (Printexc.to_string exn)); Error (Error.Network_error (Printexc.to_string exn)) let delete t ~path ?query ?body () = try let url = make_url t path query |> Uri.to_string in Log.debug (fun m -> m "DELETE %s" url); let headers = auth_headers t |> add_user_agent t in let headers, body = match body with | Some b -> Log.debug (fun m -> m "Request body: %s" b); (json_content_type headers, Some (Requests.Body.of_string Requests.Mime.json b)) | None -> (headers, None) in let response = Requests.request t.http ~headers ?body ~method_:`DELETE url in handle_response response with | exn -> Log.err (fun m -> m "DELETE %s failed: %s" path (Printexc.to_string exn)); Error (Error.Network_error (Printexc.to_string exn)) let post_unauthenticated t ~path ?query ~body () = try let url = make_url t path query |> Uri.to_string in Log.debug (fun m -> m "POST (unauth) %s" url); Log.debug (fun m -> m "Request body: %s" body); let headers = Requests.Headers.empty |> add_user_agent t |> json_content_type in let body = Requests.Body.of_string Requests.Mime.json body in let response = Requests.post t.http ~headers ~body url in handle_response response with | exn -> Log.err (fun m -> m "POST (unauth) %s failed: %s" path (Printexc.to_string exn)); Error (Error.Network_error (Printexc.to_string exn)) let decode_response jsont body = match Jsont_bytesrw.decode_string jsont body with | Ok v -> Ok v | Error e -> Log.err (fun m -> m "JSON decode error: %s" e); Error (Error.Json_error e) let encode_body jsont value = match Jsont_bytesrw.encode_string jsont value with | Ok s -> Ok s | Error e -> Log.err (fun m -> m "JSON encode error: %s" e); Error (Error.Json_error e)