Zulip bots with Eio

trim

+32 -598
+2 -70
README.md
··· 1 1 # OCaml Zulip Library 2 2 3 - A complete OCaml implementation of the Zulip REST API using the `requests` HTTP library and Eio for async operations. 3 + A complete OCaml implementation of the Zulip REST API using the `requests` HTTP 4 + library and Eio for async operations. 4 5 5 6 ## Features 6 7 ··· 78 79 let config = Config.load ~fs "echo-bot" in 79 80 Bot.run ~sw ~env ~config ~handler:echo_handler 80 81 ``` 81 - 82 - ### Atom Feed Bot 83 - 84 - The library includes a complete Atom/RSS feed bot that can: 85 - - Monitor multiple feeds 86 - - Post updates to specific Zulip channels and topics 87 - - Track which entries have been posted 88 - - Run as a scheduled daemon or interactive bot 89 - 90 - #### Scheduled Mode 91 - 92 - ```bash 93 - # Run the feed bot in scheduled mode 94 - dune exec atom_feed_bot 95 - ``` 96 - 97 - #### Interactive Mode 98 - 99 - ```bash 100 - # Run as an interactive bot that responds to commands 101 - dune exec atom_feed_bot interactive 102 - ``` 103 - 104 - Bot commands: 105 - - `!feed add <name> <url> <topic>` - Add a new feed 106 - - `!feed remove <name>` - Remove a feed 107 - - `!feed list` - List all feeds 108 - - `!feed fetch <name>` - Manually fetch a feed 109 - - `!feed help` - Show help 110 - 111 - ## Architecture 112 - 113 - ### Core Library (`zulip`) 114 - 115 - - **Auth**: Authentication and credential management 116 - - **Client**: HTTP client using the `requests` library 117 - - **Messages**: Send, edit, and retrieve messages 118 - - **Channels**: Channel/stream management 119 - - **Users**: User management 120 - - **Events**: Real-time event handling 121 - 122 - ### Bot Framework (`zulip.bot`) 123 - 124 - - **Bot**: Fiber-based bot runner 125 - - **Config**: XDG-compliant configuration management 126 - - **Message**: Bot message parsing 127 - - **Response**: Response types for bot handlers 128 - - **Storage**: Key-value state persistence via Zulip API 129 - 130 - ## Examples 131 - 132 - See the `examples/` directory for: 133 - - `test_client.ml` - Basic client functionality test 134 - - `echo_bot.ml` - Echo bot with storage commands 135 - - `atom_feed_bot.ml` - Complete Atom/RSS feed bot implementation 136 - 137 - ## Requirements 138 - 139 - - OCaml 5.0+ 140 - - Dune 3.0+ 141 - - eio 142 - - requests 143 - - jsont 144 - - uri 145 - - base64 146 - - init 147 - - logs 148 - - fmt 149 - - xdge 150 82 151 83 ## License 152 84
+10 -2
examples/atom_feed_bot.ml
··· 1 - (* Atom Feed Bot for Zulip 2 - Posts Atom/RSS feed entries to Zulip channels organized by topic *) 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Atom Feed Bot for Zulip. 7 + 8 + Posts Atom/RSS feed entries to Zulip channels organized by topic. 9 + Supports both interactive mode (responds to !feed commands) and 10 + scheduled mode (periodically fetches configured feeds). *) 3 11 4 12 (* Logging setup *) 5 13 let src = Logs.Src.create "atom_feed_bot" ~doc:"Atom feed bot for Zulip"
+10 -1
examples/atom_feed_bot.mli
··· 1 - (** Atom feed bot example *) 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Atom Feed Bot for Zulip. 7 + 8 + Posts Atom/RSS feed entries to Zulip channels. Supports both interactive 9 + mode (responds to !feed commands) and scheduled mode (periodically fetches 10 + configured feeds). *)
-20
examples/bot_config.toml
··· 1 - [weather_bot] 2 - name = "Weather Bot" 3 - default_api_key = "openweather-api-key-12345" 4 - log_level = "info" 5 - cache_duration_minutes = 30 6 - log_file = "/tmp/weather_bot.log" 7 - config_dir = "/tmp/bot_config" 8 - 9 - [logger_bot] 10 - name = "Logger Bot" 11 - log_file = "/tmp/user_messages.log" 12 - max_log_size_mb = 10 13 - archive_logs = true 14 - log_format = "json" 15 - 16 - [general] 17 - server_url = "https://company.zulipchat.com" 18 - webhook_port = 8080 19 - max_message_length = 2000 20 - rate_limit_per_minute = 60
-51
examples/bot_example.ml
··· 1 - (* Simple Bot Example using core Zulip library *) 2 - 3 - let () = 4 - Eio_main.run @@ fun env -> 5 - Eio.Switch.run @@ fun sw -> 6 - Printf.printf "OCaml Zulip Bot Example\n"; 7 - Printf.printf "=======================\n\n"; 8 - 9 - (* Create test authentication *) 10 - let auth = 11 - Zulip.Auth.create ~server_url:"https://example.zulipchat.com" 12 - ~email:"bot@example.com" ~api_key:"example-api-key" 13 - in 14 - 15 - Printf.printf "✅ Created authentication for: %s\n" (Zulip.Auth.email auth); 16 - Printf.printf "✅ Server URL: %s\n" (Zulip.Auth.server_url auth); 17 - 18 - (* Create client *) 19 - let client = Zulip.Client.create ~sw env auth in 20 - let client_str = Format.asprintf "%a" Zulip.Client.pp client in 21 - Printf.printf "✅ Created client: %s\n" client_str; 22 - 23 - (* Test message creation *) 24 - let message = 25 - Zulip.Message.create ~type_:`Channel ~to_:[ "general" ] 26 - ~content:"Hello from OCaml bot!" ~topic:"Bot Testing" () 27 - in 28 - 29 - Printf.printf "✅ Created message to: %s\n" 30 - (String.concat ", " (Zulip.Message.to_ message)); 31 - Printf.printf "✅ Message content: %s\n" (Zulip.Message.content message); 32 - Printf.printf "✅ Message topic: %s\n" 33 - (match Zulip.Message.topic message with Some t -> t | None -> "none"); 34 - 35 - (* Test API call (mock) *) 36 - (try 37 - let response = 38 - Zulip.Client.request client ~method_:`GET ~path:"/users/me" () 39 - in 40 - Printf.printf "✅ API request successful: %s\n" 41 - (match response with 42 - | Jsont.Object (fields, _) -> 43 - String.concat ", " (List.map (fun ((k, _), _) -> k) fields) 44 - | _ -> "unknown format") 45 - with Eio.Exn.Io _ as e -> 46 - Printf.printf "❌ API request failed: %s\n" (Printexc.to_string e)); 47 - 48 - Printf.printf "\n🎉 Bot example completed successfully!\n"; 49 - Printf.printf 50 - "Note: This uses mock responses since we're not connected to a real Zulip \ 51 - server.\n"
-1
examples/bot_example.mli
··· 1 - (** Example Zulip bot demonstrating the bot framework *)
-30
examples/dune
··· 1 - (executable 2 - (public_name test_client) 3 - (name test_client) 4 - (package zulip) 5 - (libraries zulip eio_main)) 6 - 7 - (executable 8 - (public_name bot_example) 9 - (name bot_example) 10 - (package zulip) 11 - (libraries zulip eio_main)) 12 - 13 1 (executable 14 2 (public_name echo_bot) 15 3 (name echo_bot) ··· 24 12 mirage-crypto-rng.unix)) 25 13 26 14 (executable 27 - (public_name test_realtime_bot) 28 - (name test_realtime_bot) 29 - (package zulip) 30 - (libraries zulip zulip.bot eio_main cmdliner logs logs.fmt)) 31 - 32 - (executable 33 15 (public_name atom_feed_bot) 34 16 (name atom_feed_bot) 35 17 (package zulip) 36 18 (libraries zulip zulip.bot eio_main cmdliner logs logs.fmt)) 37 - 38 - (executable 39 - (public_name example) 40 - (name example) 41 - (package zulip) 42 - (libraries zulip eio_main)) 43 - 44 - (executable 45 - (public_name toml_example) 46 - (name toml_example) 47 - (package zulip) 48 - (libraries zulip eio_main)) 49 19 50 20 (executable 51 21 (public_name regression_test)
+10 -1
examples/echo_bot.mli
··· 1 - (** Echo bot example *) 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Echo Bot for Zulip. 7 + 8 + A simple bot that echoes messages back to the sender, with support for 9 + storage commands (store, get, delete, list). See {!README_ECHO_BOT.md} 10 + for full documentation. *)
-42
examples/example.ml
··· 1 - open Zulip 2 - 3 - let () = 4 - Eio_main.run @@ fun env -> 5 - Eio.Switch.run @@ fun sw -> 6 - Printf.printf "OCaml Zulip Library Example\n"; 7 - Printf.printf "===========================\n\n"; 8 - 9 - (* Create authentication *) 10 - let auth = 11 - Auth.create ~server_url:"https://example.zulipchat.com" 12 - ~email:"bot@example.com" ~api_key:"your-api-key" 13 - in 14 - 15 - Printf.printf "Created auth for: %s\n" (Auth.email auth); 16 - Printf.printf "Server URL: %s\n" (Auth.server_url auth); 17 - 18 - (* Create a message *) 19 - let message = 20 - Message.create ~type_:`Channel ~to_:[ "general" ] 21 - ~content:"Hello from OCaml Zulip library!" ~topic:"Test" () 22 - in 23 - 24 - Printf.printf "\nCreated message:\n"; 25 - Printf.printf "- Type: %s\n" (Message_type.to_string (Message.type_ message)); 26 - Printf.printf "- To: %s\n" (String.concat ", " (Message.to_ message)); 27 - Printf.printf "- Content: %s\n" (Message.content message); 28 - Printf.printf "- Topic: %s\n" 29 - (match Message.topic message with Some t -> t | None -> "None"); 30 - 31 - (* Create client *) 32 - let client = Client.create ~sw env auth in 33 - Printf.printf "\nCreated client\n"; 34 - 35 - (* Test basic client request *) 36 - (try 37 - let _ = Client.request client ~method_:`GET ~path:"/test" () in 38 - Printf.printf "Mock request succeeded\n" 39 - with Eio.Exn.Io _ as e -> 40 - Printf.printf "Mock request failed: %s\n" (Printexc.to_string e)); 41 - 42 - Printf.printf "\nLibrary is working correctly!\n"
-1
examples/example.mli
··· 1 - (** Basic Zulip library usage example *)
-16
examples/example_bot_config.toml
··· 1 - # Bot configuration file 2 - name = "Weather Bot" 3 - description = "A bot that provides weather information" 4 - 5 - [bot] 6 - # Bot-specific settings 7 - api_key = "your-weather-api-key" 8 - default_location = "San Francisco" 9 - units = "metric" 10 - max_forecasts = 5 11 - 12 - [features] 13 - # Feature flags 14 - cache_enabled = true 15 - verbose_logging = false 16 - rate_limit = 60
-73
examples/test_client.ml
··· 1 - (* Simple test for Zulip client with requests library *) 2 - 3 - let test_auth () = 4 - Printf.printf "Testing Zulip authentication...\n"; 5 - try 6 - let auth = Zulip.Auth.from_zuliprc ~path:"~/.zuliprc" () in 7 - Printf.printf "Successfully loaded auth:\n"; 8 - Printf.printf " Server: %s\n" (Zulip.Auth.server_url auth); 9 - Printf.printf " Email: %s\n" (Zulip.Auth.email auth); 10 - auth 11 - with Eio.Exn.Io _ as e -> 12 - Printf.eprintf "Failed to load auth: %s\n" (Printexc.to_string e); 13 - (* Create a test auth *) 14 - Zulip.Auth.create ~server_url:"https://example.zulipchat.com" 15 - ~email:"bot@example.com" ~api_key:"test_api_key" 16 - 17 - let test_message_send env auth = 18 - Printf.printf "\nTesting message send...\n"; 19 - 20 - Eio.Switch.run @@ fun sw -> 21 - let client = Zulip.Client.create ~sw env auth in 22 - 23 - (* Create a test message *) 24 - let message = 25 - Zulip.Message.create ~type_:`Channel ~to_:[ "general" ] ~topic:"Test Topic" 26 - ~content:"Hello from OCaml Zulip client using requests library!" () 27 - in 28 - 29 - try 30 - let response = Zulip.Messages.send client message in 31 - Printf.printf "Message sent successfully!\n"; 32 - Printf.printf "Message ID: %d\n" (Zulip.Message_response.id response) 33 - with Eio.Exn.Io _ as e -> 34 - Printf.eprintf "Failed to send message: %s\n" (Printexc.to_string e) 35 - 36 - let test_fetch_messages env auth = 37 - Printf.printf "\nTesting message fetch...\n"; 38 - 39 - Eio.Switch.run @@ fun sw -> 40 - let client = Zulip.Client.create ~sw env auth in 41 - 42 - try 43 - let json = 44 - Zulip.Messages.get_messages client ~anchor:Newest ~num_before:5 45 - ~num_after:0 () 46 - in 47 - Printf.printf "Fetched messages successfully!\n"; 48 - match json with 49 - | Jsont.Object (fields, _) -> ( 50 - let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in 51 - match List.assoc_opt "messages" assoc with 52 - | Some (Jsont.Array (messages, _)) -> 53 - Printf.printf "Got %d messages\n" (List.length messages) 54 - | _ -> Printf.printf "No messages field found\n") 55 - | _ -> Printf.printf "Unexpected JSON format\n" 56 - with Eio.Exn.Io _ as e -> 57 - Printf.eprintf "Failed to fetch messages: %s\n" (Printexc.to_string e) 58 - 59 - let () = 60 - Printf.printf "Zulip OCaml Client Test\n"; 61 - Printf.printf "========================\n\n"; 62 - 63 - Eio_main.run @@ fun env -> 64 - (* Test authentication *) 65 - let auth = test_auth () in 66 - 67 - (* Test sending a message *) 68 - test_message_send env auth; 69 - 70 - (* Test fetching messages *) 71 - test_fetch_messages env auth; 72 - 73 - Printf.printf "\nAll tests completed!\n"
-1
examples/test_client.mli
··· 1 - (** Test client example *)
-101
examples/test_realtime_bot.ml
··· 1 - (* Test script to verify real-time bot event processing *) 2 - 3 - open Zulip_bot 4 - 5 - (* Logging setup *) 6 - let src = Logs.Src.create "test_realtime_bot" ~doc:"Test real-time bot" 7 - 8 - module Log = (val Logs.src_log src : Logs.LOG) 9 - 10 - (* Simple test bot handler that logs everything *) 11 - let test_handler ~storage ~identity:_ message = 12 - Log.info (fun m -> m "Received message"); 13 - 14 - let content = Message.content message in 15 - let sender = Message.sender_email message in 16 - let is_direct = Message.is_private message in 17 - 18 - Log.info (fun m -> m "Content: %s" content); 19 - Log.info (fun m -> m "Sender: %s" sender); 20 - Log.info (fun m -> m "Direct: %b" is_direct); 21 - 22 - (* Test storage *) 23 - (try 24 - Storage.set storage "last_message" content; 25 - Log.info (fun m -> m "Stored message in bot storage") 26 - with Eio.Exn.Io _ as e -> 27 - Log.err (fun m -> m "Storage error: %s" (Printexc.to_string e))); 28 - 29 - (* Always reply with confirmation *) 30 - Response.reply (Printf.sprintf "Test bot received: %s" content) 31 - 32 - let run_test verbosity env = 33 - (* Setup logging *) 34 - Logs.set_reporter (Logs_fmt.reporter ()); 35 - Logs.set_level 36 - (Some 37 - (match verbosity with 38 - | 0 -> Logs.Info 39 - | 1 -> Logs.Debug 40 - | _ -> Logs.Debug)); 41 - 42 - Log.info (fun m -> m "Real-time Bot Test"); 43 - Log.info (fun m -> m "=================="); 44 - 45 - (* Load auth from .zuliprc *) 46 - let auth = 47 - try 48 - let a = Zulip.Auth.from_zuliprc () in 49 - Log.info (fun m -> m "Loaded auth for: %s" (Zulip.Auth.email a)); 50 - Log.info (fun m -> m "Server: %s" (Zulip.Auth.server_url a)); 51 - a 52 - with Eio.Exn.Io _ as e -> 53 - Log.err (fun m -> m "Failed to load .zuliprc: %s" (Printexc.to_string e)); 54 - exit 1 55 - in 56 - 57 - Eio.Switch.run @@ fun sw -> 58 - (* Create configuration from auth *) 59 - let config = 60 - Config.create ~name:"test-bot" 61 - ~site:(Zulip.Auth.server_url auth) 62 - ~email:(Zulip.Auth.email auth) ~api_key:(Zulip.Auth.api_key auth) 63 - ~description:"A test bot that logs all messages received" () 64 - in 65 - 66 - Log.info (fun m -> m "Starting bot in real-time mode..."); 67 - Log.info (fun m -> m "The bot will:"); 68 - Log.info (fun m -> m "- Register for message events"); 69 - Log.info (fun m -> m "- Poll for new messages"); 70 - Log.info (fun m -> m "- Process and reply to messages"); 71 - Log.info (fun m -> m "- Store messages in Zulip bot storage"); 72 - Log.info (fun m -> m "Press Ctrl+C to stop."); 73 - 74 - (* Run the bot - now just a simple function call *) 75 - Bot.run ~sw ~env ~config ~handler:test_handler 76 - 77 - (* Command-line interface *) 78 - open Cmdliner 79 - 80 - let verbosity = 81 - let doc = "Increase verbosity (can be used multiple times)" in 82 - let verbosity_flags = Arg.(value & flag_all & info [ "v"; "verbose" ] ~doc) in 83 - Term.(const List.length $ verbosity_flags) 84 - 85 - let main_cmd = 86 - let doc = "Test real-time bot for Zulip" in 87 - let man = 88 - [ 89 - `S Manpage.s_description; 90 - `P 91 - "This bot tests real-time event processing with the Zulip API. It will \ 92 - echo received messages and store them in bot storage."; 93 - `P "The bot requires a configured ~/.zuliprc file with API credentials."; 94 - ] 95 - in 96 - let info = Cmd.info "test_realtime_bot" ~version:"2.0.0" ~doc ~man in 97 - let run verbosity = Eio_main.run (run_test verbosity) in 98 - let term = Term.(const run $ verbosity) in 99 - Cmd.v info term 100 - 101 - let () = exit (Cmd.eval main_cmd)
-1
examples/test_realtime_bot.mli
··· 1 - (* Test realtime bot example *)
-118
examples/toml_example.ml
··· 1 - open Zulip 2 - 3 - let () = 4 - Printf.printf "OCaml Zulip TOML Support Demo\n"; 5 - Printf.printf "=============================\n\n"; 6 - 7 - (* Example 1: Create a sample zuliprc TOML file *) 8 - let zuliprc_content = 9 - {| 10 - # Zulip API Configuration 11 - [api] 12 - email = "demo@example.com" 13 - key = "demo-api-key-12345" 14 - site = "https://demo.zulipchat.com" 15 - 16 - # Optional settings 17 - insecure = false 18 - cert_bundle = "/etc/ssl/certs/ca-certificates.crt" 19 - |} 20 - in 21 - 22 - let zuliprc_file = "demo_zuliprc.toml" in 23 - let oc = open_out zuliprc_file in 24 - output_string oc zuliprc_content; 25 - close_out oc; 26 - 27 - Printf.printf "Created sample zuliprc.toml file:\n%s\n" zuliprc_content; 28 - 29 - (* Test loading auth from TOML *) 30 - (try 31 - let auth = Auth.from_zuliprc ~path:zuliprc_file () in 32 - Printf.printf "✅ Successfully loaded authentication from TOML:\n"; 33 - Printf.printf " Email: %s\n" (Auth.email auth); 34 - Printf.printf " Server: %s\n" (Auth.server_url auth); 35 - Printf.printf " Auth Header: %s\n" (Auth.to_basic_auth_header auth); 36 - 37 - (* Test creating client *) 38 - Eio_main.run @@ fun env -> 39 - Eio.Switch.run @@ fun sw -> 40 - let client = Client.create ~sw env auth in 41 - Printf.printf "✅ Created client successfully\n\n"; 42 - 43 - (* Test basic functionality *) 44 - try 45 - let _ = Client.request client ~method_:`GET ~path:"/users/me" () in 46 - Printf.printf "✅ Mock API request succeeded\n" 47 - with Eio.Exn.Io _ as e -> 48 - Printf.printf "❌ API request failed: %s\n" (Printexc.to_string e) 49 - with Eio.Exn.Io _ as e -> 50 - Printf.printf "❌ Failed to load auth from TOML: %s\n" 51 - (Printexc.to_string e)); 52 - 53 - (* Example 2: Root-level TOML configuration *) 54 - let root_toml_content = 55 - {| 56 - email = "root-user@example.com" 57 - key = "root-api-key-67890" 58 - site = "https://root.zulipchat.com" 59 - |} 60 - in 61 - 62 - let root_file = "demo_root.toml" in 63 - let oc = open_out root_file in 64 - output_string oc root_toml_content; 65 - close_out oc; 66 - 67 - Printf.printf "\nTesting root-level TOML configuration:\n"; 68 - (try 69 - let auth = Auth.from_zuliprc ~path:root_file () in 70 - Printf.printf "✅ Root-level TOML parsed successfully:\n"; 71 - Printf.printf " Email: %s\n" (Auth.email auth); 72 - Printf.printf " Server: %s\n" (Auth.server_url auth) 73 - with Eio.Exn.Io _ as e -> 74 - Printf.printf "❌ Failed to parse root-level TOML: %s\n" 75 - (Printexc.to_string e)); 76 - 77 - (* Example 3: Test error handling with invalid TOML *) 78 - let invalid_toml = 79 - {| 80 - [api 81 - email = "invalid@example.com" # Missing closing bracket 82 - |} 83 - in 84 - 85 - let invalid_file = "demo_invalid.toml" in 86 - let oc = open_out invalid_file in 87 - output_string oc invalid_toml; 88 - close_out oc; 89 - 90 - Printf.printf "\nTesting error handling with invalid TOML:\n"; 91 - (try 92 - let _ = Auth.from_zuliprc ~path:invalid_file () in 93 - Printf.printf "❌ Should have failed with invalid TOML\n" 94 - with Eio.Exn.Io _ as e -> 95 - Printf.printf "✅ Correctly handled invalid TOML: %s\n" 96 - (Printexc.to_string e)); 97 - 98 - (* Example 4: Test missing file handling *) 99 - Printf.printf "\nTesting missing file handling:\n"; 100 - (try 101 - let _ = Auth.from_zuliprc ~path:"nonexistent.toml" () in 102 - Printf.printf "❌ Should have failed with missing file\n" 103 - with Eio.Exn.Io _ as e -> 104 - Printf.printf "✅ Correctly handled missing file: %s\n" 105 - (Printexc.to_string e)); 106 - 107 - (* Clean up *) 108 - List.iter 109 - (fun file -> if Sys.file_exists file then Sys.remove file) 110 - [ zuliprc_file; root_file; invalid_file ]; 111 - 112 - Printf.printf "\n🎉 TOML support demonstration complete!\n"; 113 - Printf.printf "\nFeatures demonstrated:\n"; 114 - Printf.printf "• Parse TOML files with [api] section\n"; 115 - Printf.printf "• Parse TOML files with root-level configuration\n"; 116 - Printf.printf "• Proper error handling for invalid TOML syntax\n"; 117 - Printf.printf "• Proper error handling for missing files\n"; 118 - Printf.printf "• Integration with existing Zulip client\n"
-1
examples/toml_example.mli
··· 1 - (** TOML support demonstration for Zulip configuration files *)
-68
test_bot.sh
··· 1 - #!/bin/bash 2 - 3 - # Simple test script for Zulip bots 4 - # Ensure you have a .zuliprc file configured first 5 - 6 - echo "Zulip Bot Test Script" 7 - echo "=====================" 8 - echo "" 9 - 10 - if [ ! -f "$HOME/.zuliprc" ]; then 11 - echo "Error: ~/.zuliprc not found" 12 - echo "" 13 - echo "Create ~/.zuliprc with the following format:" 14 - echo "[api]" 15 - echo "email = bot@example.com" 16 - echo "key = your-api-key-here" 17 - echo "site = https://your-subdomain.zulipchat.com" 18 - exit 1 19 - fi 20 - 21 - echo "Found .zuliprc configuration" 22 - echo "" 23 - 24 - PS3="Select a bot to test: " 25 - options=("Echo Bot" "Echo Bot (verbose)" "Test Realtime Bot" "Atom Feed Bot (interactive)" "Quit") 26 - 27 - select opt in "${options[@]}" 28 - do 29 - case $opt in 30 - "Echo Bot") 31 - echo "Starting Echo Bot..." 32 - echo "The bot will echo all messages it receives" 33 - echo "Press Ctrl+C to stop" 34 - echo "" 35 - dune exec zulip_bot/echo_bot 36 - break 37 - ;; 38 - "Echo Bot (verbose)") 39 - echo "Starting Echo Bot with verbose logging..." 40 - echo "The bot will echo all messages with detailed logs" 41 - echo "Press Ctrl+C to stop" 42 - echo "" 43 - dune exec zulip_bot/echo_bot -- -vv 44 - break 45 - ;; 46 - "Test Realtime Bot") 47 - echo "Starting Test Realtime Bot..." 48 - echo "This bot logs all received messages and tests storage" 49 - echo "Press Ctrl+C to stop" 50 - echo "" 51 - dune exec zulip_bot/test_realtime_bot 52 - break 53 - ;; 54 - "Atom Feed Bot (interactive)") 55 - echo "Starting Atom Feed Bot in interactive mode..." 56 - echo "Use !feed help to see available commands" 57 - echo "Press Ctrl+C to stop" 58 - echo "" 59 - dune exec zulip_bot/atom_feed_bot -- interactive 60 - break 61 - ;; 62 - "Quit") 63 - echo "Exiting..." 64 - break 65 - ;; 66 - *) echo "Invalid option $REPLY";; 67 - esac 68 - done