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

more

Changed files
+403 -110
stack
+1
stack/river/river.opam
···
"cohttp-lwt-unix" {>= "5.0.0"}
"ptime"
"lwt"
+
"ocamlnet"
"lambdasoup"
"odoc" {with-doc}
]
+145
stack/zulip/lib/zulip/lib/jsonu_syntax.ml
···
+
(** Syntax module for monadic and applicative JSON parsing *)
+
+
open Error
+
+
type 'a parser = json -> ('a, Error.t) result
+
+
(** Monadic bind operator for sequential parsing with error handling *)
+
let ( let* ) = Result.bind
+
+
(** Map operator for transforming successful results *)
+
let ( let+ ) x f = Result.map f x
+
+
(** Applicative parallel composition *)
+
let ( and+ ) x y =
+
match x, y with
+
| Ok x, Ok y -> Ok (x, y)
+
| Error e, _ | _, Error e -> Error e
+
+
(** Applicative parallel composition for 3 values *)
+
let ( and++ ) xy z =
+
match xy, z with
+
| Ok (x, y), Ok z -> Ok (x, y, z)
+
| Error e, _ | _, Error e -> Error e
+
+
(** Applicative parallel composition for 4 values *)
+
let ( and+++ ) xyz w =
+
match xyz, w with
+
| Ok (x, y, z), Ok w -> Ok (x, y, z, w)
+
| Error e, _ | _, Error e -> Error e
+
+
(** Applicative parallel composition for 5 values *)
+
let ( and++++ ) xyzw v =
+
match xyzw, v with
+
| Ok (x, y, z, w), Ok v -> Ok (x, y, z, w, v)
+
| Error e, _ | _, Error e -> Error e
+
+
(** Alternative operator - try first, if fails try second *)
+
let ( <|> ) x y =
+
match x with
+
| Ok _ -> x
+
| Error _ -> y
+
+
(** Provide a default value if parsing fails *)
+
let ( |? ) x default =
+
match x with
+
| Ok v -> v
+
| Error _ -> default
+
+
(** Convert option to result with error message *)
+
let required name = function
+
| Some v -> Ok v
+
| None -> Error (Error.create ~code:(Other "missing_field") ~msg:(Printf.sprintf "Required field '%s' not found" name) ())
+
+
(** Convert option to result with default *)
+
let default v = function
+
| Some x -> x
+
| None -> v
+
+
(** Lift a pure value into parser context *)
+
let pure x = Ok x
+
+
(** Fail with an error message *)
+
let fail msg = Error (Error.create ~code:(Other "parse_error") ~msg ())
+
+
(** Map over a list with error handling *)
+
let traverse f lst =
+
let rec go acc = function
+
| [] -> Ok (List.rev acc)
+
| x :: xs ->
+
let* v = f x in
+
go (v :: acc) xs
+
in
+
go [] lst
+
+
(** Filter and map over a list, dropping errors *)
+
let filter_map f lst =
+
List.filter_map (fun x ->
+
match f x with
+
| Ok v -> Some v
+
| Error _ -> None
+
) lst
+
+
(** Parse a field with a custom parser *)
+
let field fields key parser =
+
match List.assoc_opt key fields with
+
| Some json -> parser json
+
| None -> Error (Jsonu.field_missing_error key)
+
+
(** Parse an optional field with a custom parser *)
+
let field_opt fields key parser =
+
match List.assoc_opt key fields with
+
| Some json ->
+
(match parser json with
+
| Ok v -> Ok (Some v)
+
| Error _ -> Ok None)
+
| None -> Ok None
+
+
(** Parse a field with a default value if missing or fails *)
+
let field_or fields key parser default =
+
match List.assoc_opt key fields with
+
| Some json ->
+
(match parser json with
+
| Ok v -> Ok v
+
| Error _ -> Ok default)
+
| None -> Ok default
+
+
(** Common parsers *)
+
let string = function
+
| `String s -> Ok s
+
| _ -> Error (Error.create ~code:(Other "type_error") ~msg:"Expected string" ())
+
+
let int = function
+
| `Float f -> Ok (int_of_float f)
+
| `String s ->
+
(try Ok (int_of_string s)
+
with _ -> Error (Error.create ~code:(Other "type_error") ~msg:"Expected integer" ()))
+
| _ -> Error (Error.create ~code:(Other "type_error") ~msg:"Expected integer" ())
+
+
let float = function
+
| `Float f -> Ok f
+
| `String s ->
+
(try Ok (float_of_string s)
+
with _ -> Error (Error.create ~code:(Other "type_error") ~msg:"Expected float" ()))
+
| _ -> Error (Error.create ~code:(Other "type_error") ~msg:"Expected float" ())
+
+
let bool = function
+
| `Bool b -> Ok b
+
| _ -> Error (Error.create ~code:(Other "type_error") ~msg:"Expected boolean" ())
+
+
let array parser = function
+
| `A items -> traverse parser items
+
| _ -> Error (Error.create ~code:(Other "type_error") ~msg:"Expected array" ())
+
+
let object_ = function
+
| `O fields -> Ok fields
+
| _ -> Error (Error.create ~code:(Other "type_error") ~msg:"Expected object" ())
+
+
(** Run a parser on JSON *)
+
let parse parser json = parser json
+
+
(** Run a parser with error context *)
+
let with_context ctx parser json =
+
match parser json with
+
| Ok v -> Ok v
+
| Error e -> Error (Error.create ~code:(Error.code e) ~msg:(Printf.sprintf "%s: %s" ctx (Error.message e)) ())
+97
stack/zulip/lib/zulip/lib/jsonu_syntax.mli
···
+
(** Syntax module for monadic and applicative JSON parsing
+
+
This module provides binding operators and combinators to make JSON parsing
+
more ergonomic and composable. It enables code like:
+
+
{[
+
let parse_user json =
+
with_object "user" @@ fun fields ->
+
let+ user_id = field fields "user_id" int
+
and+ email = field fields "email" string
+
and+ full_name = field fields "full_name" string in
+
{ user_id; email; full_name }
+
]}
+
*)
+
+
open Error
+
+
type 'a parser = json -> ('a, Error.t) result
+
+
(** {1 Binding Operators} *)
+
+
(** Monadic bind operator for sequential parsing with error handling *)
+
val ( let* ) : ('a, 'e) result -> ('a -> ('b, 'e) result) -> ('b, 'e) result
+
+
(** Map operator for transforming successful results *)
+
val ( let+ ) : ('a, 'e) result -> ('a -> 'b) -> ('b, 'e) result
+
+
(** Applicative parallel composition for independent field extraction *)
+
val ( and+ ) : ('a, 'e) result -> ('b, 'e) result -> ('a * 'b, 'e) result
+
val ( and++ ) : ('a * 'b, 'e) result -> ('c, 'e) result -> ('a * 'b * 'c, 'e) result
+
val ( and+++ ) : ('a * 'b * 'c, 'e) result -> ('d, 'e) result -> ('a * 'b * 'c * 'd, 'e) result
+
val ( and++++ ) : ('a * 'b * 'c * 'd, 'e) result -> ('f, 'e) result -> ('a * 'b * 'c * 'd * 'f, 'e) result
+
+
(** {1 Alternative and Default Operators} *)
+
+
(** Alternative operator - try first parser, if fails try second *)
+
val ( <|> ) : ('a, 'e) result -> ('a, 'e) result -> ('a, 'e) result
+
+
(** Provide a default value if parsing fails *)
+
val ( |? ) : ('a, 'e) result -> 'a -> 'a
+
+
(** {1 Field Extraction} *)
+
+
(** Parse a required field with a custom parser *)
+
val field : (string * json) list -> string -> 'a parser -> ('a, Error.t) result
+
+
(** Parse an optional field with a custom parser *)
+
val field_opt : (string * json) list -> string -> 'a parser -> ('a option, Error.t) result
+
+
(** Parse a field with a default value if missing or fails *)
+
val field_or : (string * json) list -> string -> 'a parser -> 'a -> ('a, Error.t) result
+
+
(** {1 Basic Parsers} *)
+
+
(** Parse a JSON string *)
+
val string : string parser
+
+
(** Parse a JSON number as integer (handles both int and float) *)
+
val int : int parser
+
+
(** Parse a JSON number as float *)
+
val float : float parser
+
+
(** Parse a JSON boolean *)
+
val bool : bool parser
+
+
(** Parse a JSON array with a parser for elements *)
+
val array : 'a parser -> 'a list parser
+
+
(** Parse a JSON object to get its fields *)
+
val object_ : json -> ((string * json) list, Error.t) result
+
+
(** {1 Utility Functions} *)
+
+
(** Convert option to result with error message *)
+
val required : string -> 'a option -> ('a, Error.t) result
+
+
(** Get value from option with default *)
+
val default : 'a -> 'a option -> 'a
+
+
(** Lift a pure value into parser context *)
+
val pure : 'a -> ('a, 'e) result
+
+
(** Fail with an error message *)
+
val fail : string -> ('a, Error.t) result
+
+
(** Map over a list with error handling *)
+
val traverse : ('a -> ('b, Error.t) result) -> 'a list -> ('b list, Error.t) result
+
+
(** Filter and map over a list, dropping errors *)
+
val filter_map : ('a -> ('b, Error.t) result) -> 'a list -> 'b list
+
+
(** Run a parser on JSON *)
+
val parse : 'a parser -> json -> ('a, Error.t) result
+
+
(** Run a parser with error context *)
+
val with_context : string -> 'a parser -> 'a parser
+90 -110
stack/zulip/lib/zulip_bot/lib/message.ml
···
let full_name t = t.full_name
let short_name t = t.short_name
-
let of_json json =
-
Zulip.Jsonu.with_object "user" (fun fields ->
-
(* Handle both "user_id" and "id" field names *)
-
let user_id_result =
-
match Zulip.Jsonu.get_int fields "user_id" with
-
| Ok id -> Ok id
-
| Error _ -> Zulip.Jsonu.get_int fields "id"
-
in
-
match user_id_result, Zulip.Jsonu.get_string fields "email", Zulip.Jsonu.get_string fields "full_name" with
-
| Ok user_id, Ok email, Ok full_name ->
-
let short_name = Zulip.Jsonu.get_string_opt fields "short_name" in
-
Ok { user_id; email; full_name; short_name }
-
| Error e, _, _ | _, Error e, _ | _, _, Error e -> Error e
-
) json
+
let of_json (json : Zulip.Error.json) : (t, Zulip.Error.t) result =
+
let open Zulip.Jsonu_syntax in
+
(Zulip.Jsonu.with_object "user" @@ fun fields ->
+
let* user_id = (field fields "user_id" int) <|> (field fields "id" int) in
+
let* email = field fields "email" string in
+
let* full_name = field fields "full_name" string in
+
let* short_name = field_opt fields "short_name" string in
+
Ok { user_id; email; full_name; short_name }) json
end
(** Reaction representation *)
···
let reaction_type t = t.reaction_type
let user_id t = t.user_id
-
let of_json json =
-
Zulip.Jsonu.with_object "reaction" (fun fields ->
-
match Zulip.Jsonu.get_string fields "emoji_name",
-
Zulip.Jsonu.get_string fields "emoji_code",
-
Zulip.Jsonu.get_string fields "reaction_type" with
-
| Ok emoji_name, Ok emoji_code, Ok reaction_type ->
-
(* Try both "user_id" and "user" -> "user_id" patterns *)
-
let user_id_result =
-
match Zulip.Jsonu.get_int fields "user_id" with
-
| Ok id -> Ok id
-
| Error _ ->
-
match Zulip.Jsonu.get_object fields "user" with
-
| Ok user_obj -> Zulip.Jsonu.get_int user_obj "user_id"
-
| Error e -> Error e
-
in
-
(match user_id_result with
-
| Ok user_id -> Ok { emoji_name; emoji_code; reaction_type; user_id }
-
| Error e -> Error e)
-
| Error e, _, _ | _, Error e, _ | _, _, Error e -> Error e
-
) json
+
let of_json (json : Zulip.Error.json) : (t, Zulip.Error.t) result =
+
let open Zulip.Jsonu_syntax in
+
(Zulip.Jsonu.with_object "reaction" @@ fun fields ->
+
let* emoji_name = field fields "emoji_name" string in
+
let* emoji_code = field fields "emoji_code" string in
+
let* reaction_type = field fields "reaction_type" string in
+
let* user_id =
+
(field fields "user_id" int) <|>
+
(match field fields "user" object_ with
+
| Ok user_obj -> field user_obj "user_id" int
+
| Error _ -> fail "user_id not found") in
+
Ok { emoji_name; emoji_code; reaction_type; user_id }) json
end
+
+
let parse_reaction_json json = Reaction.of_json json
+
let parse_user_json json = User.of_json json
(** Common message fields *)
type common = {
···
(** Helper function to parse common fields *)
let parse_common json =
-
Zulip.Jsonu.parse_with_error "common fields" (fun () ->
-
Zulip.Jsonu.with_object "message" (fun fields ->
-
match Zulip.Jsonu.get_int fields "id",
-
Zulip.Jsonu.get_int fields "sender_id",
-
Zulip.Jsonu.get_string fields "sender_email",
-
Zulip.Jsonu.get_string fields "sender_full_name" with
-
| Ok id, Ok sender_id, Ok sender_email, Ok sender_full_name ->
-
let sender_short_name = Zulip.Jsonu.get_string_opt fields "sender_short_name" in
-
let timestamp = Zulip.Jsonu.get_float_default fields "timestamp" 0.0 in
-
let content = Zulip.Jsonu.get_string_default fields "content" "" in
-
let content_type = Zulip.Jsonu.get_string_default fields "content_type" "text/html" in
+
Zulip.Jsonu.parse_with_error "common fields" @@ fun () ->
+
(Zulip.Jsonu.with_object "message" @@ fun fields ->
+
let open Zulip.Jsonu_syntax in
+
let* id = field fields "id" int in
+
let* sender_id = field fields "sender_id" int in
+
let* sender_email = field fields "sender_email" string in
+
let* sender_full_name = field fields "sender_full_name" string in
+
let sender_short_name = field_opt fields "sender_short_name" string |? None in
+
let timestamp = field_or fields "timestamp" float 0.0 |? 0.0 in
+
let content = field_or fields "content" string "" |? "" in
+
let content_type = field_or fields "content_type" string "text/html" |? "text/html" in
-
let reactions =
-
match Zulip.Jsonu.get_array_opt fields "reactions" with
-
| Some reactions_json ->
-
List.filter_map (fun r ->
-
match Reaction.of_json r with
-
| Ok reaction -> Some reaction
-
| Error err ->
-
Log.warn (fun m -> m "Failed to parse reaction: %s" (Zulip.Error.message err));
-
None
-
) reactions_json
-
| None -> []
-
in
+
let reactions =
+
match Zulip.Jsonu.get_array_opt fields "reactions" with
+
| Some reactions_json ->
+
List.filter_map (fun r ->
+
match parse_reaction_json r with
+
| Ok reaction -> Some reaction
+
| Error err ->
+
Log.warn (fun m -> m "Failed to parse reaction: %s" (Zulip.Error.message err));
+
None
+
) reactions_json
+
| None -> []
+
in
-
let submessages = Zulip.Jsonu.get_array_opt fields "submessages" |> Option.value ~default:[] in
+
let submessages = Zulip.Jsonu.get_array_opt fields "submessages" |> Option.value ~default:[] in
-
let flags =
-
match Zulip.Jsonu.get_array_opt fields "flags" with
-
| Some flags_json ->
-
List.filter_map Zulip.Jsonu.to_string_safe flags_json
-
| None -> []
-
in
+
let flags =
+
match Zulip.Jsonu.get_array_opt fields "flags" with
+
| Some flags_json -> List.filter_map Zulip.Jsonu.to_string_safe flags_json
+
| None -> []
+
in
-
let is_me_message = Zulip.Jsonu.get_bool_default fields "is_me_message" false in
-
let client = Zulip.Jsonu.get_string_default fields "client" "" in
-
let gravatar_hash = Zulip.Jsonu.get_string_default fields "gravatar_hash" "" in
-
let avatar_url = Zulip.Jsonu.get_string_opt fields "avatar_url" in
+
let is_me_message = field_or fields "is_me_message" bool false |? false in
+
let client = field_or fields "client" string "" |? "" in
+
let gravatar_hash = field_or fields "gravatar_hash" string "" |? "" in
+
let avatar_url = field_opt fields "avatar_url" string |? None in
-
Ok {
-
id; sender_id; sender_email; sender_full_name; sender_short_name;
-
timestamp; content; content_type; reactions; submessages;
-
flags; is_me_message; client; gravatar_hash; avatar_url
-
}
-
| Error e, _, _, _ | _, Error e, _, _ | _, _, Error e, _ | _, _, _, Error e -> Error e
-
) json
-
)
+
Ok {
+
id; sender_id; sender_email; sender_full_name; sender_short_name;
+
timestamp; content; content_type; reactions; submessages;
+
flags; is_me_message; client; gravatar_hash; avatar_url
+
}) json
(** JSON parsing *)
let of_json json =
Log.debug (fun m -> m "Parsing message JSON: %s" (Zulip.Jsonu.to_string_pretty json));
+
let open Zulip.Jsonu_syntax in
match parse_common json with
| Error err -> Error (Zulip.Error.message err)
| Ok common ->
-
Zulip.Jsonu.parse_with_error "message type" (fun () ->
-
Zulip.Jsonu.with_object "message" (fun fields ->
-
match Zulip.Jsonu.get_string fields "type" with
-
| Ok "private" ->
-
(match Zulip.Jsonu.get_array fields "display_recipient" with
-
| Ok recipient_json ->
-
let users_results = List.map User.of_json recipient_json in
-
let users = List.fold_left (fun acc result ->
-
match result with
-
| Ok user -> user :: acc
-
| Error err ->
-
Log.warn (fun m -> m "Failed to parse user in display_recipient: %s" (Zulip.Error.message err));
-
acc
-
) [] (List.rev users_results) in
+
(Zulip.Jsonu.parse_with_error "message type" @@ fun () ->
+
(Zulip.Jsonu.with_object "message" @@ fun fields ->
+
match Zulip.Jsonu.get_string fields "type" with
+
| Ok "private" ->
+
let* recipient_json = field fields "display_recipient" (array (fun x -> Ok x)) in
+
let users = List.filter_map (fun u ->
+
match parse_user_json u with
+
| Ok user -> Some user
+
| Error err ->
+
Log.warn (fun m -> m "Failed to parse user in display_recipient: %s" (Zulip.Error.message err));
+
None
+
) recipient_json in
-
if List.length users = 0 && List.length users_results > 0 then
-
Error (Zulip.Jsonu.json_error "Failed to parse any users in display_recipient")
-
else
-
Ok (Private { common; display_recipient = users })
-
| Error e -> Error e)
+
if List.length users = 0 && List.length recipient_json > 0 then
+
fail "Failed to parse any users in display_recipient"
+
else
+
Ok (Private { common; display_recipient = users })
-
| Ok "stream" ->
-
(match Zulip.Jsonu.get_string fields "display_recipient",
-
Zulip.Jsonu.get_int fields "stream_id",
-
Zulip.Jsonu.get_string fields "subject" with
-
| Ok display_recipient, Ok stream_id, Ok subject ->
-
Ok (Stream { common; display_recipient; stream_id; subject })
-
| Error e, _, _ | _, Error e, _ | _, _, Error e -> Error e)
+
| Ok "stream" ->
+
let* display_recipient = field fields "display_recipient" string in
+
let* stream_id = field fields "stream_id" int in
+
let* subject = field fields "subject" string in
+
Ok (Stream { common; display_recipient; stream_id; subject })
-
| Ok unknown_type ->
-
Log.warn (fun m -> m "Unknown message type: %s" unknown_type);
-
Ok (Unknown { common; raw_json = json })
+
| Ok unknown_type ->
+
Log.warn (fun m -> m "Unknown message type: %s" unknown_type);
+
Ok (Unknown { common; raw_json = json })
-
| Error _ ->
-
Log.warn (fun m -> m "No message type field found");
-
Ok (Unknown { common; raw_json = json })
-
) json
-
) |> Result.map_error Zulip.Error.message
+
| Error _ ->
+
Log.warn (fun m -> m "No message type field found");
+
Ok (Unknown { common; raw_json = json })
+
) json) |> Result.map_error Zulip.Error.message
(** Accessor functions *)
let get_common = function
+2
stack/zulip/lib/zulip_bot/lib/message.mli
···
val email : t -> string
val full_name : t -> string
val short_name : t -> string option
+
val of_json : Zulip.Error.json -> (t, Zulip.Error.t) result
end
(** Reaction representation *)
···
val emoji_code : t -> string
val reaction_type : t -> string
val user_id : t -> int
+
val of_json : Zulip.Error.json -> (t, Zulip.Error.t) result
end
(** Common message fields *)
+68
stack/zulip/test_bot.sh
···
+
#!/bin/bash
+
+
# Simple test script for Zulip bots
+
# Ensure you have a .zuliprc file configured first
+
+
echo "Zulip Bot Test Script"
+
echo "====================="
+
echo ""
+
+
if [ ! -f "$HOME/.zuliprc" ]; then
+
echo "Error: ~/.zuliprc not found"
+
echo ""
+
echo "Create ~/.zuliprc with the following format:"
+
echo "[api]"
+
echo "email = bot@example.com"
+
echo "key = your-api-key-here"
+
echo "site = https://your-subdomain.zulipchat.com"
+
exit 1
+
fi
+
+
echo "Found .zuliprc configuration"
+
echo ""
+
+
PS3="Select a bot to test: "
+
options=("Echo Bot" "Echo Bot (verbose)" "Test Realtime Bot" "Atom Feed Bot (interactive)" "Quit")
+
+
select opt in "${options[@]}"
+
do
+
case $opt in
+
"Echo Bot")
+
echo "Starting Echo Bot..."
+
echo "The bot will echo all messages it receives"
+
echo "Press Ctrl+C to stop"
+
echo ""
+
dune exec zulip_bot/echo_bot
+
break
+
;;
+
"Echo Bot (verbose)")
+
echo "Starting Echo Bot with verbose logging..."
+
echo "The bot will echo all messages with detailed logs"
+
echo "Press Ctrl+C to stop"
+
echo ""
+
dune exec zulip_bot/echo_bot -- -vv
+
break
+
;;
+
"Test Realtime Bot")
+
echo "Starting Test Realtime Bot..."
+
echo "This bot logs all received messages and tests storage"
+
echo "Press Ctrl+C to stop"
+
echo ""
+
dune exec zulip_bot/test_realtime_bot
+
break
+
;;
+
"Atom Feed Bot (interactive)")
+
echo "Starting Atom Feed Bot in interactive mode..."
+
echo "Use !feed help to see available commands"
+
echo "Press Ctrl+C to stop"
+
echo ""
+
dune exec zulip_bot/atom_feed_bot -- interactive
+
break
+
;;
+
"Quit")
+
echo "Exiting..."
+
break
+
;;
+
*) echo "Invalid option $REPLY";;
+
esac
+
done