My agentic slop goes here. Not intended for anyone else!
1(* Enhanced Echo Bot for Zulip with Logging and CLI
2 Responds to direct messages and mentions by echoing back the message
3 Features verbose logging and command-line configuration *)
4
5open Zulip_bot
6
7(* Set up logging *)
8let src = Logs.Src.create "echo_bot" ~doc:"Zulip Echo Bot"
9module Log = (val Logs.src_log src : Logs.LOG)
10
11module Echo_bot_handler : Bot_handler.S = struct
12 let initialize _config =
13 Log.info (fun m -> m "Initializing echo bot handler");
14 Log.debug (fun m -> m "Bot handler initialized");
15 Ok ()
16
17 let usage () =
18 "Echo Bot - I repeat everything you say to me!"
19
20 let description () =
21 "A simple echo bot that repeats messages sent to it. \
22 Send me a direct message or mention me in a channel."
23
24 let handle_message ~config:_ ~storage ~identity ~message ~env:_ =
25 (* Log the message with colorful formatting *)
26 Log.debug (fun m -> m "@[<h>Received: %a@]" (Message.pp_ansi ~show_json:false) message);
27
28 (* Use the new Message type for cleaner handling *)
29 match message with
30 | Message.Private { common; display_recipient = _ } ->
31
32 (* Check if this is our own message to avoid loops *)
33 let bot_email = Bot_handler.Identity.email identity in
34 if common.sender_email = bot_email then (
35 Log.debug (fun m -> m "Ignoring own message");
36 Ok Bot_handler.Response.None
37 ) else
38 (* Process the message content *)
39 let sender_name = common.sender_full_name in
40
41 (* Remove bot mention using Message utility *)
42 let bot_email = Bot_handler.Identity.email identity in
43 let cleaned_msg = Message.strip_mention message ~user_email:bot_email in
44 Log.debug (fun m -> m "Cleaned message: %s" cleaned_msg);
45
46 (* Create echo response *)
47 let response_content =
48 let lower_msg = String.lowercase_ascii cleaned_msg in
49 if cleaned_msg = "" then
50 Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" sender_name
51 else if lower_msg = "help" then
52 Printf.sprintf "Hi %s! I'm an echo bot with storage. Commands:\n\
53 • `help` - Show this help\n\
54 • `ping` - Test if I'm alive\n\
55 • `store <key> <value>` - Store a value\n\
56 • `get <key>` - Retrieve a value\n\
57 • `delete <key>` - Delete a stored value\n\
58 • `list` - List all stored keys\n\
59 • Any other message - I'll echo it back!" sender_name
60 else if lower_msg = "ping" then (
61 Log.info (fun m -> m "Responding to ping from %s" sender_name);
62 Printf.sprintf "Pong! 🏓 (from %s)" sender_name
63 )
64 else if String.starts_with ~prefix:"store " lower_msg then (
65 (* Parse store command: store <key> <value> *)
66 let parts = String.sub cleaned_msg 6 (String.length cleaned_msg - 6) |> String.trim in
67 match String.index_opt parts ' ' with
68 | Some idx ->
69 let key = String.sub parts 0 idx |> String.trim in
70 let value = String.sub parts (idx + 1) (String.length parts - idx - 1) |> String.trim in
71 (match Bot_storage.put storage ~key ~value with
72 | Ok () ->
73 Log.info (fun m -> m "Stored key=%s value=%s for user %s" key value sender_name);
74 Printf.sprintf "✅ Stored: `%s` = `%s`" key value
75 | Error e ->
76 Log.err (fun m -> m "Failed to store key=%s: %s" key (Zulip.error_message e));
77 Printf.sprintf "❌ Failed to store: %s" (Zulip.error_message e))
78 | None ->
79 "Usage: `store <key> <value>` - Example: `store name John`"
80 )
81 else if String.starts_with ~prefix:"get " lower_msg then (
82 (* Parse get command: get <key> *)
83 let key = String.sub cleaned_msg 4 (String.length cleaned_msg - 4) |> String.trim in
84 match Bot_storage.get storage ~key with
85 | Some value ->
86 Log.info (fun m -> m "Retrieved key=%s value=%s for user %s" key value sender_name);
87 Printf.sprintf "📦 `%s` = `%s`" key value
88 | None ->
89 Log.info (fun m -> m "Key not found: %s" key);
90 Printf.sprintf "❓ Key not found: `%s`" key
91 )
92 else if String.starts_with ~prefix:"delete " lower_msg then (
93 (* Parse delete command: delete <key> *)
94 let key = String.sub cleaned_msg 7 (String.length cleaned_msg - 7) |> String.trim in
95 match Bot_storage.remove storage ~key with
96 | Ok () ->
97 Log.info (fun m -> m "Deleted key=%s for user %s" key sender_name);
98 Printf.sprintf "🗑️ Deleted key: `%s`" key
99 | Error e ->
100 Log.err (fun m -> m "Failed to delete key=%s: %s" key (Zulip.error_message e));
101 Printf.sprintf "❌ Failed to delete: %s" (Zulip.error_message e)
102 )
103 else if lower_msg = "list" then (
104 (* List all stored keys *)
105 match Bot_storage.keys storage with
106 | Ok keys when keys = [] ->
107 "📭 No keys stored yet. Use `store <key> <value>` to add data!"
108 | Ok keys ->
109 let key_list = String.concat "\n" (List.map (fun k -> "• `" ^ k ^ "`") keys) in
110 Printf.sprintf "📋 Stored keys:\n%s\n\nUse `get <key>` to retrieve values." key_list
111 | Error e ->
112 Printf.sprintf "❌ Failed to list keys: %s" (Zulip.error_message e)
113 )
114 else
115 Printf.sprintf "Echo from %s: %s" sender_name cleaned_msg
116 in
117
118 Log.debug (fun m -> m "Generated response: %s" response_content);
119 Log.info (fun m -> m "Sending private reply");
120 Ok (Bot_handler.Response.Reply response_content)
121
122 | Message.Stream { common; display_recipient = _; subject = _; _ } ->
123 (* Check if this is our own message to avoid loops *)
124 let bot_email = Bot_handler.Identity.email identity in
125 if common.sender_email = bot_email then (
126 Log.debug (fun m -> m "Ignoring own message");
127 Ok Bot_handler.Response.None
128 ) else
129 (* Process the message content *)
130 let sender_name = common.sender_full_name in
131
132 (* Remove bot mention using Message utility *)
133 let cleaned_msg = Message.strip_mention message ~user_email:bot_email in
134
135 (* Create echo response *)
136 let response_content =
137 let lower_msg = String.lowercase_ascii cleaned_msg in
138 if cleaned_msg = "" then
139 Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" sender_name
140 else if lower_msg = "help" then
141 Printf.sprintf "Hi %s! I'm an echo bot with storage. Commands:\n\
142 • `help` - Show this help\n\
143 • `ping` - Test if I'm alive\n\
144 • `store <key> <value>` - Store a value\n\
145 • `get <key>` - Retrieve a value\n\
146 • `delete <key>` - Delete a stored value\n\
147 • `list` - List all stored keys\n\
148 • Any other message - I'll echo it back!" sender_name
149 else if lower_msg = "ping" then (
150 Log.info (fun m -> m "Responding to ping from %s" sender_name);
151 Printf.sprintf "Pong! 🏓 (from %s)" sender_name
152 )
153 else if String.starts_with ~prefix:"store " lower_msg then (
154 (* Parse store command: store <key> <value> *)
155 let parts = String.sub cleaned_msg 6 (String.length cleaned_msg - 6) |> String.trim in
156 match String.index_opt parts ' ' with
157 | Some idx ->
158 let key = String.sub parts 0 idx |> String.trim in
159 let value = String.sub parts (idx + 1) (String.length parts - idx - 1) |> String.trim in
160 (match Bot_storage.put storage ~key ~value with
161 | Ok () ->
162 Log.info (fun m -> m "Stored key=%s value=%s for user %s" key value sender_name);
163 Printf.sprintf "✅ Stored: `%s` = `%s`" key value
164 | Error e ->
165 Log.err (fun m -> m "Failed to store key=%s: %s" key (Zulip.error_message e));
166 Printf.sprintf "❌ Failed to store: %s" (Zulip.error_message e))
167 | None ->
168 "Usage: `store <key> <value>` - Example: `store name John`"
169 )
170 else if String.starts_with ~prefix:"get " lower_msg then (
171 (* Parse get command: get <key> *)
172 let key = String.sub cleaned_msg 4 (String.length cleaned_msg - 4) |> String.trim in
173 match Bot_storage.get storage ~key with
174 | Some value ->
175 Log.info (fun m -> m "Retrieved key=%s value=%s for user %s" key value sender_name);
176 Printf.sprintf "📦 `%s` = `%s`" key value
177 | None ->
178 Log.info (fun m -> m "Key not found: %s" key);
179 Printf.sprintf "❓ Key not found: `%s`" key
180 )
181 else if String.starts_with ~prefix:"delete " lower_msg then (
182 (* Parse delete command: delete <key> *)
183 let key = String.sub cleaned_msg 7 (String.length cleaned_msg - 7) |> String.trim in
184 match Bot_storage.remove storage ~key with
185 | Ok () ->
186 Log.info (fun m -> m "Deleted key=%s for user %s" key sender_name);
187 Printf.sprintf "🗑️ Deleted key: `%s`" key
188 | Error e ->
189 Log.err (fun m -> m "Failed to delete key=%s: %s" key (Zulip.error_message e));
190 Printf.sprintf "❌ Failed to delete: %s" (Zulip.error_message e)
191 )
192 else if lower_msg = "list" then (
193 (* List all stored keys *)
194 match Bot_storage.keys storage with
195 | Ok keys when keys = [] ->
196 "📭 No keys stored yet. Use `store <key> <value>` to add data!"
197 | Ok keys ->
198 let key_list = String.concat "\n" (List.map (fun k -> "• `" ^ k ^ "`") keys) in
199 Printf.sprintf "📋 Stored keys:\n%s\n\nUse `get <key>` to retrieve values." key_list
200 | Error e ->
201 Printf.sprintf "❌ Failed to list keys: %s" (Zulip.error_message e)
202 )
203 else
204 Printf.sprintf "Echo from %s: %s" sender_name cleaned_msg
205 in
206
207 Log.debug (fun m -> m "Generated response: %s" response_content);
208 Log.info (fun m -> m "Sending stream reply");
209 Ok (Bot_handler.Response.Reply response_content)
210
211 | Message.Unknown _ ->
212 Log.err (fun m -> m "Received unknown message format");
213 Ok Bot_handler.Response.None
214end
215
216let run_echo_bot config_file verbosity env =
217 (* Set up logging based on verbosity *)
218 Logs.set_reporter (Logs_fmt.reporter ());
219 let log_level = match verbosity with
220 | 0 -> Logs.Info
221 | 1 -> Logs.Debug
222 | _ -> Logs.Debug (* Cap at debug level *)
223 in
224 Logs.set_level (Some log_level);
225
226 (* Also set levels for related modules if they exist *)
227 Logs.Src.set_level src (Some log_level);
228
229 Log.app (fun m -> m "Starting Zulip Echo Bot");
230 Log.app (fun m -> m "Log level: %s" (Logs.level_to_string (Some log_level)));
231 Log.app (fun m -> m "=============================\n");
232
233 (* Load authentication from .zuliprc file *)
234 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with
235 | Ok a ->
236 Log.info (fun m -> m "Loaded authentication for: %s" (Zulip.Auth.email a));
237 Log.info (fun m -> m "Server: %s" (Zulip.Auth.server_url a));
238 a
239 | Error e ->
240 Log.err (fun m -> m "Failed to load .zuliprc: %s" (Zulip.error_message e));
241 Log.app (fun m -> m "\nPlease create a ~/.zuliprc file with:");
242 Log.app (fun m -> m "[api]");
243 Log.app (fun m -> m "email=bot@example.com");
244 Log.app (fun m -> m "key=your-api-key");
245 Log.app (fun m -> m "site=https://your-domain.zulipchat.com");
246 exit 1
247 in
248
249 Eio.Switch.run @@ fun sw ->
250 Log.debug (fun m -> m "Creating Zulip client");
251 let client = Zulip.Client.create ~sw env auth in
252
253 (* Create bot configuration *)
254 let config = Bot_config.create [] in
255 let bot_email = Zulip.Auth.email auth in
256
257 Log.debug (fun m -> m "Creating bot storage for %s" bot_email);
258 let storage = Bot_storage.create client ~bot_email in
259
260 let identity = Bot_handler.Identity.create
261 ~full_name:"Echo Bot"
262 ~email:bot_email
263 ~mention_name:"echobot"
264 in
265 Log.info (fun m -> m "Bot identity created: %s (%s)"
266 (Bot_handler.Identity.full_name identity)
267 (Bot_handler.Identity.email identity));
268
269 (* Create and run the bot *)
270 Log.debug (fun m -> m "Creating bot handler");
271 let handler = Bot_handler.create
272 (module Echo_bot_handler)
273 ~config ~storage ~identity
274 in
275
276 Log.debug (fun m -> m "Creating bot runner");
277 let runner = Bot_runner.create ~env ~client ~handler in
278
279 Log.app (fun m -> m "Echo bot is running!");
280 Log.app (fun m -> m "Send a direct message or mention @echobot in a channel.");
281 Log.app (fun m -> m "Commands: 'help', 'ping', or any message to echo");
282 Log.app (fun m -> m "Press Ctrl+C to stop.\n");
283
284 (* Run in real-time mode *)
285 Log.info (fun m -> m "Starting real-time event loop");
286 try
287 Bot_runner.run_realtime runner;
288 Log.info (fun m -> m "Bot runner exited normally")
289 with
290 | Sys.Break ->
291 Log.info (fun m -> m "Received interrupt signal, shutting down");
292 Bot_runner.shutdown runner
293 | exn ->
294 Log.err (fun m -> m "Bot crashed with exception: %s" (Printexc.to_string exn));
295 Log.debug (fun m -> m "Backtrace: %s" (Printexc.get_backtrace ()));
296 raise exn
297
298(* Command-line interface *)
299open Cmdliner
300
301let config_file =
302 let doc = "Path to .zuliprc configuration file" in
303 Arg.(value & opt (some string) None & info ["c"; "config"] ~docv:"FILE" ~doc)
304
305let verbosity =
306 let doc = "Increase verbosity. Use multiple times for more verbose output." in
307 Arg.(value & flag_all & info ["v"; "verbose"] ~doc)
308
309let verbosity_term =
310 Term.(const List.length $ verbosity)
311
312let bot_cmd eio_env =
313 let doc = "Zulip Echo Bot with verbose logging" in
314 let man = [
315 `S Manpage.s_description;
316 `P "A simple echo bot for Zulip that responds to messages by echoing them back. \
317 Features verbose logging for debugging and development.";
318 `S "CONFIGURATION";
319 `P "The bot reads configuration from a .zuliprc file (default: ~/.zuliprc).";
320 `P "The file should contain:";
321 `Pre "[api]\n\
322 email=bot@example.com\n\
323 key=your-api-key\n\
324 site=https://your-domain.zulipchat.com";
325 `S "LOGGING";
326 `P "Use -v for info level logging, -vv for debug level logging.";
327 `P "Log messages include:";
328 `P "- Message metadata (sender, type, ID)";
329 `P "- Message processing steps";
330 `P "- Bot responses";
331 `P "- Error conditions";
332 `S "COMMANDS";
333 `P "The bot responds to:";
334 `P "- 'help' - Show usage information";
335 `P "- 'ping' - Respond with 'Pong!'";
336 `P "- Any other message - Echo it back";
337 `S Manpage.s_examples;
338 `P "Run with default configuration:";
339 `Pre " echo_bot";
340 `P "Run with verbose logging:";
341 `Pre " echo_bot -v";
342 `P "Run with debug logging:";
343 `Pre " echo_bot -vv";
344 `P "Run with custom config file:";
345 `Pre " echo_bot -c /path/to/.zuliprc";
346 `P "Run with maximum verbosity and custom config:";
347 `Pre " echo_bot -vv -c ~/my-bot.zuliprc";
348 `S Manpage.s_bugs;
349 `P "Report bugs at https://github.com/your-org/zulip-ocaml/issues";
350 `S Manpage.s_see_also;
351 `P "zulip(1), zulip-bot(1)";
352 ] in
353 let info = Cmd.info "echo_bot" ~version:"1.0.0" ~doc ~man in
354 Cmd.v info Term.(const (run_echo_bot) $ config_file $ verbosity_term $ const eio_env)
355
356let () =
357 (* Initialize the cryptographic RNG for the application *)
358 Mirage_crypto_rng_unix.use_default ();
359 Eio_main.run @@ fun env ->
360 exit (Cmd.eval (bot_cmd env))