My agentic slop goes here. Not intended for anyone else!
at jsont 17 kB view raw
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))