diff options
author | Clombrong <clombrong@egregore.fun> | 2025-04-24 16:22:03 +0200 |
---|---|---|
committer | Clombrong <cromblong@egregore.fun> | 2025-04-24 16:22:03 +0200 |
commit | 1db20a50974dd33874d28aa78f69a25aa4294a94 (patch) | |
tree | d2b24d2801fdd21001cdd7bc2da6ecad65724d2e /portal | |
parent | 9c36f4089d4636f37853d0c13d6a6ca2f4fd6b05 (diff) |
fix: rearrange all portals under common portal/ folder
Diffstat (limited to 'portal')
-rw-r--r-- | portal/lib/dune | 6 | ||||
-rw-r--r-- | portal/lib/portal_ws.ml | 62 | ||||
-rw-r--r-- | portal/test/js/dune | 14 | ||||
-rw-r--r-- | portal/test/js/package-lock.json | 21 | ||||
-rw-r--r-- | portal/test/js/package.json | 5 | ||||
-rw-r--r-- | portal/test/js/polyfill.js | 1 | ||||
-rw-r--r-- | portal/test/js/websockets_hello.ml | 35 |
7 files changed, 144 insertions, 0 deletions
diff --git a/portal/lib/dune b/portal/lib/dune new file mode 100644 index 0000000..8ce0232 --- /dev/null +++ b/portal/lib/dune @@ -0,0 +1,6 @@ +(library + (name portal_ws) + (modules portal_ws) + (public_name flesh_websockets) + (libraries lwt js_of_ocaml js_of_ocaml-lwt xmlm) + (preprocess (pps js_of_ocaml-ppx))) diff --git a/portal/lib/portal_ws.ml b/portal/lib/portal_ws.ml new file mode 100644 index 0000000..1570a9e --- /dev/null +++ b/portal/lib/portal_ws.ml @@ -0,0 +1,62 @@ +open Lwt.Syntax +open Js_of_ocaml +let jss = Js.string +let sjs = Js.to_string + +(* sic. XEP-0156: "host-meta files MUST be fetched only over HTTPS". I don't make the rules. *) +let well_known_of (domain : string) = "https://" ^ domain ^ "/.well-known/host-meta" + +let ws_endpoint (domain : string) = + (** [ws_endpoint domain] is a promise containing the XMPP websocket endpoint associated with [domain], by using the + domain's Web-host Metadata. + + This function uses XMLHttpRequest, so while it should work fine in the browser, in environments that don't provide + this constructor (Node.js), there should be some sort of polyfill. + + Lastly, if [domain] doesn't provide a well-formed Web-host Metadata file, the function throws an exception. *) + + let+ host_meta = Js_of_ocaml_lwt.XmlHttpRequest.perform_raw_url (well_known_of domain) + in let i = Xmlm.make_input (`String (0, host_meta.content)) + (* This ugly function extracts the href element from a Link tag's attributes if it's a websocket. *) + and link_websocket attrs = + match attrs with + | ((_, "href"), href) :: ((_, "rel"), "urn:xmpp:alt-connections:websocket") :: _ -> Some href + | ((_, "rel"), "urn:xmpp:alt-connections:websocket") :: ((_, "href"), href) :: _ -> Some href + | _ -> None + in let parse_tree = (* Parse a single XRD tree. *) + Xmlm.input_tree + ~el:(fun tag children + -> match tag with + | ((_, "Link"), attributes) -> link_websocket attributes + | ((_, "XRD"), _) -> List.find_map (fun x -> x) children + | _ -> None) + ~data:(fun x -> Some x) + in ignore (Xmlm.input i); (* DTD *) + match parse_tree i with + | Some uri -> uri + | None -> failwith (domain ^ "doesn't advertise a WebSocket endpoint via Web-host Metadata.") + +let ws_stream (url : string) = + (** [ws_stream url] returns a stream (and its push function) that talk with the websocket located at [url]. + + Pushing [None] closes the websocket. + + If the websocket is closed server-side, it's still up to the caller to close the stream. *) + let open Lwt_stream in + let handle ws incoming () = + let+ _ = iter (fun msg -> ws##send (jss msg)) incoming + in (ws##close) + in let stream, message = create () (* websocket -> user *) + and incoming, push = create () (* user -> websocket *) + in let (ws : WebSockets.webSocket Js.t) = new%js WebSockets.webSocket (jss url) + in ws##.onmessage := + Dom.handler (fun x -> Some (sjs x##.data) |> message; Js._false); + ws##.onopen := + Dom.handler (fun _ -> Lwt.async @@ handle ws incoming; Js._false); + ws##.onclose := + Dom.handler (fun _ -> message None; Js._true); + stream, push + +(* let connect domain = *) +(* let+ url = server_ws domain *) +(* in ws_stream url *) diff --git a/portal/test/js/dune b/portal/test/js/dune new file mode 100644 index 0000000..b34c980 --- /dev/null +++ b/portal/test/js/dune @@ -0,0 +1,14 @@ +(test + (name websockets_hello) + (libraries portal_ws lwt js_of_ocaml) + (modes js) + (preprocess (pps js_of_ocaml-ppx)) + (deps node_modules) + (js_of_ocaml + (javascript_files polyfill.js))) + +(rule + (alias npm) + (target node_modules) + (deps package.json package-lock.json) + (action (system "npm ci"))) diff --git a/portal/test/js/package-lock.json b/portal/test/js/package-lock.json new file mode 100644 index 0000000..8b5d148 --- /dev/null +++ b/portal/test/js/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "js", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "xmlhttprequest": "^1.8.0" + } + }, + "node_modules/xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + } + } +} diff --git a/portal/test/js/package.json b/portal/test/js/package.json new file mode 100644 index 0000000..3b36bf4 --- /dev/null +++ b/portal/test/js/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "xmlhttprequest": "^1.8.0" + } +} diff --git a/portal/test/js/polyfill.js b/portal/test/js/polyfill.js new file mode 100644 index 0000000..e394ec8 --- /dev/null +++ b/portal/test/js/polyfill.js @@ -0,0 +1 @@ +global.XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; diff --git a/portal/test/js/websockets_hello.ml b/portal/test/js/websockets_hello.ml new file mode 100644 index 0000000..fa47965 --- /dev/null +++ b/portal/test/js/websockets_hello.ml @@ -0,0 +1,35 @@ +open Lwt.Syntax +open Js_of_ocaml + +(* https://stackoverflow.com/questions/34929382/what-are-the-differences-between-lwt-async-and-lwt-main-run-on-ocaml-node-js *) +let rec run t = + let next_tick (_callback : unit -> unit) = + Js.Unsafe.(fun_call + (js_expr "process.nextTick") + [| inject (Js.wrap_callback _callback) |]) + in Lwt.wakeup_paused (); + match Lwt.poll t with + | Some x -> x + | None -> + if Lwt.paused_count () > 0 + then next_tick (fun () -> run t) + else () + +let () = + run @@ + let* server = Portal_ws.ws_endpoint "telepath.im" in + let stream, push = + (* Echo is a websocket that... echoes you stuff. *) + Portal_ws.ws_stream "wss://echo.websocket.org" in + push (Some "great text"); + push (Some "other text"); + push (Some "yet another text"); + push (Some "BYE"); + let+ _ = Lwt_stream.iter + (fun greetings -> + match greetings with + (* When the websocket sends "BYE", we close. *) + | "BYE" -> print_endline "CLOSING BYE"; push None + | hello -> print_endline ("> " ^ hello)) + stream + in print_endline server |