this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

sync

+162 -356
+154 -1
ocaml-zulip/CLAUDE.md
··· 4 4 5 5 My target is to use the OCaml EIO direct-style library, with an idiomatic as 6 6 possible API that implements it. For JSON parsing, using the jsonm library is 7 - right. For HTTPS, use cohttp-eio with the tls-eio library. 7 + right. For HTTPS, use cohttp-eio with the tls-eio library. You have access to 8 + an OCaml LSP via MCP which provides type hints and other language server 9 + features after you complete a `dune build`. 8 10 9 11 # OCaml Zulip Library Design 10 12 ··· 518 520 - `jsonm` - Streaming JSON codec 519 521 - `uri` - URI parsing and manipulation 520 522 - `base64` - Base64 encoding for authentication 523 + 524 + # Architecture Analysis: zulip_bot vs zulip_botserver 525 + 526 + ## Library Separation 527 + 528 + ### `zulip_bot` - Individual Bot Framework 529 + **Purpose**: Library for building and running a single bot instance 530 + 531 + **Key Components**: 532 + - `Bot_handler` - Interface for bot logic with EIO environment access 533 + - `Bot_runner` - Manages lifecycle of one bot (real-time events or webhook mode) 534 + - `Bot_config` - Configuration for a single bot 535 + - `Bot_storage` - Simple in-memory storage for bot state 536 + 537 + **Usage Pattern**: 538 + ```ocaml 539 + (* Run a single bot directly *) 540 + let my_bot = Bot_handler.create (module My_echo_bot) ~config ~storage ~identity in 541 + let runner = Bot_runner.create ~client ~handler:my_bot in 542 + Bot_runner.run_realtime runner (* Bot connects to Zulip events API directly *) 543 + ``` 544 + 545 + ### `zulip_botserver` - Multi-Bot Server Infrastructure 546 + **Purpose**: HTTP server that manages multiple bots via webhooks 547 + 548 + **Key Components**: 549 + - `Bot_server` - HTTP server receiving webhook events from Zulip 550 + - `Bot_registry` - Manages multiple bot instances 551 + - `Server_config` - Configuration for multiple bots + server settings 552 + - `Webhook_handler` - Parses incoming webhook requests and routes to appropriate bots 553 + 554 + **Usage Pattern**: 555 + ```ocaml 556 + (* Run a server hosting multiple bots *) 557 + let registry = Bot_registry.create () in 558 + Bot_registry.register registry echo_bot_module; 559 + Bot_registry.register registry weather_bot_module; 560 + 561 + let server = Bot_server.create ~env ~config ~registry in 562 + Bot_server.run server (* HTTP server waits for webhook calls *) 563 + ``` 564 + 565 + ## EIO Environment Requirements 566 + 567 + ### Why Bot Handlers Need Direct EIO Access 568 + 569 + Bot handlers require direct access to the EIO environment for legitimate I/O operations beyond HTTP requests to Zulip: 570 + 571 + 1. **Network Operations**: Custom HTTP requests, API calls to external services 572 + 2. **File System Operations**: Reading configuration files, CSV dictionaries, logs 573 + 3. **Resource Management**: Proper cleanup via structured concurrency 574 + 575 + ### Example: URL Checker Bot 576 + ```ocaml 577 + module Url_checker_bot : Zulip_bot.Bot_handler.Bot_handler = struct 578 + let handle_message ~config ~storage ~identity ~message ~env = 579 + match parse_command message with 580 + | "!check", url -> 581 + (* Direct EIO network access needed *) 582 + Eio.Switch.run @@ fun sw -> 583 + let client = Cohttp_eio.Client.make ~sw env#net in 584 + let response = Cohttp_eio.Client.head ~sw client (Uri.of_string url) in 585 + let status = Cohttp.Code.code_of_status response.status in 586 + Ok (Response.reply ~content:(format_status_message url status)) 587 + | _ -> Ok Response.none 588 + end 589 + ``` 590 + 591 + ### Example: CSV Dictionary Bot 592 + ```ocaml 593 + module Csv_dict_bot : Zulip_bot.Bot_handler.Bot_handler = struct 594 + let handle_message ~config ~storage ~identity ~message ~env = 595 + match parse_command message with 596 + | "!lookup", term -> 597 + (* Direct EIO file system access needed *) 598 + let csv_path = Bot_config.get_required config ~key:"csv_file" in 599 + let content = Eio.Path.load env#fs (Eio.Path.parse csv_path) in 600 + let matches = search_csv_content content term in 601 + Ok (Response.reply ~content:(format_matches matches)) 602 + | _ -> Ok Response.none 603 + end 604 + ``` 605 + 606 + ## Refined Bot Handler Interface 607 + 608 + Based on analysis, the current EIO environment plumbing is **essential** and should be cleaned up: 609 + 610 + ```ocaml 611 + (** Clean bot handler interface with direct EIO access *) 612 + module type Bot_handler = sig 613 + val initialize : Bot_config.t -> (unit, Zulip.Error.t) result 614 + val usage : unit -> string 615 + val description : unit -> string 616 + 617 + (** Handle message with full EIO environment access *) 618 + val handle_message : 619 + config:Bot_config.t -> 620 + storage:Bot_storage.t -> 621 + identity:Identity.t -> 622 + message:Message_context.t -> 623 + env:#Eio.Env.t -> (* Essential for custom I/O *) 624 + (Response.t, Zulip.Error.t) result 625 + end 626 + 627 + type t 628 + 629 + (** Single creation interface *) 630 + val create : 631 + (module Bot_handler) -> 632 + config:Bot_config.t -> 633 + storage:Bot_storage.t -> 634 + identity:Identity.t -> 635 + t 636 + 637 + (** Single message handler requiring EIO environment *) 638 + val handle_message : t -> #Eio.Env.t -> Message_context.t -> (Response.t, Zulip.Error.t) result 639 + ``` 640 + 641 + ## Storage Strategy 642 + 643 + Bot storage can be simplified to in-memory key-value storage since it's server-side: 644 + 645 + ```ocaml 646 + (* In zulip_bot - storage per bot instance *) 647 + module Bot_storage = struct 648 + type t = (string, string) Hashtbl.t (* Simple in-memory key-value *) 649 + 650 + let create () = Hashtbl.create 16 651 + let get t ~key = Hashtbl.find_opt t key 652 + let put t ~key ~value = Hashtbl.replace t key value 653 + let contains t ~key = Hashtbl.mem t key 654 + end 655 + 656 + (* In zulip_botserver - storage shared across bots *) 657 + module Server_storage = struct 658 + type t = (string * string, string) Hashtbl.t (* (bot_email, key) -> value *) 659 + 660 + let create () = Hashtbl.create 64 661 + let get t ~bot_email ~key = Hashtbl.find_opt t (bot_email, key) 662 + let put t ~bot_email ~key ~value = Hashtbl.replace t (bot_email, key) value 663 + end 664 + ``` 665 + 666 + ## Interface Cleanup Recommendations 667 + 668 + 1. **Remove** the problematic `handle_message` function with mock environment 669 + 2. **Keep** `handle_message_with_env` but rename to `handle_message` 670 + 3. **Use** `#Eio.Env.t` constraint for clean typing 671 + 4. **Document** that bot handlers have full EIO access for custom I/O operations 672 + 673 + This design maintains flexibility for real-world bot functionality while providing clean, type-safe interfaces. 521 674 522 675 ## Sources and References 523 676
+5 -5
ocaml-zulip/examples/dune
··· 16 16 (package zulip_bot) 17 17 (libraries zulip zulip_bot)) 18 18 19 - (executable 20 - (public_name zulip_realistic_bot_example) 21 - (name realistic_bot_example) 22 - (package zulip_bot) 23 - (libraries zulip zulip_bot unix eio eio_main toml)) 19 + ; (executable 20 + ; (public_name zulip_realistic_bot_example) 21 + ; (name realistic_bot_example) 22 + ; (package zulip_bot) 23 + ; (libraries zulip zulip_bot unix eio eio_main toml))
-346
ocaml-zulip/examples/realistic_bot_example.ml
··· 1 - (* Realistic Bot Example with Real EIO Operations *) 2 - 3 - (* Weather Bot that: 4 - 1. Reads TOML configuration from filesystem using real EIO 5 - 2. Makes external HTTP API calls for weather data 6 - 3. Logs bot activity to files using real EIO filesystem access 7 - 4. Demonstrates proper EIO usage in bot handlers 8 - *) 9 - 10 - let load_bot_config env = 11 - (* Read TOML config file using real EIO filesystem *) 12 - match Eio.Path.with_open_in (env#fs / "examples" / "bot_config.toml") (fun flow -> 13 - let content = Eio.Flow.read_all flow in 14 - Toml.Parser.from_string content 15 - ) with 16 - | exception Eio.Io (Eio.Fs.E Not_found, _) -> 17 - Printf.printf "[CONFIG] Config file not found, using defaults\n"; 18 - Ok (Toml.table []) 19 - | Ok toml -> Ok toml 20 - | Error (`Msg msg) -> 21 - Error (Printf.sprintf "TOML parse error: %s" msg) 22 - 23 - let log_to_file env log_file message = 24 - (* Write to log file using real EIO filesystem *) 25 - try 26 - let timestamp = Unix.time () |> Unix.gmtime |> fun tm -> 27 - Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d" 28 - (tm.tm_year + 1900) (tm.tm_mon + 1) tm.tm_mday 29 - tm.tm_hour tm.tm_min tm.tm_sec in 30 - let log_entry = Printf.sprintf "[%s] %s\n" timestamp message in 31 - 32 - Eio.Path.with_open_out ~create:(`If_missing 0o644) ~append:true 33 - (env#fs / log_file) (fun flow -> 34 - Eio.Flow.write_string flow log_entry 35 - ); 36 - Printf.printf "[FS] Logged to %s: %s" log_file (String.trim log_entry); 37 - Ok () 38 - with 39 - | Eio.Io (err, _) -> 40 - Error (Printf.sprintf "Log write failed: %s" (Printexc.to_string err)) 41 - 42 - let make_http_request env url = 43 - (* Make real HTTP request using EIO network *) 44 - try 45 - Printf.printf "[NET] Making HTTP request to %s\n" url; 46 - (* In a real implementation, you would use cohttp-eio here *) 47 - (* For now, we'll simulate the network call with a delay *) 48 - Eio.Time.sleep env#clock 0.1; (* Simulate network latency *) 49 - 50 - (* Simulate successful API response *) 51 - let response_body = `O [ 52 - ("status", `String "success"); 53 - ("temperature", `Float 22.5); 54 - ("condition", `String "Sunny"); 55 - ("humidity", `Float 65.0); 56 - ] in 57 - Ok response_body 58 - with 59 - | Eio.Io (err, _) -> 60 - Error (Printf.sprintf "HTTP request failed: %s" (Printexc.to_string err)) 61 - 62 - let read_toml_value toml section key = 63 - try 64 - match Toml.Lenses.(get toml (key section |-- table |-- key key |-- string)) with 65 - | Some value -> Some value 66 - | None -> None 67 - with _ -> None 68 - 69 - (* Weather Bot Implementation with Real EIO *) 70 - module Weather_bot = struct 71 - let initialize _config = 72 - Printf.printf "Weather bot initialized with real EIO support!\n"; 73 - Ok () 74 - 75 - let usage () = 76 - "Weather bot - get weather for cities. Usage: @bot weather <city>" 77 - 78 - let description () = 79 - "A weather bot that demonstrates real EIO filesystem and network access" 80 - 81 - let handle_message ~config:_ ~storage ~identity:_ ~message ~env = 82 - let content = Zulip_bot.Bot_handler.Message_context.content message in 83 - let sender = Zulip_bot.Bot_handler.Message_context.sender_full_name message in 84 - 85 - (* Log the incoming message using real EIO *) 86 - (match log_to_file env "tmp/weather_bot.log" 87 - (Printf.sprintf "Received message from %s: %s" sender content) with 88 - | Ok () -> () 89 - | Error err -> Printf.printf "[ERROR] %s\n" err); 90 - 91 - (* Parse command *) 92 - if String.length content > 8 && String.sub content 0 8 = "weather " then ( 93 - let city = String.sub content 8 (String.length content - 8) in 94 - 95 - (* Read bot configuration from filesystem using real EIO *) 96 - (match load_bot_config env with 97 - | Ok toml -> 98 - let api_key = match read_toml_value toml "weather_bot" "default_api_key" with 99 - | Some key -> key 100 - | None -> "demo-key" in 101 - let log_level = match read_toml_value toml "weather_bot" "log_level" with 102 - | Some level -> level 103 - | None -> "info" in 104 - 105 - Printf.printf "[CONFIG] Using API key: %s, Log level: %s\n" api_key log_level; 106 - 107 - (* Store request in bot storage *) 108 - (match Zulip_bot.Bot_storage.put storage ~key:("last_request_" ^ sender) ~value:city with 109 - | Ok () -> 110 - (* Make external API call using real EIO network *) 111 - let weather_url = Printf.sprintf "https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s" city api_key in 112 - (match make_http_request env weather_url with 113 - | Ok weather_json -> 114 - (* Parse weather response *) 115 - let weather_info = match weather_json with 116 - | `O fields -> 117 - let temp = List.assoc_opt "temperature" fields 118 - |> Option.value ~default:(`Float 0.0) in 119 - let condition = List.assoc_opt "condition" fields 120 - |> Option.value ~default:(`String "Unknown") in 121 - (match temp, condition with 122 - | `Float t, `String c -> 123 - Printf.sprintf "Weather in %s: %.1f°C, %s" city t c 124 - | _ -> Printf.sprintf "Weather in %s: Data unavailable" city) 125 - | _ -> Printf.sprintf "Weather in %s: Invalid response format" city in 126 - 127 - (* Log successful API call *) 128 - (match log_to_file env "tmp/weather_bot.log" 129 - (Printf.sprintf "Successfully retrieved weather for %s" city) with 130 - | Ok () -> () 131 - | Error err -> Printf.printf "[ERROR] %s\n" err); 132 - 133 - Ok (Zulip_bot.Bot_handler.Response.reply ~content:weather_info) 134 - | Error msg -> 135 - let error_msg = Printf.sprintf "Weather API error: %s" msg in 136 - (match log_to_file env "tmp/weather_bot.log" error_msg with 137 - | Ok () -> () 138 - | Error err -> Printf.printf "[ERROR] %s\n" err); 139 - Ok (Zulip_bot.Bot_handler.Response.reply ~content:("Weather service unavailable: " ^ msg))) 140 - | Error err -> 141 - Error err) 142 - | Error msg -> 143 - let error_msg = Printf.sprintf "Config read error: %s" msg in 144 - (match log_to_file env "tmp/weather_bot.log" error_msg with 145 - | Ok () -> () 146 - | Error err -> Printf.printf "[ERROR] %s\n" err); 147 - Ok (Zulip_bot.Bot_handler.Response.reply ~content:"Bot configuration error")) 148 - ) else if content = "help" then ( 149 - Ok (Zulip_bot.Bot_handler.Response.reply ~content:(usage ())) 150 - ) else ( 151 - let error_msg = Printf.sprintf "Unknown command: %s" content in 152 - (match log_to_file env "tmp/weather_bot.log" error_msg with 153 - | Ok () -> () 154 - | Error err -> Printf.printf "[ERROR] %s\n" err); 155 - Ok (Zulip_bot.Bot_handler.Response.reply ~content:"Unknown command. Type 'help' for usage.") 156 - ) 157 - end 158 - 159 - (* File Logger Bot Implementation with Real EIO *) 160 - module Logger_bot = struct 161 - let initialize _config = 162 - Printf.printf "Logger bot initialized with real EIO support!\n"; 163 - Ok () 164 - 165 - let usage () = 166 - "Logger bot - logs all messages to files. Usage: @bot log <message>" 167 - 168 - let description () = 169 - "A logging bot that demonstrates real EIO filesystem access" 170 - 171 - let handle_message ~config:_ ~storage:_ ~identity:_ ~message ~env = 172 - let content = Zulip_bot.Bot_handler.Message_context.content message in 173 - let sender = Zulip_bot.Bot_handler.Message_context.sender_full_name message in 174 - let message_id = Zulip_bot.Bot_handler.Message_context.message_id message in 175 - 176 - if String.length content > 4 && String.sub content 0 4 = "log " then ( 177 - let log_content = String.sub content 4 (String.length content - 4) in 178 - 179 - (* Read logging configuration from TOML *) 180 - let (log_file, max_size) = match load_bot_config env with 181 - | Ok toml -> 182 - let file = read_toml_value toml "logger_bot" "log_file" 183 - |> Option.value ~default:"tmp/user_messages.log" in 184 - let size = read_toml_value toml "logger_bot" "max_log_size_mb" 185 - |> Option.map int_of_string |> Option.value ~default:10 in 186 - (file, size) 187 - | Error _ -> ("tmp/user_messages.log", 10) in 188 - 189 - let log_entry = Printf.sprintf "Message %d from %s: %s" 190 - message_id sender log_content in 191 - 192 - (* Write to log file using real EIO filesystem *) 193 - (match log_to_file env log_file log_entry with 194 - | Ok () -> 195 - Printf.printf "[FS] Successfully wrote to %s\n" log_file; 196 - Ok (Zulip_bot.Bot_handler.Response.reply ~content:"Message logged successfully!") 197 - | Error err -> 198 - Printf.printf "[ERROR] Failed to write log: %s\n" err; 199 - Ok (Zulip_bot.Bot_handler.Response.reply ~content:("Logging failed: " ^ err))) 200 - ) else ( 201 - Ok (Zulip_bot.Bot_handler.Response.reply ~content:"Usage: @bot log <your message>") 202 - ) 203 - end 204 - 205 - let create_temp_dirs env = 206 - (* Create temporary directories for logging using real EIO *) 207 - try 208 - let tmp_dir = env#fs / "tmp" in 209 - Eio.Path.mkdir ~perm:0o755 tmp_dir; 210 - Printf.printf "[FS] Created tmp/ directory for logging\n"; 211 - Ok () 212 - with 213 - | Eio.Io (Eio.Fs.E Already_exists, _) -> 214 - Printf.printf "[FS] tmp/ directory already exists\n"; 215 - Ok () 216 - | Eio.Io (err, _) -> 217 - Error (Printf.sprintf "Failed to create directories: %s" (Printexc.to_string err)) 218 - 219 - let () = 220 - Printf.printf "Realistic OCaml Zulip Bot with Real EIO Operations\n"; 221 - Printf.printf "================================================\n\n"; 222 - 223 - (* Run with real EIO environment *) 224 - Eio_main.run @@ fun env -> 225 - 226 - (* Create necessary directories *) 227 - (match create_temp_dirs env with 228 - | Ok () -> () 229 - | Error err -> Printf.printf "[ERROR] %s\n" err); 230 - 231 - (* Create test authentication *) 232 - let auth = Zulip.Auth.create 233 - ~server_url:"https://company.zulipchat.com" 234 - ~email:"weather-bot@company.com" 235 - ~api_key:"real-api-key-here" in 236 - 237 - Printf.printf "✅ Created authentication for: %s\n" (Zulip.Auth.email auth); 238 - 239 - (* Create client with real EIO environment *) 240 - let client = Zulip.Client.create env auth in 241 - Printf.printf "✅ Created EIO-capable client\n"; 242 - 243 - (* Load TOML configuration *) 244 - (match load_bot_config env with 245 - | Ok toml -> 246 - Printf.printf "✅ Loaded TOML configuration\n"; 247 - let server_url = read_toml_value toml "general" "server_url" 248 - |> Option.value ~default:"https://default.zulipchat.com" in 249 - Printf.printf "[CONFIG] Server URL: %s\n" server_url 250 - | Error err -> 251 - Printf.printf "⚠️ Config load failed: %s\n" err); 252 - 253 - (* Create bot config *) 254 - let config = Zulip_bot.Bot_config.create [ 255 - ("weather_api_key", "api-key-12345"); 256 - ("log_level", "info"); 257 - ("data_dir", "/tmp/bot"); 258 - ] in 259 - 260 - (* Create bot storage *) 261 - let storage = Zulip_bot.Bot_storage.create client ~bot_email:"weather-bot@company.com" in 262 - 263 - (* Create bot identity *) 264 - let identity = Zulip_bot.Bot_handler.Identity.create 265 - ~full_name:"Weather Bot" 266 - ~email:"weather-bot@company.com" 267 - ~mention_name:"WeatherBot" in 268 - 269 - (* Create Weather Bot handler *) 270 - let weather_handler = Zulip_bot.Bot_handler.create 271 - (module Weather_bot) ~config ~storage ~identity in 272 - 273 - (* Create Logger Bot handler *) 274 - let logger_handler = Zulip_bot.Bot_handler.create 275 - (module Logger_bot) ~config ~storage ~identity in 276 - 277 - Printf.printf "✅ Created bot handlers with real EIO support\n"; 278 - 279 - (* Test Weather Bot with real EIO operations *) 280 - Printf.printf "\n=== Testing Weather Bot with Real EIO ===\n"; 281 - 282 - let test_weather_message = Zulip_bot.Bot_handler.Message_context.create 283 - ~message_id:1001 284 - ~sender_email:"user@company.com" 285 - ~sender_full_name:"Alice Smith" 286 - ~content:"weather London" 287 - ~message_type:`Direct 288 - () in 289 - 290 - (match Zulip_bot.Bot_handler.handle_message_with_env weather_handler env test_weather_message with 291 - | Ok response -> 292 - Printf.printf "✅ Weather bot response: %s\n" 293 - (match response with 294 - | Zulip_bot.Bot_handler.Response.Reply content -> content 295 - | _ -> "Other response type") 296 - | Error err -> 297 - Printf.printf "❌ Weather bot error: %s\n" (Zulip.Error.message err)); 298 - 299 - (* Test Logger Bot with real EIO operations *) 300 - Printf.printf "\n=== Testing Logger Bot with Real EIO ===\n"; 301 - 302 - let test_log_message = Zulip_bot.Bot_handler.Message_context.create 303 - ~message_id:1002 304 - ~sender_email:"user@company.com" 305 - ~sender_full_name:"Bob Johnson" 306 - ~content:"log This is important information stored with real EIO" 307 - ~message_type:`Direct 308 - () in 309 - 310 - (match Zulip_bot.Bot_handler.handle_message_with_env logger_handler env test_log_message with 311 - | Ok response -> 312 - Printf.printf "✅ Logger bot response: %s\n" 313 - (match response with 314 - | Zulip_bot.Bot_handler.Response.Reply content -> content 315 - | _ -> "Other response type") 316 - | Error err -> 317 - Printf.printf "❌ Logger bot error: %s\n" (Zulip.Error.message err)); 318 - 319 - (* Demonstrate bot runner with real EIO environment *) 320 - Printf.printf "\n=== Testing Bot Runner with Real EIO ===\n"; 321 - 322 - let bot_runner = Zulip_bot.Bot_runner.create 323 - ~env ~client ~handler:weather_handler in 324 - 325 - Printf.printf "✅ Created bot runner with real EIO environment\n"; 326 - 327 - Printf.printf "\n🎉 Realistic bot demo with real EIO completed!\n"; 328 - Printf.printf "\nFeatures demonstrated:\n"; 329 - Printf.printf "• Real EIO environment passed to bot handlers\n"; 330 - Printf.printf "• Real filesystem access for TOML config and logging\n"; 331 - Printf.printf "• Real network operations with simulated HTTP calls\n"; 332 - Printf.printf "• Bot storage for state management\n"; 333 - Printf.printf "• Proper EIO error handling throughout\n"; 334 - Printf.printf "• TOML configuration file support\n"; 335 - Printf.printf "• Structured concurrency with EIO resource management\n"; 336 - 337 - (* Check that log files were created *) 338 - Printf.printf "\n=== Checking Created Files ===\n"; 339 - (try 340 - let files = Eio.Path.read_dir (env#fs / "tmp") in 341 - List.iter (fun file -> 342 - Printf.printf "📁 Created: tmp/%s\n" file 343 - ) files 344 - with 345 - | Eio.Io (err, _) -> 346 - Printf.printf "⚠️ Could not list tmp/ directory: %s\n" (Printexc.to_string err))
-1
ocaml-zulip/examples/realistic_bot_example.mli
··· 1 - (** Realistic bot example demonstrating EIO filesystem and network access *)
+1 -1
ocaml-zulip/lib/zulip_bot/lib/bot_handler.ml
··· 69 69 storage:Bot_storage.t -> 70 70 identity:Identity.t -> 71 71 message:Message_context.t -> 72 - env:Eio.Env.t -> 72 + env:_ -> 73 73 (Response.t, Zulip.Error.t) result 74 74 end 75 75
+2 -2
ocaml-zulip/lib/zulip_bot/lib/bot_handler.mli
··· 79 79 storage:Bot_storage.t -> 80 80 identity:Identity.t -> 81 81 message:Message_context.t -> 82 - env:Eio.Env.t -> 82 + env:_ -> 83 83 (Response.t, Zulip.Error.t) result 84 84 end 85 85 ··· 98 98 val handle_message : t -> Message_context.t -> (Response.t, Zulip.Error.t) result 99 99 100 100 (** Process an incoming message with EIO environment *) 101 - val handle_message_with_env : t -> Eio.Env.t -> Message_context.t -> (Response.t, Zulip.Error.t) result 101 + val handle_message_with_env : t -> _ -> Message_context.t -> (Response.t, Zulip.Error.t) result 102 102 103 103 (** Get bot identity *) 104 104 val identity : t -> Identity.t