···
1
-
(* Simple Echo Bot for Zulip
2
-
Responds to direct messages and mentions by echoing back the message *)
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 *)
8
+
let src = Logs.Src.create "echo_bot" ~doc:"Zulip Echo Bot"
9
+
module Log = (val Logs.src_log src : Logs.LOG)
module Echo_bot_handler : Bot_handler.S = struct
7
-
let initialize _ = Ok ()
12
+
let initialize _config =
13
+
Log.info (fun m -> m "Initializing echo bot handler");
14
+
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:_ =
25
+
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
20
-
| Some (`String s) -> Some s
30
+
| Some (`String s) ->
31
+
Log.debug (fun m -> m "Extracted field %s: %s" name s);
34
+
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
26
-
| Some (`Float f) -> Some (int_of_float f)
40
+
| Some (`Float f) ->
41
+
let i = int_of_float f in
42
+
Log.debug (fun m -> m "Extracted field %s: %d" name i);
45
+
Log.debug (fun m -> m "Field %s not found or not a number" name);
51
+
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
37
-
let _sender_id = extract_int_field fields "sender_id" in
58
+
let sender_id = extract_int_field fields "sender_id" in
59
+
let message_id = extract_int_field fields "id" in
61
+
(* Log message metadata *)
63
+
m "Message metadata: type=%s, sender=%s (%s), id=%s"
64
+
(Option.value message_type ~default:"unknown")
65
+
(Option.value sender_full_name ~default:"unknown")
66
+
(Option.value sender_email ~default:"unknown")
67
+
(Option.fold ~none:"none" ~some:string_of_int message_id));
(* Check if this is our own message to avoid loops *)
70
+
let bot_email = Bot_handler.Identity.email identity in
let is_own_message = match sender_email with
41
-
| Some email -> email = (Bot_handler.Identity.email identity)
73
+
let is_own = email = bot_email in
75
+
Log.debug (fun m -> m "Ignoring own message from %s" email);
45
-
if is_own_message then
80
+
if is_own_message then (
81
+
Log.debug (fun m -> m "Skipping response to own message");
Ok Bot_handler.Response.None
(* Process the message content *)
49
-
let response_content = match content, sender_full_name with
50
-
| Some msg, Some name ->
85
+
let response_content = match content, sender_full_name, sender_id with
86
+
| Some msg, Some name, Some id ->
87
+
Log.info (fun m -> m "Processing message from %s (ID: %d): %s" name id msg);
(* Remove bot mention if present *)
90
+
let mention = "@**" ^ Bot_handler.Identity.mention_name identity ^ "**" in
53
-
let mention = "@**" ^ Bot_handler.Identity.mention_name identity ^ "**" in
55
-
if String.starts_with ~prefix:mention msg then
56
-
String.sub msg (String.length mention) (String.length msg - String.length mention)
92
+
if String.starts_with ~prefix:mention msg then (
93
+
let cleaned = String.sub msg
94
+
(String.length mention)
95
+
(String.length msg - String.length mention) in
96
+
Log.debug (fun m -> m "Removed bot mention, cleaned message: %s" cleaned);
99
+
Log.debug (fun m -> m "No bot mention to remove");
(* Create echo response *)
63
-
if cleaned_msg = "" then
64
-
Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" name
65
-
else if String.lowercase_ascii cleaned_msg = "help" then
66
-
Printf.sprintf "Hi %s! I'm an echo bot. Whatever you say to me, I'll repeat back. \
67
-
Try sending me any message!" name
69
-
Printf.sprintf "Echo from %s: %s" name cleaned_msg
106
+
if cleaned_msg = "" then
107
+
Printf.sprintf "Hello %s! Send me a message and I'll echo it back!" name
108
+
else if String.lowercase_ascii cleaned_msg = "help" then
109
+
Printf.sprintf "Hi %s! I'm an echo bot. Whatever you say to me, I'll repeat back. \
110
+
Try sending me any message!" name
111
+
else if String.lowercase_ascii cleaned_msg = "ping" then (
112
+
Log.info (fun m -> m "Responding to ping from %s" name);
113
+
Printf.sprintf "Pong! 🏓 (from %s)" name
116
+
Printf.sprintf "Echo from %s: %s" name cleaned_msg
119
+
Log.debug (fun m -> m "Generated response: %s" response);
122
+
| Some msg, None, _ ->
123
+
Log.warn (fun m -> m "Message without sender name: %s" msg);
Printf.sprintf "Echo: %s" msg
127
+
Log.warn (fun m -> m "Received message without content");
"I couldn't understand that message."
131
+
Log.warn (fun m -> m "Incomplete message data");
132
+
"I couldn't process that message properly."
(* Determine response type based on original message type *)
let response = match message_type with
79
-
(* For private messages, reply directly *)
138
+
Log.info (fun m -> m "Sending private reply: %s" response_content);
Bot_handler.Response.Reply response_content
82
-
(* For stream messages, reply in the same topic *)
141
+
Log.info (fun m -> m "Sending stream reply: %s" response_content);
Bot_handler.Response.Reply response_content
144
+
Log.warn (fun m -> m "Unknown message type: %s" other);
145
+
Bot_handler.Response.None
147
+
Log.warn (fun m -> m "No message type specified");
Bot_handler.Response.None
154
+
Log.err (fun m -> m "Received non-object message: %s"
157
+
| `Bool _ -> "bool"
158
+
| `Float _ -> "float"
159
+
| `String _ -> "string"
161
+
| _ -> "unknown"));
Ok Bot_handler.Response.None
93
-
let run_echo_bot env =
94
-
Printf.printf "Starting Zulip Echo Bot...\n";
95
-
Printf.printf "=============================\n\n";
165
+
let run_echo_bot config_file verbosity env =
166
+
(* Set up logging based on verbosity *)
167
+
Logs.set_reporter (Logs_fmt.reporter ());
168
+
let log_level = match verbosity with
171
+
| _ -> Logs.Debug (* Cap at debug level *)
173
+
Logs.set_level (Some log_level);
175
+
(* Also set levels for related modules if they exist *)
176
+
Logs.Src.set_level src (Some log_level);
178
+
Log.app (fun m -> m "Starting Zulip Echo Bot");
179
+
Log.app (fun m -> m "Log level: %s" (Logs.level_to_string (Some log_level)));
180
+
Log.app (fun m -> m "=============================\n");
(* Load authentication from .zuliprc file *)
98
-
let auth = match Zulip.Auth.from_zuliprc () with
183
+
let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with
100
-
Printf.printf "Loaded authentication for: %s\n" (Zulip.Auth.email a);
101
-
Printf.printf "Server: %s\n\n" (Zulip.Auth.server_url a);
185
+
Log.info (fun m -> m "Loaded authentication for: %s" (Zulip.Auth.email a));
186
+
Log.info (fun m -> m "Server: %s" (Zulip.Auth.server_url a));
104
-
Printf.eprintf "Failed to load .zuliprc: %s\n" (Zulip.Error.message e);
105
-
Printf.eprintf "\nPlease create a ~/.zuliprc file with:\n";
106
-
Printf.eprintf "[api]\n";
107
-
Printf.eprintf "email=bot@example.com\n";
108
-
Printf.eprintf "key=your-api-key\n";
109
-
Printf.eprintf "site=https://your-domain.zulipchat.com\n";
189
+
Log.err (fun m -> m "Failed to load .zuliprc: %s" (Zulip.Error.message e));
190
+
Log.app (fun m -> m "\nPlease create a ~/.zuliprc file with:");
191
+
Log.app (fun m -> m "[api]");
192
+
Log.app (fun m -> m "email=bot@example.com");
193
+
Log.app (fun m -> m "key=your-api-key");
194
+
Log.app (fun m -> m "site=https://your-domain.zulipchat.com");
Eio.Switch.run @@ fun sw ->
199
+
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
206
+
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
214
+
Log.info (fun m -> m "Bot identity created: %s (%s)"
215
+
(Bot_handler.Identity.full_name identity)
216
+
(Bot_handler.Identity.email identity));
(* Create and run the bot *)
219
+
Log.debug (fun m -> m "Creating bot handler");
let handler = Bot_handler.create
(module Echo_bot_handler)
~config ~storage ~identity
225
+
Log.debug (fun m -> m "Creating bot runner");
let runner = Bot_runner.create ~env ~client ~handler in
134
-
Printf.printf "Echo bot is running!\n";
135
-
Printf.printf "Send a direct message or mention @echobot in a channel.\n";
136
-
Printf.printf "Press Ctrl+C to stop.\n\n";
228
+
Log.app (fun m -> m "Echo bot is running!");
229
+
Log.app (fun m -> m "Send a direct message or mention @echobot in a channel.");
230
+
Log.app (fun m -> m "Commands: 'help', 'ping', or any message to echo");
231
+
Log.app (fun m -> m "Press Ctrl+C to stop.\n");
(* Run in real-time mode *)
139
-
Bot_runner.run_realtime runner
234
+
Log.info (fun m -> m "Starting real-time event loop");
236
+
Bot_runner.run_realtime runner;
237
+
Log.info (fun m -> m "Bot runner exited normally")
240
+
Log.info (fun m -> m "Received interrupt signal, shutting down");
241
+
Bot_runner.shutdown runner
243
+
Log.err (fun m -> m "Bot crashed with exception: %s" (Printexc.to_string exn));
244
+
Log.debug (fun m -> m "Backtrace: %s" (Printexc.get_backtrace ()));
247
+
(* Command-line interface *)
251
+
let doc = "Path to .zuliprc configuration file" in
252
+
Arg.(value & opt (some string) None & info ["c"; "config"] ~docv:"FILE" ~doc)
255
+
let doc = "Increase verbosity. Use multiple times for more verbose output." in
256
+
Arg.(value & flag_all & info ["v"; "verbose"] ~doc)
258
+
let verbosity_term =
259
+
Term.(const List.length $ verbosity)
262
+
let doc = "Zulip Echo Bot with verbose logging" in
264
+
`S Manpage.s_description;
265
+
`P "A simple echo bot for Zulip that responds to messages by echoing them back. \
266
+
Features verbose logging for debugging and development.";
267
+
`S "CONFIGURATION";
268
+
`P "The bot reads configuration from a .zuliprc file (default: ~/.zuliprc).";
269
+
`P "The file should contain:";
271
+
email=bot@example.com\n\
272
+
key=your-api-key\n\
273
+
site=https://your-domain.zulipchat.com";
275
+
`P "Use -v for info level logging, -vv for debug level logging.";
276
+
`P "Log messages include:";
277
+
`P "- Message metadata (sender, type, ID)";
278
+
`P "- Message processing steps";
279
+
`P "- Bot responses";
280
+
`P "- Error conditions";
282
+
`P "The bot responds to:";
283
+
`P "- 'help' - Show usage information";
284
+
`P "- 'ping' - Respond with 'Pong!'";
285
+
`P "- Any other message - Echo it back";
286
+
`S Manpage.s_examples;
287
+
`P "Run with default configuration:";
289
+
`P "Run with verbose logging:";
290
+
`Pre " echo_bot -v";
291
+
`P "Run with debug logging:";
292
+
`Pre " echo_bot -vv";
293
+
`P "Run with custom config file:";
294
+
`Pre " echo_bot -c /path/to/.zuliprc";
295
+
`P "Run with maximum verbosity and custom config:";
296
+
`Pre " echo_bot -vv -c ~/my-bot.zuliprc";
298
+
`P "Report bugs at https://github.com/your-org/zulip-ocaml/issues";
299
+
`S Manpage.s_see_also;
300
+
`P "zulip(1), zulip-bot(1)";
302
+
let info = Cmd.info "echo_bot" ~version:"1.0.0" ~doc ~man in
303
+
Cmd.v info Term.(const (run_echo_bot) $ config_file $ verbosity_term $ const env)
142
-
Eio_main.run run_echo_bot
306
+
Eio_main.run @@ fun env ->
307
+
exit (Cmd.eval (bot_cmd env))