Zulip bots with Eio

Pass event-level flags through to bot handlers

Zulip puts flags like "mentioned" on the event envelope, not the
message object. Previously the handler had no access to these flags,
causing mention detection to fail for display-name mentions like
@**Poe**. Thread the flags as a string list through the handler type
so handlers can check them directly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+21 -14
+1 -1
README.md
··· 69 69 ```ocaml 70 70 open Zulip_bot 71 71 72 - let echo_handler ~storage:_ ~identity:_ msg = 72 + let echo_handler ~storage:_ ~identity:_ ~flags:_ msg = 73 73 Response.reply ("Echo: " ^ Message.content msg) 74 74 75 75 let () =
+1 -1
examples/atom_feed_bot.ml
··· 291 291 ] 292 292 293 293 (* Create a handler function for the bot *) 294 - let create_handler bot_state ~storage:_ ~identity message = 294 + let create_handler bot_state ~storage:_ ~identity ~flags:_ message = 295 295 let content = Message.content message in 296 296 let bot_email = identity.Bot.email in 297 297 if Message.is_from_email message ~email:bot_email then Response.silent
+1 -1
examples/echo_bot.ml
··· 15 15 module Log = (val Logs.src_log src : Logs.LOG) 16 16 17 17 (* The handler is now just a function *) 18 - let echo_handler ~storage ~identity msg = 18 + let echo_handler ~storage ~identity ~flags:_ msg = 19 19 Log.debug (fun m -> 20 20 m "@[<h>Received: %a@]" (Message.pp_ansi ~show_json:false) msg); 21 21
+1 -1
examples/regression_test.ml
··· 298 298 299 299 (** Bot handler - triggers on "regress" DM *) 300 300 let make_handler ~env ~channel = 301 - fun ~storage ~identity:_ msg -> 301 + fun ~storage ~identity:_ ~flags:_ msg -> 302 302 let content = String.lowercase_ascii (String.trim (Message.content msg)) in 303 303 let sender_email = Message.sender_email msg in 304 304
+8 -7
lib/zulip_bot/bot.ml
··· 8 8 module Log = (val Logs.src_log src : Logs.LOG) 9 9 10 10 type identity = { user_id : int; email : string; full_name : string } 11 - type handler = storage:Storage.t -> identity:identity -> Message.t -> Response.t 11 + type handler = storage:Storage.t -> identity:identity -> flags:string list -> Message.t -> Response.t 12 12 13 13 let create_client ~sw ~env ~config = 14 14 let auth = ··· 79 79 let flgs = 80 80 List.assoc_opt "flags" assoc 81 81 |> Option.fold ~none:[] ~some:(function 82 - | Jsont.Array (f, _) -> f 82 + | Jsont.Array (f, _) -> 83 + List.filter_map 84 + (function Jsont.String (s, _) -> Some s | _ -> None) 85 + f 83 86 | _ -> []) 84 87 in 85 88 (msg, flgs) ··· 93 96 Log.info (fun m -> 94 97 m "@[<h>%a@]" (Message.pp_ansi ~show_json:false) message); 95 98 let is_mentioned = 96 - List.exists 97 - (function Jsont.String ("mentioned", _) -> true | _ -> false) 98 - flags 99 + List.mem "mentioned" flags 99 100 || Message.is_mentioned message ~user_email:identity.email 100 101 in 101 102 let is_private = Message.is_private message in ··· 112 113 if should_process then ( 113 114 Log.info (fun m -> m "Bot should respond to this message"); 114 115 try 115 - let response = handler ~storage ~identity message in 116 + let response = handler ~storage ~identity ~flags message in 116 117 send_response client ~in_reply_to:message response 117 118 with Eio.Exn.Io (e, _) -> 118 119 Log.err (fun m -> m "Error handling message: %a" Eio.Exn.pp_err e)) ··· 179 180 Log.err (fun m -> m "Failed to parse webhook message: %s" err); 180 181 None 181 182 | Ok message -> 182 - let response = handler ~storage ~identity message in 183 + let response = handler ~storage ~identity ~flags:[] message in 183 184 Some response)
+8 -2
lib/zulip_bot/bot.mli
··· 11 11 12 12 {b Example: Single bot} 13 13 {[ 14 - let echo_handler ~storage:_ ~identity:_ msg = 14 + let echo_handler ~storage:_ ~identity:_ ~flags:_ msg = 15 15 Response.reply ("Echo: " ^ Message.content msg) 16 16 17 17 let () = ··· 51 51 } 52 52 (** Bot identity information retrieved from Zulip. *) 53 53 54 - type handler = storage:Storage.t -> identity:identity -> Message.t -> Response.t 54 + type handler = 55 + storage:Storage.t -> 56 + identity:identity -> 57 + flags:string list -> 58 + Message.t -> 59 + Response.t 55 60 (** Handler function signature. 56 61 57 62 A handler receives: 58 63 - [storage]: Key-value storage via Zulip's bot storage API 59 64 - [identity]: The bot's identity (email, name, user_id) 65 + - [flags]: Event-level flags from Zulip (e.g. ["mentioned"], ["wildcard_mentioned"]) 60 66 - The incoming [Message.t] 61 67 62 68 And returns a [Response.t] indicating what action to take. *)
+1 -1
lib/zulip_bot/cmd.mli
··· 18 18 19 19 {b Example usage:} 20 20 {[ 21 - let my_handler ~storage:_ ~identity:_ msg = 21 + let my_handler ~storage:_ ~identity:_ ~flags:_ msg = 22 22 Response.reply ("Hello: " ^ Message.content msg) 23 23 24 24 let () =