My agentic slop goes here. Not intended for anyone else!

more

+5
stack/requests/lib/requests.ml
···
module Retry = Retry
module Cache = Cache
(* Main API - Session functionality with concurrent fiber spawning *)
type ('clock, 'net) t = {
···
module Retry = Retry
module Cache = Cache
+
(* Note: RNG initialization should be done by the application using
+
Mirage_crypto_rng_unix.initialize before calling Eio_main.run.
+
We don't call use_default() here as it spawns background threads
+
that are incompatible with Eio's structured concurrency. *)
+
(* Main API - Session functionality with concurrent fiber spawning *)
type ('clock, 'net) t = {
+2 -12
stack/requests/lib/response.ml
···
Eio.Switch.on_release sw (fun () ->
if not response.closed then begin
Log.debug (fun m -> m "Auto-closing response for %s via switch" url);
-
try
-
(* Read and discard remaining data *)
-
let rec drain () =
-
let buf = Cstruct.create 8192 in
-
match Eio.Flow.single_read body buf with
-
| 0 -> () (* EOF *)
-
| _ -> drain ()
-
in
-
drain ();
-
response.closed <- true
-
with _ ->
-
response.closed <- true
end
);
···
Eio.Switch.on_release sw (fun () ->
if not response.closed then begin
Log.debug (fun m -> m "Auto-closing response for %s via switch" url);
+
response.closed <- true;
+
(* TODO Body cleanup is handled by the underlying HTTP library but test this *)
end
);
+1 -1
stack/river/bin/dune
···
(executable
(public_name river-cli)
(name river_cli)
-
(libraries river cmdliner yojson logs logs.fmt logs.cli fmt fmt.tty fmt.cli eio_main unix ptime syndic))
···
(executable
(public_name river-cli)
(name river_cli)
+
(libraries river cmdliner yojson logs logs.fmt logs.cli fmt fmt.tty fmt.cli eio_main unix ptime syndic xdge))
+88 -103
stack/river/bin/river_cli.ml
···
}
type state = {
-
state_dir : Eio.Fs.dir_ty Eio.Path.t;
}
(* State directory management *)
module State = struct
-
let users_dir state = Eio.Path.(state.state_dir / "users")
-
let feeds_dir state = Eio.Path.(state.state_dir / "feeds")
let user_feeds_dir state = Eio.Path.(feeds_dir state / "user")
let user_file state username =
···
Ptime.compare b.updated a.updated
) combined
-
let sync_user env state ~username =
match State.load_user state username with
| None ->
Log.err (fun m -> m "User %s not found" username);
···
| Some user ->
Log.info (fun m -> m "Syncing feeds for user %s..." username);
-
(* Fetch all feeds *)
let fetched_feeds =
List.filter_map (fun source ->
try
Log.info (fun m -> m " Fetching %s (%s)..." source.River.name source.River.url);
-
Some (River.fetch env source)
with e ->
Log.err (fun m -> m " Failed to fetch %s: %s"
source.River.name (Printexc.to_string e));
···
0
end
-
let sync_all env state =
let users = State.list_users state in
if users = [] then begin
Log.info (fun m -> m "No users to sync");
···
Log.info (fun m -> m "Syncing %d users..." (List.length users));
let results =
List.map (fun username ->
-
let result = sync_user env state ~username in
Log.debug (fun m -> m "Completed sync for user");
result
) users
···
(* Cmdliner interface *)
open Cmdliner
-
let state_dir =
-
let doc = "State directory for storing user and feed data" in
-
Arg.(value & opt string "~/.river" & info ["state-dir"; "d"] ~doc)
-
let username_arg =
let doc = "Username" in
Arg.(required & pos 0 (some string) None & info [] ~docv:"USERNAME" ~doc)
···
let log_level = Logs_cli.level ()
let log_style_renderer = Fmt_cli.style_renderer ()
-
(* Commands *)
-
let user_add_cmd =
let doc = "Add a new user" in
-
let term = Term.(const (fun state_dir log_level style_renderer username fullname email ->
setup_logs style_renderer log_level;
-
Eio_main.run @@ fun env ->
-
let state_dir =
-
let path = if String.starts_with ~prefix:"~" state_dir then
-
Filename.concat (Sys.getenv "HOME") (String.sub state_dir 2 (String.length state_dir - 2))
-
else state_dir in
-
Eio.Path.(Eio.Stdenv.fs env / path)
-
in
-
let state = { state_dir } in
State.ensure_directories state;
User.add state ~username ~fullname ~email
-
) $ state_dir $ log_level $ log_style_renderer $ username_arg $ fullname_arg $ email_arg) in
Cmd.v (Cmd.info "add" ~doc) term
-
let user_remove_cmd =
let doc = "Remove a user" in
-
let term = Term.(const (fun state_dir log_level style_renderer username ->
setup_logs style_renderer log_level;
-
Eio_main.run @@ fun env ->
-
let state_dir =
-
let path = if String.starts_with ~prefix:"~" state_dir then
-
Filename.concat (Sys.getenv "HOME") (String.sub state_dir 2 (String.length state_dir - 2))
-
else state_dir in
-
Eio.Path.(Eio.Stdenv.fs env / path)
-
in
-
let state = { state_dir } in
User.remove state ~username
-
) $ state_dir $ log_level $ log_style_renderer $ username_arg) in
Cmd.v (Cmd.info "remove" ~doc) term
-
let user_list_cmd =
let doc = "List all users" in
-
let term = Term.(const (fun state_dir log_level style_renderer ->
setup_logs style_renderer log_level;
-
Eio_main.run @@ fun env ->
-
let state_dir =
-
let path = if String.starts_with ~prefix:"~" state_dir then
-
Filename.concat (Sys.getenv "HOME") (String.sub state_dir 2 (String.length state_dir - 2))
-
else state_dir in
-
Eio.Path.(Eio.Stdenv.fs env / path)
-
in
-
let state = { state_dir } in
User.list state
-
) $ state_dir $ log_level $ log_style_renderer) in
Cmd.v (Cmd.info "list" ~doc) term
-
let user_show_cmd =
let doc = "Show user details" in
-
let term = Term.(const (fun state_dir log_level style_renderer username ->
setup_logs style_renderer log_level;
-
Eio_main.run @@ fun env ->
-
let state_dir =
-
let path = if String.starts_with ~prefix:"~" state_dir then
-
Filename.concat (Sys.getenv "HOME") (String.sub state_dir 2 (String.length state_dir - 2))
-
else state_dir in
-
Eio.Path.(Eio.Stdenv.fs env / path)
-
in
-
let state = { state_dir } in
User.show state ~username
-
) $ state_dir $ log_level $ log_style_renderer $ username_arg) in
Cmd.v (Cmd.info "show" ~doc) term
-
let user_add_feed_cmd =
let doc = "Add a feed to a user" in
-
let term = Term.(const (fun state_dir log_level style_renderer username name url ->
setup_logs style_renderer log_level;
-
Eio_main.run @@ fun env ->
-
let state_dir =
-
let path = if String.starts_with ~prefix:"~" state_dir then
-
Filename.concat (Sys.getenv "HOME") (String.sub state_dir 2 (String.length state_dir - 2))
-
else state_dir in
-
Eio.Path.(Eio.Stdenv.fs env / path)
-
in
-
let state = { state_dir } in
User.add_feed state ~username ~name ~url
-
) $ state_dir $ log_level $ log_style_renderer $ username_arg $ feed_name_arg $ feed_url_arg) in
Cmd.v (Cmd.info "add-feed" ~doc) term
-
let user_remove_feed_cmd =
let doc = "Remove a feed from a user" in
-
let term = Term.(const (fun state_dir log_level style_renderer username url ->
setup_logs style_renderer log_level;
-
Eio_main.run @@ fun env ->
-
let state_dir =
-
let path = if String.starts_with ~prefix:"~" state_dir then
-
Filename.concat (Sys.getenv "HOME") (String.sub state_dir 2 (String.length state_dir - 2))
-
else state_dir in
-
Eio.Path.(Eio.Stdenv.fs env / path)
-
in
-
let state = { state_dir } in
User.remove_feed state ~username ~url
-
) $ state_dir $ log_level $ log_style_renderer $ username_arg $ feed_url_arg) in
Cmd.v (Cmd.info "remove-feed" ~doc) term
-
let user_cmd =
let doc = "Manage users" in
let info = Cmd.info "user" ~doc in
Cmd.group info [
-
user_add_cmd;
-
user_remove_cmd;
-
user_list_cmd;
-
user_show_cmd;
-
user_add_feed_cmd;
-
user_remove_feed_cmd;
]
-
let sync_cmd =
let doc = "Sync feeds for users" in
let username_opt =
let doc = "Sync specific user (omit to sync all)" in
Arg.(value & pos 0 (some string) None & info [] ~docv:"USERNAME" ~doc)
in
-
let term = Term.(const (fun state_dir log_level style_renderer username_opt ->
setup_logs style_renderer log_level;
-
Eio_main.run @@ fun env ->
-
let state_dir =
-
let path = if String.starts_with ~prefix:"~" state_dir then
-
Filename.concat (Sys.getenv "HOME") (String.sub state_dir 2 (String.length state_dir - 2))
-
else state_dir in
-
Eio.Path.(Eio.Stdenv.fs env / path)
in
-
let state = { state_dir } in
-
State.ensure_directories state;
-
match username_opt with
-
| Some username -> Sync.sync_user env state ~username
-
| None -> Sync.sync_all env state
-
) $ state_dir $ log_level $ log_style_renderer $ username_opt) in
Cmd.v (Cmd.info "sync" ~doc) term
-
let main_cmd =
let doc = "River feed management CLI" in
let info = Cmd.info "river-cli" ~version:"1.0" ~doc in
-
Cmd.group info [user_cmd; sync_cmd]
let () =
-
exit (Cmd.eval' main_cmd)
···
}
type state = {
+
xdg : Xdge.t;
}
(* State directory management *)
module State = struct
+
let users_dir state = Eio.Path.(Xdge.state_dir state.xdg / "users")
+
let feeds_dir state = Eio.Path.(Xdge.state_dir state.xdg / "feeds")
let user_feeds_dir state = Eio.Path.(feeds_dir state / "user")
let user_file state username =
···
Ptime.compare b.updated a.updated
) combined
+
let sync_user ~sw env state ~username =
match State.load_user state username with
| None ->
Log.err (fun m -> m "User %s not found" username);
···
| Some user ->
Log.info (fun m -> m "Syncing feeds for user %s..." username);
+
(* Create a single Requests session for all feeds *)
+
let requests = Requests.create ~sw env
+
~follow_redirects:true
+
~max_redirects:5 in
+
+
(* Fetch all feeds using the shared session and switch *)
let fetched_feeds =
List.filter_map (fun source ->
try
Log.info (fun m -> m " Fetching %s (%s)..." source.River.name source.River.url);
+
Some (River.fetch ~sw ~requests env source)
with e ->
Log.err (fun m -> m " Failed to fetch %s: %s"
source.River.name (Printexc.to_string e));
···
0
end
+
let sync_all ~sw env state =
let users = State.list_users state in
if users = [] then begin
Log.info (fun m -> m "No users to sync");
···
Log.info (fun m -> m "Syncing %d users..." (List.length users));
let results =
List.map (fun username ->
+
let result = sync_user ~sw env state ~username in
Log.debug (fun m -> m "Completed sync for user");
result
) users
···
(* Cmdliner interface *)
open Cmdliner
let username_arg =
let doc = "Username" in
Arg.(required & pos 0 (some string) None & info [] ~docv:"USERNAME" ~doc)
···
let log_level = Logs_cli.level ()
let log_style_renderer = Fmt_cli.style_renderer ()
+
(* Commands - these are created within Eio context *)
+
let user_add_cmd fs =
let doc = "Add a new user" in
+
let xdg_term = Xdge.Cmd.term "river" fs ~config:false ~data:false ~cache:false ~runtime:false () in
+
let run log_level style_renderer (xdg, _cfg) username fullname email =
setup_logs style_renderer log_level;
+
let state = { xdg } in
State.ensure_directories state;
User.add state ~username ~fullname ~email
+
in
+
let term = Term.(const run $ log_level $ log_style_renderer $ xdg_term $ username_arg $ fullname_arg $ email_arg) in
Cmd.v (Cmd.info "add" ~doc) term
+
let user_remove_cmd fs =
let doc = "Remove a user" in
+
let xdg_term = Xdge.Cmd.term "river" fs ~config:false ~data:false ~cache:false ~runtime:false () in
+
let run log_level style_renderer (xdg, _cfg) username =
setup_logs style_renderer log_level;
+
let state = { xdg } in
User.remove state ~username
+
in
+
let term = Term.(const run $ log_level $ log_style_renderer $ xdg_term $ username_arg) in
Cmd.v (Cmd.info "remove" ~doc) term
+
let user_list_cmd fs =
let doc = "List all users" in
+
let xdg_term = Xdge.Cmd.term "river" fs ~config:false ~data:false ~cache:false ~runtime:false () in
+
let run log_level style_renderer (xdg, _cfg) =
setup_logs style_renderer log_level;
+
let state = { xdg } in
User.list state
+
in
+
let term = Term.(const run $ log_level $ log_style_renderer $ xdg_term) in
Cmd.v (Cmd.info "list" ~doc) term
+
let user_show_cmd fs =
let doc = "Show user details" in
+
let xdg_term = Xdge.Cmd.term "river" fs ~config:false ~data:false ~cache:false ~runtime:false () in
+
let run log_level style_renderer (xdg, _cfg) username =
setup_logs style_renderer log_level;
+
let state = { xdg } in
User.show state ~username
+
in
+
let term = Term.(const run $ log_level $ log_style_renderer $ xdg_term $ username_arg) in
Cmd.v (Cmd.info "show" ~doc) term
+
let user_add_feed_cmd fs =
let doc = "Add a feed to a user" in
+
let xdg_term = Xdge.Cmd.term "river" fs ~config:false ~data:false ~cache:false ~runtime:false () in
+
let run log_level style_renderer (xdg, _cfg) username name url =
setup_logs style_renderer log_level;
+
let state = { xdg } in
User.add_feed state ~username ~name ~url
+
in
+
let term = Term.(const run $ log_level $ log_style_renderer $ xdg_term $ username_arg $ feed_name_arg $ feed_url_arg) in
Cmd.v (Cmd.info "add-feed" ~doc) term
+
let user_remove_feed_cmd fs =
let doc = "Remove a feed from a user" in
+
let xdg_term = Xdge.Cmd.term "river" fs ~config:false ~data:false ~cache:false ~runtime:false () in
+
let run log_level style_renderer (xdg, _cfg) username url =
setup_logs style_renderer log_level;
+
let state = { xdg } in
User.remove_feed state ~username ~url
+
in
+
let term = Term.(const run $ log_level $ log_style_renderer $ xdg_term $ username_arg $ feed_url_arg) in
Cmd.v (Cmd.info "remove-feed" ~doc) term
+
let user_cmd fs =
let doc = "Manage users" in
let info = Cmd.info "user" ~doc in
Cmd.group info [
+
user_add_cmd fs;
+
user_remove_cmd fs;
+
user_list_cmd fs;
+
user_show_cmd fs;
+
user_add_feed_cmd fs;
+
user_remove_feed_cmd fs;
]
+
let sync_cmd fs env =
let doc = "Sync feeds for users" in
+
let xdg_term = Xdge.Cmd.term "river" fs ~config:false ~data:false ~cache:false ~runtime:false () in
let username_opt =
let doc = "Sync specific user (omit to sync all)" in
Arg.(value & pos 0 (some string) None & info [] ~docv:"USERNAME" ~doc)
in
+
let run log_level style_renderer (xdg, _cfg) username_opt =
setup_logs style_renderer log_level;
+
let state = { xdg } in
+
State.ensure_directories state;
+
(* Create a switch for all sync operations *)
+
Logs.info (fun m -> m "Creating switch for sync operations");
+
let result = Eio.Switch.run @@ fun sw ->
+
Logs.info (fun m -> m "Switch created, running sync");
+
let res = match username_opt with
+
| Some username -> Sync.sync_user ~sw env state ~username
+
| None -> Sync.sync_all ~sw env state
+
in
+
Logs.info (fun m -> m "Sync completed, about to exit switch");
+
res
in
+
Logs.info (fun m -> m "Switch exited, returning result %d" result);
+
result
+
in
+
let term = Term.(const run $ log_level $ log_style_renderer $ xdg_term $ username_opt) in
Cmd.v (Cmd.info "sync" ~doc) term
+
let main_cmd fs env =
let doc = "River feed management CLI" in
let info = Cmd.info "river-cli" ~version:"1.0" ~doc in
+
Cmd.group info [user_cmd fs; sync_cmd fs env]
let () =
+
(* Initialize the Mirage_crypto RNG for TLS.
+
Note: This spawns a background thread for entropy collection. *)
+
Mirage_crypto_rng_unix.use_default ();
+
+
let exit_code = ref 0 in
+
Eio_main.run @@ fun env ->
+
exit_code := Cmd.eval' (main_cmd env#fs env);
+
Logs.info (fun m -> m "About to exit Eio_main.run");
+
Logs.info (fun m -> m "Exited Eio_main.run, calling exit %d" !exit_code);
+
exit !exit_code
+1
stack/river/dune-project
···
cmdliner
yojson
fmt
(odoc :with-doc)))
···
cmdliner
yojson
fmt
+
xdge
(odoc :with-doc)))
+2 -1
stack/river/example/aggregate_feeds.ml
···
]
let main env =
-
let feeds = List.map (River.fetch env) sources in
let posts = River.posts feeds in
let entries = River.create_atom_entries posts in
let feed =
···
]
let main env =
+
Eio.Switch.run @@ fun sw ->
+
let feeds = List.map (River.fetch ~sw env) sources in
let posts = River.posts feeds in
let entries = River.create_atom_entries posts in
let feed =
+2 -2
stack/river/lib/feed.ml
···
msg (fst pos) (snd pos));
failwith "Neither Atom nor RSS2 feed")
-
let fetch env (source : source) =
Log.info (fun m -> m "Fetching feed '%s' from %s" source.name source.url);
let xmlbase = Uri.of_string @@ source.url in
let response =
-
try Http.get env source.url
with e ->
Log.err (fun m -> m "Failed to fetch feed '%s': %s" source.name (Printexc.to_string e));
raise e
···
msg (fst pos) (snd pos));
failwith "Neither Atom nor RSS2 feed")
+
let fetch ~sw ?requests env (source : source) =
Log.info (fun m -> m "Fetching feed '%s' from %s" source.name source.url);
let xmlbase = Uri.of_string @@ source.url in
let response =
+
try Http.get ~sw ?requests env source.url
with e ->
Log.err (fun m -> m "Failed to fetch feed '%s': %s" source.name (Printexc.to_string e));
raise e
+10 -7
stack/river/lib/http.ml
···
exception Status_unhandled of string
exception Timeout
-
let get env url =
Log.info (fun m -> m "Fetching URL: %s" url);
-
Eio.Switch.run @@ fun sw ->
try
-
(* Create a Requests session with automatic redirect following *)
-
let req = Requests.create ~sw env
-
~follow_redirects:true
-
~max_redirects:5 in
-
Log.debug (fun m -> m "Created Requests session with max_redirects=5");
(* Make the GET request with timeout *)
let response =
···
exception Status_unhandled of string
exception Timeout
+
let get ~sw ?requests env url =
Log.info (fun m -> m "Fetching URL: %s" url);
try
+
(* Create or use existing Requests session with automatic redirect following *)
+
let req = match requests with
+
| Some r -> r
+
| None ->
+
Requests.create ~sw env
+
~follow_redirects:true
+
~max_redirects:5
+
in
+
Log.debug (fun m -> m "Using Requests session with max_redirects=5");
(* Make the GET request with timeout *)
let response =
-25
stack/river/lib/http.mli
···
-
(*
-
* Copyright (c) 2014, OCaml.org project
-
* Copyright (c) 2015 KC Sivaramakrishnan <sk826@cl.cam.ac.uk>
-
*
-
* Permission to use, copy, modify, and distribute this software for any
-
* purpose with or without fee is hereby granted, provided that the above
-
* copyright notice and this permission notice appear in all copies.
-
*
-
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
*)
-
-
exception Status_unhandled of string
-
exception Timeout
-
-
val get : Eio_unix.Stdenv.base -> string -> string
-
(** [get env uri] returns the body of the response of the HTTP GET request on [uri].
-
-
If the answer is a redirection, it will follow the redirections up to 5
-
redirects. Uses the provided Eio environment. *)
···
+2 -2
stack/river/lib/river.ml
···
type feed = Feed.t
type post = Post.t
-
let fetch env source =
Log.info (fun m -> m "Fetching feed: %s" source.name);
-
Feed.fetch env source
let name feed = feed.Feed.name
let url feed = feed.Feed.url
···
type feed = Feed.t
type post = Post.t
+
let fetch ~sw ?requests env source =
Log.info (fun m -> m "Fetching feed: %s" source.name);
+
Feed.fetch ~sw ?requests env source
let name feed = feed.Feed.name
let url feed = feed.Feed.url
+15 -4
stack/river/lib/river.mli
···
type feed
type post
-
val fetch : Eio_unix.Stdenv.base -> source -> feed
-
(** [fetch env source] returns an Atom or RSS feed from a source
-
using the provided Eio environment. *)
val name : feed -> string
(** [name feed] is the name of the feed source passed to [fetch]. *)
···
val create_atom_entries : post list -> Syndic.Atom.entry list
(** [create_atom_feed posts] creates a list of atom entries, which can then be
-
used to create an atom feed that is an aggregate of the posts. *)
···
type feed
type post
+
val fetch :
+
sw:Eio.Switch.t ->
+
?requests:(([> float Eio.Time.clock_ty ] as 'a) Eio.Resource.t, ([> [> `Generic ] Eio.Net.ty ] as 'b) Eio.Resource.t) Requests.t ->
+
< clock : 'a Eio.Resource.t;
+
fs : Eio.Fs.dir_ty Eio.Path.t;
+
net : 'b Eio.Resource.t; .. > ->
+
source ->
+
feed
+
(** [fetch ~sw ?requests env source] returns an Atom or RSS feed from a source.
+
+
@param sw The switch to use for resource cleanup
+
@param requests Optional Requests session to reuse. If not provided, a new session will be created
+
@param env The Eio environment
+
@param source The feed source to fetch *)
val name : feed -> string
(** [name feed] is the name of the feed source passed to [fetch]. *)
···
val create_atom_entries : post list -> Syndic.Atom.entry list
(** [create_atom_feed posts] creates a list of atom entries, which can then be
+
used to create an atom feed that is an aggregate of the posts. *)
+1
stack/river/river.opam
···
"cmdliner"
"yojson"
"fmt"
"odoc" {with-doc}
]
build: [
···
"cmdliner"
"yojson"
"fmt"
+
"xdge"
"odoc" {with-doc}
]
build: [
+2 -1
stack/river/test/test_eio_river.ml
···
let main env =
Printf.printf "Testing River library with Eio and Requests...\n";
(* Test fetching feeds *)
let feeds =
try
-
List.map (River.fetch env) test_sources
with
| River__Http.Status_unhandled msg ->
Printf.printf "HTTP error: %s\n" msg;
···
let main env =
Printf.printf "Testing River library with Eio and Requests...\n";
+
Eio.Switch.run @@ fun sw ->
(* Test fetching feeds *)
let feeds =
try
+
List.map (River.fetch ~sw env) test_sources
with
| River__Http.Status_unhandled msg ->
Printf.printf "HTTP error: %s\n" msg;
+2 -1
stack/river/test/test_logging.ml
···
(* Test with logging *)
Printf.printf "Testing River library with logging...\n\n";
(* Demonstrate fetching with logging *)
let feeds =
try
-
List.map (River.fetch env) test_sources
with
| River__Http.Status_unhandled msg ->
Printf.printf "Expected error (for demo): %s\n" msg;
···
(* Test with logging *)
Printf.printf "Testing River library with logging...\n\n";
+
Eio.Switch.run @@ fun sw ->
(* Demonstrate fetching with logging *)
let feeds =
try
+
List.map (River.fetch ~sw env) test_sources
with
| River__Http.Status_unhandled msg ->
Printf.printf "Expected error (for demo): %s\n" msg;
+15 -13
stack/zulip/examples/bot_example.ml
···
(* Simple Bot Example using core Zulip library *)
-
let () =
Printf.printf "OCaml Zulip Bot Example\n";
Printf.printf "=======================\n\n";
-
(* Create test authentication *)
-
let auth = Zulip.Auth.create
~server_url:"https://example.zulipchat.com"
-
~email:"bot@example.com"
~api_key:"example-api-key" in
-
Printf.printf "✅ Created authentication for: %s\n" (Zulip.Auth.email auth);
Printf.printf "✅ Server URL: %s\n" (Zulip.Auth.server_url auth);
-
(* Create client *)
-
let client = Zulip.Client.create () auth in
let client_str = Format.asprintf "%a" Zulip.Client.pp client in
Printf.printf "✅ Created client: %s\n" client_str;
-
(* Test message creation *)
let message = Zulip.Message.create
~type_:`Channel
···
~content:"Hello from OCaml bot!"
~topic:"Bot Testing"
() in
-
Printf.printf "✅ Created message to: %s\n" (String.concat ", " (Zulip.Message.to_ message));
Printf.printf "✅ Message content: %s\n" (Zulip.Message.content message);
Printf.printf "✅ Message topic: %s\n" (match Zulip.Message.topic message with Some t -> t | None -> "none");
-
(* Test API call (mock) *)
(match Zulip.Client.request client ~method_:`GET ~path:"/users/me" () with
| Ok response ->
-
Printf.printf "✅ API request successful: %s\n"
-
(match response with
| `O fields -> String.concat ", " (List.map fst fields)
| _ -> "unknown format")
| Error err ->
Printf.printf "❌ API request failed: %s\n" (Zulip.error_message err));
-
Printf.printf "\n🎉 Bot example completed successfully!\n";
Printf.printf "Note: This uses mock responses since we're not connected to a real Zulip server.\n"
···
(* Simple Bot Example using core Zulip library *)
+
let () = Eio_main.run @@ fun env ->
+
Eio.Switch.run @@ fun sw ->
+
Printf.printf "OCaml Zulip Bot Example\n";
Printf.printf "=======================\n\n";
+
(* Create test authentication *)
+
let auth = Zulip.Auth.create
~server_url:"https://example.zulipchat.com"
+
~email:"bot@example.com"
~api_key:"example-api-key" in
+
Printf.printf "✅ Created authentication for: %s\n" (Zulip.Auth.email auth);
Printf.printf "✅ Server URL: %s\n" (Zulip.Auth.server_url auth);
+
(* Create client *)
+
let client = Zulip.Client.create ~sw env auth in
let client_str = Format.asprintf "%a" Zulip.Client.pp client in
Printf.printf "✅ Created client: %s\n" client_str;
+
(* Test message creation *)
let message = Zulip.Message.create
~type_:`Channel
···
~content:"Hello from OCaml bot!"
~topic:"Bot Testing"
() in
+
Printf.printf "✅ Created message to: %s\n" (String.concat ", " (Zulip.Message.to_ message));
Printf.printf "✅ Message content: %s\n" (Zulip.Message.content message);
Printf.printf "✅ Message topic: %s\n" (match Zulip.Message.topic message with Some t -> t | None -> "none");
+
(* Test API call (mock) *)
(match Zulip.Client.request client ~method_:`GET ~path:"/users/me" () with
| Ok response ->
+
Printf.printf "✅ API request successful: %s\n"
+
(match response with
| `O fields -> String.concat ", " (List.map fst fields)
| _ -> "unknown format")
| Error err ->
Printf.printf "❌ API request failed: %s\n" (Zulip.error_message err));
+
Printf.printf "\n🎉 Bot example completed successfully!\n";
Printf.printf "Note: This uses mock responses since we're not connected to a real Zulip server.\n"
+22 -20
stack/zulip/examples/example.ml
···
open Zulip
-
let () =
Printf.printf "OCaml Zulip Library Example\n";
Printf.printf "===========================\n\n";
-
(* Create authentication *)
-
let auth = Auth.create
~server_url:"https://example.zulipchat.com"
-
~email:"bot@example.com"
~api_key:"your-api-key" in
-
Printf.printf "Created auth for: %s\n" (Auth.email auth);
Printf.printf "Server URL: %s\n" (Auth.server_url auth);
-
(* Create a message *)
-
let message = Message.create
~type_:`Channel
~to_:["general"]
~content:"Hello from OCaml Zulip library!"
~topic:"Test"
() in
-
Printf.printf "\nCreated message:\n";
Printf.printf "- Type: %s\n" (Message_type.to_string (Message.type_ message));
Printf.printf "- To: %s\n" (String.concat ", " (Message.to_ message));
Printf.printf "- Content: %s\n" (Message.content message);
-
Printf.printf "- Topic: %s\n"
(match Message.topic message with Some t -> t | None -> "None");
-
(* Test JSON serialization *)
let json = Message.to_json message in
-
Printf.printf "\nMessage JSON: %s\n"
-
(match json with
-
| `O _ -> "JSON object (serialized correctly)"
| _ -> "Invalid JSON");
-
-
(* Create client (mock) *)
-
let client = Client.create () auth in
-
Printf.printf "\nCreated mock client\n";
-
(* Test basic client request *)
(match Client.request client ~method_:`GET ~path:"/test" () with
| Ok _ -> Printf.printf "Mock request succeeded\n"
-
| Error err -> Printf.printf "Mock request failed: %s\n" (Error.message err));
-
Printf.printf "\nLibrary is working correctly!\n"
···
open Zulip
+
let () = Eio_main.run @@ fun env ->
+
Eio.Switch.run @@ fun sw ->
+
Printf.printf "OCaml Zulip Library Example\n";
Printf.printf "===========================\n\n";
+
(* Create authentication *)
+
let auth = Auth.create
~server_url:"https://example.zulipchat.com"
+
~email:"bot@example.com"
~api_key:"your-api-key" in
+
Printf.printf "Created auth for: %s\n" (Auth.email auth);
Printf.printf "Server URL: %s\n" (Auth.server_url auth);
+
(* Create a message *)
+
let message = Message.create
~type_:`Channel
~to_:["general"]
~content:"Hello from OCaml Zulip library!"
~topic:"Test"
() in
+
Printf.printf "\nCreated message:\n";
Printf.printf "- Type: %s\n" (Message_type.to_string (Message.type_ message));
Printf.printf "- To: %s\n" (String.concat ", " (Message.to_ message));
Printf.printf "- Content: %s\n" (Message.content message);
+
Printf.printf "- Topic: %s\n"
(match Message.topic message with Some t -> t | None -> "None");
+
(* Test JSON serialization *)
let json = Message.to_json message in
+
Printf.printf "\nMessage JSON: %s\n"
+
(match json with
+
| `O _ -> "JSON object (serialized correctly)"
| _ -> "Invalid JSON");
+
+
(* Create client *)
+
let client = Client.create ~sw env auth in
+
Printf.printf "\nCreated client\n";
+
(* Test basic client request *)
(match Client.request client ~method_:`GET ~path:"/test" () with
| Ok _ -> Printf.printf "Mock request succeeded\n"
+
| Error err -> Printf.printf "Mock request failed: %s\n" (Zulip.error_message err));
+
Printf.printf "\nLibrary is working correctly!\n"
+9 -7
stack/zulip/examples/toml_example.ml
···
Printf.printf " Auth Header: %s\n" (Auth.to_basic_auth_header auth);
(* Test creating client *)
-
let client = Client.create () auth in
Printf.printf "✅ Created client successfully\n\n";
-
(* Test basic functionality *)
(match Client.request client ~method_:`GET ~path:"/users/me" () with
| Ok _response -> Printf.printf "✅ Mock API request succeeded\n"
-
| Error err -> Printf.printf "❌ API request failed: %s\n" (Error.message err))
| Error err ->
-
Printf.printf "❌ Failed to load auth from TOML: %s\n" (Error.message err));
(* Example 2: Root-level TOML configuration *)
let root_toml_content = {|
···
Printf.printf " Email: %s\n" (Auth.email auth);
Printf.printf " Server: %s\n" (Auth.server_url auth)
| Error err ->
-
Printf.printf "❌ Failed to parse root-level TOML: %s\n" (Error.message err));
(* Example 3: Test error handling with invalid TOML *)
let invalid_toml = {|
···
Printf.printf "\nTesting error handling with invalid TOML:\n";
(match Auth.from_zuliprc ~path:invalid_file () with
| Ok _ -> Printf.printf "❌ Should have failed with invalid TOML\n"
-
| Error err -> Printf.printf "✅ Correctly handled invalid TOML: %s\n" (Error.message err));
(* Example 4: Test missing file handling *)
Printf.printf "\nTesting missing file handling:\n";
(match Auth.from_zuliprc ~path:"nonexistent.toml" () with
| Ok _ -> Printf.printf "❌ Should have failed with missing file\n"
-
| Error err -> Printf.printf "✅ Correctly handled missing file: %s\n" (Error.message err));
(* Clean up *)
List.iter (fun file ->
···
Printf.printf " Auth Header: %s\n" (Auth.to_basic_auth_header auth);
(* Test creating client *)
+
Eio_main.run @@ fun env ->
+
Eio.Switch.run @@ fun sw ->
+
let client = Client.create ~sw env auth in
Printf.printf "✅ Created client successfully\n\n";
+
(* Test basic functionality *)
(match Client.request client ~method_:`GET ~path:"/users/me" () with
| Ok _response -> Printf.printf "✅ Mock API request succeeded\n"
+
| Error err -> Printf.printf "❌ API request failed: %s\n" (Zulip.error_message err))
| Error err ->
+
Printf.printf "❌ Failed to load auth from TOML: %s\n" (Zulip.error_message err));
(* Example 2: Root-level TOML configuration *)
let root_toml_content = {|
···
Printf.printf " Email: %s\n" (Auth.email auth);
Printf.printf " Server: %s\n" (Auth.server_url auth)
| Error err ->
+
Printf.printf "❌ Failed to parse root-level TOML: %s\n" (Zulip.error_message err));
(* Example 3: Test error handling with invalid TOML *)
let invalid_toml = {|
···
Printf.printf "\nTesting error handling with invalid TOML:\n";
(match Auth.from_zuliprc ~path:invalid_file () with
| Ok _ -> Printf.printf "❌ Should have failed with invalid TOML\n"
+
| Error err -> Printf.printf "✅ Correctly handled invalid TOML: %s\n" (Zulip.error_message err));
(* Example 4: Test missing file handling *)
Printf.printf "\nTesting missing file handling:\n";
(match Auth.from_zuliprc ~path:"nonexistent.toml" () with
| Ok _ -> Printf.printf "❌ Should have failed with missing file\n"
+
| Error err -> Printf.printf "✅ Correctly handled missing file: %s\n" (Zulip.error_message err));
(* Clean up *)
List.iter (fun file ->
+1
stack/zulip/lib/zulip_bot/lib/dune
···
(library
(public_name zulip_bot)
(name zulip_bot)
(libraries zulip unix eio logs mirage-crypto-rng fmt)
(flags (:standard -warn-error -3)))
···
(library
(public_name zulip_bot)
(name zulip_bot)
+
(wrapped true)
(libraries zulip unix eio logs mirage-crypto-rng fmt)
(flags (:standard -warn-error -3)))