aboutsummaryrefslogtreecommitdiff
path: root/portal
diff options
context:
space:
mode:
authorClombrong <clombrong@egregore.fun>2025-04-24 16:22:03 +0200
committerClombrong <cromblong@egregore.fun>2025-04-24 16:22:03 +0200
commit1db20a50974dd33874d28aa78f69a25aa4294a94 (patch)
treed2b24d2801fdd21001cdd7bc2da6ecad65724d2e /portal
parent9c36f4089d4636f37853d0c13d6a6ca2f4fd6b05 (diff)
fix: rearrange all portals under common portal/ folder
Diffstat (limited to 'portal')
-rw-r--r--portal/lib/dune6
-rw-r--r--portal/lib/portal_ws.ml62
-rw-r--r--portal/test/js/dune14
-rw-r--r--portal/test/js/package-lock.json21
-rw-r--r--portal/test/js/package.json5
-rw-r--r--portal/test/js/polyfill.js1
-rw-r--r--portal/test/js/websockets_hello.ml35
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