···
+
(* Enhanced Echo Bot for Zulip with Logging and CLI
+
Responds to direct messages and mentions by echoing back the message
+
Features verbose logging and command-line configuration *)
+
let src = Logs.Src.create "echo_bot" ~doc:"Zulip Echo Bot"
+
module Log = (val Logs.src_log src : Logs.LOG)
module Echo_bot_handler : Bot_handler.S = struct
+
let initialize _config =
+
Log.info (fun m -> m "Initializing echo bot handler");
+
Log.debug (fun m -> m "Bot handler initialized");
"Echo Bot - I repeat everything you say to me!"
···
Send me a direct message or mention me in a channel."
let handle_message ~config:_ ~storage:_ ~identity ~message ~env:_ =
+
Log.debug (fun m -> m "Received message for processing");
(* Parse the incoming message *)
let extract_field fields name =
match List.assoc_opt name fields with
+
Log.debug (fun m -> m "Extracted field %s: %s" name s);
+
Log.debug (fun m -> m "Field %s not found or not a string" name);
let extract_int_field fields name =
match List.assoc_opt name fields with
+
let i = int_of_float f in
+
Log.debug (fun m -> m "Extracted field %s: %d" name i);
+
Log.debug (fun m -> m "Field %s not found or not a number" name);
+
Log.info (fun m -> m "Processing message with %d fields" (List.length fields));
(* Extract message details *)
let content = extract_field fields "content" in
let sender_email = extract_field fields "sender_email" in
let sender_full_name = extract_field fields "sender_full_name" in
let message_type = extract_field fields "type" in
+
let sender_id = extract_int_field fields "sender_id" in
+
let message_id = extract_int_field fields "id" in
+
(* Log message metadata *)
+
m "Message metadata: type=%s, sender=%s (%s), id=%s"
+
(Option.value message_type ~default:"unknown")
+
(Option.value sender_full_name ~default:"unknown")
+
(Option.value sender_email ~default:"unknown")
+
(Option.fold ~none:"none" ~some:string_of_int message_id));
(* Check if this is our own message to avoid loops *)
+
let bot_email = Bot_handler.Identity.email identity in
let is_own_message = match sender_email with
+
let is_own = email = bot_email in
+
Log.debug (fun m -> m "Ignoring own message from %s" email);
+
if is_own_message then (
+
Log.debug (fun m -> m "Skipping response to own message");
Ok Bot_handler.Response.None
(* Process the message content *)
+
let response_content = match content, sender_full_name, sender_id with
+
| Some msg, Some name, Some id ->
+
Log.info (fun m -> m "Processing message from %s (ID: %d): %s" name id msg);
(* Remove bot mention if present *)
+
let mention = "@**" ^ Bot_handler.Identity.mention_name identity ^ "**" in
+
if String.starts_with ~prefix:mention msg then (
+
let cleaned = String.sub msg
+
(String.length mention)
+
(String.length msg - String.length mention) in
+
Log.debug (fun m -> m "Removed bot mention, cleaned message: %s" cleaned);
+
Log.debug (fun m -> m "No bot mention to remove");
(* Create echo response *)
+
if cleaned_msg = "" then
+
Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" name
+
else if String.lowercase_ascii cleaned_msg = "help" then
+
Printf.sprintf "Hi %s! I'm an echo bot. Whatever you say to me, I'll repeat back. \
+
Try sending me any message!" name
+
else if String.lowercase_ascii cleaned_msg = "ping" then (
+
Log.info (fun m -> m "Responding to ping from %s" name);
+
Printf.sprintf "Pong! 🏓 (from %s)" name
+
Printf.sprintf "Echo from %s: %s" name cleaned_msg
+
Log.debug (fun m -> m "Generated response: %s" response);
+
Log.warn (fun m -> m "Message without sender name: %s" msg);
Printf.sprintf "Echo: %s" msg
+
Log.warn (fun m -> m "Received message without content");
"I couldn't understand that message."
+
Log.warn (fun m -> m "Incomplete message data");
+
"I couldn't process that message properly."
(* Determine response type based on original message type *)
let response = match message_type with
+
Log.info (fun m -> m "Sending private reply: %s" response_content);
Bot_handler.Response.Reply response_content
+
Log.info (fun m -> m "Sending stream reply: %s" response_content);
Bot_handler.Response.Reply response_content
+
Log.warn (fun m -> m "Unknown message type: %s" other);
+
Bot_handler.Response.None
+
Log.warn (fun m -> m "No message type specified");
Bot_handler.Response.None
+
Log.err (fun m -> m "Received non-object message: %s"
+
| `String _ -> "string"
Ok Bot_handler.Response.None
+
let run_echo_bot config_file verbosity env =
+
(* Set up logging based on verbosity *)
+
Logs.set_reporter (Logs_fmt.reporter ());
+
let log_level = match verbosity with
+
| _ -> Logs.Debug (* Cap at debug level *)
+
Logs.set_level (Some log_level);
+
(* Also set levels for related modules if they exist *)
+
Logs.Src.set_level src (Some log_level);
+
Log.app (fun m -> m "Starting Zulip Echo Bot");
+
Log.app (fun m -> m "Log level: %s" (Logs.level_to_string (Some log_level)));
+
Log.app (fun m -> m "=============================\n");
(* Load authentication from .zuliprc file *)
+
let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with
+
Log.info (fun m -> m "Loaded authentication for: %s" (Zulip.Auth.email a));
+
Log.info (fun m -> m "Server: %s" (Zulip.Auth.server_url a));
+
Log.err (fun m -> m "Failed to load .zuliprc: %s" (Zulip.Error.message e));
+
Log.app (fun m -> m "\nPlease create a ~/.zuliprc file with:");
+
Log.app (fun m -> m "[api]");
+
Log.app (fun m -> m "email=bot@example.com");
+
Log.app (fun m -> m "key=your-api-key");
+
Log.app (fun m -> m "site=https://your-domain.zulipchat.com");
Eio.Switch.run @@ fun sw ->
+
Log.debug (fun m -> m "Creating Zulip client");
let client = Zulip.Client.create ~sw env auth in
(* Create bot configuration *)
let config = Bot_config.create [] in
let bot_email = Zulip.Auth.email auth in
+
Log.debug (fun m -> m "Creating bot storage for %s" bot_email);
let storage = Bot_storage.create client ~bot_email in
let identity = Bot_handler.Identity.create
+
Log.info (fun m -> m "Bot identity created: %s (%s)"
+
(Bot_handler.Identity.full_name identity)
+
(Bot_handler.Identity.email identity));
(* Create and run the bot *)
+
Log.debug (fun m -> m "Creating bot handler");
let handler = Bot_handler.create
(module Echo_bot_handler)
~config ~storage ~identity
+
Log.debug (fun m -> m "Creating bot runner");
let runner = Bot_runner.create ~env ~client ~handler in
+
Log.app (fun m -> m "Echo bot is running!");
+
Log.app (fun m -> m "Send a direct message or mention @echobot in a channel.");
+
Log.app (fun m -> m "Commands: 'help', 'ping', or any message to echo");
+
Log.app (fun m -> m "Press Ctrl+C to stop.\n");
(* Run in real-time mode *)
+
Log.info (fun m -> m "Starting real-time event loop");
+
Bot_runner.run_realtime runner;
+
Log.info (fun m -> m "Bot runner exited normally")
+
Log.info (fun m -> m "Received interrupt signal, shutting down");
+
Bot_runner.shutdown runner
+
Log.err (fun m -> m "Bot crashed with exception: %s" (Printexc.to_string exn));
+
Log.debug (fun m -> m "Backtrace: %s" (Printexc.get_backtrace ()));
+
(* Command-line interface *)
+
let doc = "Path to .zuliprc configuration file" in
+
Arg.(value & opt (some string) None & info ["c"; "config"] ~docv:"FILE" ~doc)
+
let doc = "Increase verbosity. Use multiple times for more verbose output." in
+
Arg.(value & flag_all & info ["v"; "verbose"] ~doc)
+
Term.(const List.length $ verbosity)
+
let doc = "Zulip Echo Bot with verbose logging" in
+
`S Manpage.s_description;
+
`P "A simple echo bot for Zulip that responds to messages by echoing them back. \
+
Features verbose logging for debugging and development.";
+
`P "The bot reads configuration from a .zuliprc file (default: ~/.zuliprc).";
+
`P "The file should contain:";
+
email=bot@example.com\n\
+
site=https://your-domain.zulipchat.com";
+
`P "Use -v for info level logging, -vv for debug level logging.";
+
`P "Log messages include:";
+
`P "- Message metadata (sender, type, ID)";
+
`P "- Message processing steps";
+
`P "- Error conditions";
+
`P "The bot responds to:";
+
`P "- 'help' - Show usage information";
+
`P "- 'ping' - Respond with 'Pong!'";
+
`P "- Any other message - Echo it back";
+
`P "Run with default configuration:";
+
`P "Run with verbose logging:";
+
`P "Run with debug logging:";
+
`P "Run with custom config file:";
+
`Pre " echo_bot -c /path/to/.zuliprc";
+
`P "Run with maximum verbosity and custom config:";
+
`Pre " echo_bot -vv -c ~/my-bot.zuliprc";
+
`P "Report bugs at https://github.com/your-org/zulip-ocaml/issues";
+
`P "zulip(1), zulip-bot(1)";
+
let info = Cmd.info "echo_bot" ~version:"1.0.0" ~doc ~man in
+
Cmd.v info Term.(const (run_echo_bot) $ config_file $ verbosity_term $ const env)
+
Eio_main.run @@ fun env ->
+
exit (Cmd.eval (bot_cmd env))