My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Squashed 'ocaml-zulip/' changes from d2ec426c..50b1018b

50b1018b Pass event-level flags through to bot handlers
b2c5d3a6 Add multi-turn session tracking with silent context accumulation

git-subtree-dir: ocaml-zulip
git-subtree-split: 50b1018bd0ef6ae027decb7dff2b966b598afa51

+50 -28
+2 -2
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 () = ··· 77 77 Eio.Switch.run @@ fun sw -> 78 78 let fs = Eio.Stdenv.fs env in 79 79 let config = Config.load ~fs "echo-bot" in 80 - Bot.run ~sw ~env ~config ~handler:echo_handler 80 + Bot.run ~sw ~env ~config ~handler:echo_handler () 81 81 ``` 82 82 83 83 ## License
+2 -2
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 ··· 339 339 Log.info (fun m -> m "Feed bot is running! Use !feed help for commands."); 340 340 341 341 Zulip_bot.Bot.run ~sw ~env ~config 342 - ~handler:(Interactive_feed_bot.create_handler bot_state) 342 + ~handler:(Interactive_feed_bot.create_handler bot_state) () 343 343 344 344 (* Run scheduled fetcher mode *) 345 345 let run_scheduled verbosity env =
+2 -2
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 ··· 129 129 Log.app (fun m -> m "Commands: 'help', 'ping', or any message to echo"); 130 130 Log.app (fun m -> m "Press Ctrl+C to stop.\n"); 131 131 132 - try Bot.run ~sw ~env ~config ~handler:echo_handler with 132 + try Bot.run ~sw ~env ~config ~handler:echo_handler () with 133 133 | Sys.Break -> 134 134 Log.info (fun m -> m "Received interrupt signal, shutting down") 135 135 | exn ->
+2 -2
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 ··· 330 330 Random.self_init (); 331 331 Eio.Switch.run @@ fun sw -> 332 332 let handler = make_handler ~env ~channel in 333 - Bot.run ~sw ~env ~config ~handler 333 + Bot.run ~sw ~env ~config ~handler () 334 334 335 335 open Cmdliner 336 336
+18 -11
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 = ··· 61 61 m "Stream message sent (id: %d)" (Zulip.Message_response.id resp)) 62 62 | Response.Silent -> Log.debug (fun m -> m "Handler returned silent response") 63 63 64 - let process_event ~client ~storage ~identity ~handler event = 64 + let process_event ?(process_all_messages = false) ~client ~storage ~identity 65 + ~handler event = 65 66 Log.debug (fun m -> 66 67 m "Processing event type: %s" 67 68 (Zulip.Event_type.to_string (Zulip.Event.type_ event))); ··· 78 79 let flgs = 79 80 List.assoc_opt "flags" assoc 80 81 |> Option.fold ~none:[] ~some:(function 81 - | Jsont.Array (f, _) -> f 82 + | Jsont.Array (f, _) -> 83 + List.filter_map 84 + (function Jsont.String (s, _) -> Some s | _ -> None) 85 + f 82 86 | _ -> []) 83 87 in 84 88 (msg, flgs) ··· 92 96 Log.info (fun m -> 93 97 m "@[<h>%a@]" (Message.pp_ansi ~show_json:false) message); 94 98 let is_mentioned = 95 - List.exists 96 - (function Jsont.String ("mentioned", _) -> true | _ -> false) 97 - flags 99 + List.mem "mentioned" flags 98 100 || Message.is_mentioned message ~user_email:identity.email 99 101 in 100 102 let is_private = Message.is_private message in ··· 104 106 Log.debug (fun m -> 105 107 m "Message check: mentioned=%b, private=%b, from_self=%b" 106 108 is_mentioned is_private is_from_self); 107 - if (is_mentioned || is_private) && not is_from_self then ( 109 + let should_process = 110 + if process_all_messages then not is_from_self 111 + else (is_mentioned || is_private) && not is_from_self 112 + in 113 + if should_process then ( 108 114 Log.info (fun m -> m "Bot should respond to this message"); 109 115 try 110 - let response = handler ~storage ~identity message in 116 + let response = handler ~storage ~identity ~flags message in 111 117 send_response client ~in_reply_to:message response 112 118 with Eio.Exn.Io (e, _) -> 113 119 Log.err (fun m -> m "Error handling message: %a" Eio.Exn.pp_err e)) ··· 116 122 m "Not processing (not mentioned and not private)")) 117 123 | _ -> () 118 124 119 - let run ~sw ~env ~config ~handler = 125 + let run ~sw ~env ~config ~handler ?(process_all_messages = false) () = 120 126 Log.info (fun m -> m "Starting bot: %s" config.Config.name); 121 127 let client = create_client ~sw ~env ~config in 122 128 let identity = fetch_identity client in ··· 143 149 Log.debug (fun m -> 144 150 m "Event id=%d, type=%s" (Zulip.Event.id event) 145 151 (Zulip.Event_type.to_string (Zulip.Event.type_ event))); 146 - process_event ~client ~storage ~identity ~handler event) 152 + process_event ~process_all_messages ~client ~storage ~identity 153 + ~handler event) 147 154 events; 148 155 let new_last_id = 149 156 List.fold_left ··· 173 180 Log.err (fun m -> m "Failed to parse webhook message: %s" err); 174 181 None 175 182 | Ok message -> 176 - let response = handler ~storage ~identity message in 183 + let response = handler ~storage ~identity ~flags:[] message in 177 184 Some response)
+22 -7
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 () = ··· 19 19 Eio.Switch.run @@ fun sw -> 20 20 let fs = Eio.Stdenv.fs env in 21 21 let config = Config.load ~fs "echo-bot" in 22 - Bot.run ~sw ~env ~config ~handler:echo_handler 22 + Bot.run ~sw ~env ~config ~handler:echo_handler () 23 23 ]} 24 24 25 25 {b Example: Multiple bots} ··· 34 34 (fun () -> 35 35 Bot.run ~sw ~env 36 36 ~config:(Config.load ~fs "echo-bot") 37 - ~handler:echo_handler); 37 + ~handler:echo_handler ()); 38 38 (fun () -> 39 39 Bot.run ~sw ~env 40 40 ~config:(Config.load ~fs "help-bot") 41 - ~handler:help_handler); 41 + ~handler:help_handler ()); 42 42 ] 43 43 ]} *) 44 44 ··· 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. *) ··· 72 78 ; .. > -> 73 79 config:Config.t -> 74 80 handler:handler -> 81 + ?process_all_messages:bool -> 82 + unit -> 75 83 unit 76 - (** [run ~sw ~env ~config ~handler] runs a bot as a fiber. 84 + (** [run ~sw ~env ~config ~handler ()] runs a bot as a fiber. 77 85 78 86 The bot connects to Zulip's real-time events API and processes incoming 79 87 messages. It runs until the switch is cancelled. ··· 88 96 @param sw Eio switch controlling the bot's lifetime 89 97 @param env Eio environment with clock, net, and fs capabilities 90 98 @param config Bot configuration (credentials and metadata) 91 - @param handler Function to process incoming messages *) 99 + @param handler Function to process incoming messages 100 + @param process_all_messages If true, pass all messages to handler (not just 101 + mentions and DMs). Handler can return [Response.Silent] to not respond. 102 + Default is [false]. *) 92 103 93 104 (** {1 Webhook Mode} *) 94 105 ··· 144 155 @raise Eio.Io on API errors *) 145 156 146 157 val process_event : 158 + ?process_all_messages:bool -> 147 159 client:Zulip.Client.t -> 148 160 storage:Storage.t -> 149 161 identity:identity -> ··· 152 164 unit 153 165 (** [process_event ~client ~storage ~identity ~handler event] processes a single 154 166 Zulip event. 167 + 168 + @param process_all_messages If true, pass all messages to handler (not just 169 + mentions and DMs). Default is [false]. 155 170 156 171 This is useful for custom event loops that need finer control over event 157 172 processing than [run] provides. *)
+2 -2
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 () = 25 25 Eio_main.run @@ fun env -> 26 26 Eio.Switch.run @@ fun sw -> 27 - let run config = Bot.run ~sw ~env ~config ~handler:my_handler in 27 + let run config = Bot.run ~sw ~env ~config ~handler:my_handler () in 28 28 let cmd = Cmd.v info Term.(const run $ Zulip_bot.Cmd.config_term "mybot" env) in 29 29 Cmdliner.Cmd.eval cmd 30 30 ]} *)