open Lwt.Syntax open Lwt.Infix exception ClosedStream type features = { mechanisms : Sasl.auth_mechanism list; starttls : [`Required | `Optional | `None]; unknown : Xml.element list; } (** [parse_features el] is a [features] record with all the features of the [] stanza contained in [el]. *) let parse_features (el : Xml.element) : features = let open Xml in let open Either in let parse_mechanism_stanza = function | Left {local_name = "mechanism"; children = [Right mechanism]; _} -> Some (Sasl.parse_auth_mechanism mechanism) | _ -> None in let parse_feature (acc : features) (feature : Xml.element) : features = let parse_mechanisms ch = List.filter_map parse_mechanism_stanza ch and parse_starttls = function | [Left {local_name="required"; _}] -> `Required | [] -> `Optional | _ -> raise (InvalidStanza (element_to_string el)) in match feature.local_name with | "mechanisms" -> {acc with mechanisms=parse_mechanisms feature.children} | "starttls" -> {acc with starttls=parse_starttls feature.children} | _ -> {acc with unknown = feature :: acc.unknown} in List.fold_left parse_feature {mechanisms=[]; starttls=`None; unknown=[]} (List.filter_map find_left el.children) (** [negotiate domain stream] is a promise containing the features supported by the XMPP server communicating with [stream]. This function should be called every time a stream needs to be reopened and stream negotiation takes place. Basically, it conforms to {{: https://datatracker.ietf.org/doc/html/rfc6120#section-4.3 }}. *) let negotiate (domain : string) (portal : Portal.t) : features Lwt.t = let* _id = Portal.header domain portal in let+ features = Wire.get portal.stream >|= parse_features in features (** [initiate domain] initiates a stream with the XMPP server [domain]. Once [None] is pushed into the stream, the receiving stream is drained and the socket is closed. *) let initiate (domain : string) : (Portal.t * features) Lwt.t = let open Portal in let* p = connect domain in let push = function | Some n -> p.push (Some n) | None -> p.push (Some close); (* Empty the stream completely, then close the socket. *) Lwt.async (fun () -> let+ () = Markup_lwt.drain p.stream in p.push None) in let portal = {p with push} in let+ features = negotiate domain portal in (portal, features)