OCaml HTML5 parser/serialiser based on Python's JustHTML

Squashed 'ocaml-zulip/' changes from c074b19..a1aa9ef

a1aa9ef Add fallback channel lookup by searching channel list
df014fd Suppress TLS tracing debug output, improve Zulip request logging
6eaac4f Fix channel name encoding in Zulip get_id
977f71b Run message handler concurrently with broadcast loop in poe
373eb2b bot improvements
c109b1c Remove public_name/package from test and example executables

git-subtree-dir: ocaml-zulip
git-subtree-split: a1aa9ef4bcfd0a0c4d2273f353537666bd3bafd4

+60 -20
-6
examples/dune
··· 1 1 (executable 2 - (public_name echo_bot) 3 2 (name echo_bot) 4 - (package zulip) 5 3 (libraries 6 4 zulip 7 5 zulip.bot ··· 12 10 mirage-crypto-rng.unix)) 13 11 14 12 (executable 15 - (public_name atom_feed_bot) 16 13 (name atom_feed_bot) 17 - (package zulip) 18 14 (libraries zulip zulip.bot eio_main cmdliner logs logs.fmt)) 19 15 20 16 (executable 21 - (public_name regression_test) 22 17 (name regression_test) 23 - (package zulip) 24 18 (libraries zulip zulip.bot eio_main cmdliner logs logs.fmt unix))
+28 -8
lib/zulip/channels.ml
··· 43 43 in 44 44 Error.decode_or_raise streams_codec json "parsing channels list" 45 45 46 + (* Search for a channel by name in the list of all channels *) 47 + let find_channel_by_name channels ~name = 48 + match List.find_opt (fun ch -> Channel.name ch = name) channels with 49 + | Some ch -> Channel.stream_id ch 50 + | None -> None 51 + 46 52 let get_id client ~name = 47 - let encoded_name = Uri.pct_encode name in 48 53 let response_codec = 49 54 Jsont.Object.( 50 55 map ~kind:"StreamIdResponse" Fun.id 51 56 |> mem "stream_id" Jsont.int ~enc:Fun.id 52 57 |> finish) 53 58 in 54 - let json = 55 - Client.request client ~method_:`GET 56 - ~path:("/api/v1/get_stream_id?stream=" ^ encoded_name) 57 - () 58 - in 59 - Error.decode_or_raise response_codec json 60 - (Printf.sprintf "getting stream id for %s" name) 59 + try 60 + let json = 61 + Client.request client ~method_:`GET 62 + ~path:"/api/v1/get_stream_id" 63 + ~params:[("stream", name)] 64 + () 65 + in 66 + Error.decode_or_raise response_codec json 67 + (Printf.sprintf "getting stream id for %s" name) 68 + with Eio.Io (Error.E { code = Bad_request; _ }, _) -> 69 + (* Fallback: search through channel list for exact name match *) 70 + let channels = list client in 71 + match find_channel_by_name channels ~name with 72 + | Some id -> id 73 + | None -> 74 + (* Re-raise with helpful context about available channels *) 75 + let available = List.map Channel.name channels |> String.concat ", " in 76 + Error.raise_with_context 77 + (Error.make ~code:Bad_request 78 + ~message:(Printf.sprintf "Channel '%s' not found. Available: %s" name available) 79 + ()) 80 + "getting stream id for %s" name 61 81 62 82 let get_by_id client ~stream_id = 63 83 let response_codec =
+5 -4
lib/zulip/client.ml
··· 37 37 | `PATCH -> "PATCH" 38 38 39 39 let request t ~method_ ~path ?params ?body ?content_type () = 40 - let url = Auth.server_url t.auth ^ path in 41 - Log.debug (fun m -> m "Request: %s %s" (method_to_string method_) path); 40 + let base_url = Auth.server_url t.auth ^ path in 42 41 43 42 (* Convert params to URL query string if provided *) 44 43 let url = 45 44 params 46 45 |> Option.map (fun p -> 47 - Uri.of_string url 46 + Uri.of_string base_url 48 47 |> Fun.flip 49 48 (List.fold_left (fun u (k, v) -> Uri.add_query_param' u (k, v))) 50 49 p 51 50 |> Uri.to_string) 52 - |> Option.value ~default:url 51 + |> Option.value ~default:base_url 53 52 in 53 + 54 + Log.debug (fun m -> m "Request: %s %s" (method_to_string method_) url); 54 55 55 56 (* Prepare request body if provided *) 56 57 let body_opt =
+13
lib/zulip_bot/bot.mli
··· 142 142 (** [fetch_identity client] retrieves the bot's identity from the Zulip server. 143 143 144 144 @raise Eio.Io on API errors *) 145 + 146 + val process_event : 147 + client:Zulip.Client.t -> 148 + storage:Storage.t -> 149 + identity:identity -> 150 + handler:handler -> 151 + Zulip.Event.t -> 152 + unit 153 + (** [process_event ~client ~storage ~identity ~handler event] processes a single 154 + Zulip event. 155 + 156 + This is useful for custom event loops that need finer control over event 157 + processing than [run] provides. *)
+14 -2
lib/zulip_bot/storage.ml
··· 90 90 Log.warn (fun m -> m "Failed to parse storage response: %s" msg); 91 91 None 92 92 with Eio.Exn.Io (e, _) -> 93 - Log.warn (fun m -> m "Error fetching key %s: %a" key Eio.Exn.pp_err e); 94 - None) 93 + let is_key_not_found = match e with 94 + | Zulip.Error.E err -> 95 + Zulip.Error.code err = Zulip.Error.Bad_request && 96 + String.equal (Zulip.Error.message err) "Key does not exist." 97 + | _ -> false 98 + in 99 + if is_key_not_found then begin 100 + (* Key not found is a normal case, not an error *) 101 + Log.debug (fun m -> m "Key not found in storage: %s" key); 102 + None 103 + end else begin 104 + Log.warn (fun m -> m "Error fetching key %s: %a" key Eio.Exn.pp_err e); 105 + None 106 + end) 95 107 96 108 let set t key value = 97 109 Log.debug (fun m -> m "Storing key: %s" key);