My agentic slop goes here. Not intended for anyone else!
at main 16 kB view raw
1(* Vicuna Bot - User Registration and Management Bot for Zulip *) 2 3open Zulip_bot 4 5(* Set up logging *) 6let src = Logs.Src.create "vicuna" ~doc:"Vicuna User Registration Bot" 7module Log = (val Logs.src_log src : Logs.LOG) 8 9let run_vicuna_bot config_file verbosity env = 10 (* Set up logging based on verbosity *) 11 Logs.set_reporter (Logs_fmt.reporter ()); 12 let log_level = match verbosity with 13 | 0 -> Logs.Info 14 | 1 -> Logs.Debug 15 | _ -> Logs.Debug (* Cap at debug level *) 16 in 17 Logs.set_level (Some log_level); 18 Logs.Src.set_level src (Some log_level); 19 20 Log.app (fun m -> m "Starting Vicuna Bot - User Registration Manager"); 21 Log.app (fun m -> m "Log level: %s" (Logs.level_to_string (Some log_level))); 22 Log.app (fun m -> m "========================================\n"); 23 24 (* Load authentication from .zuliprc file *) 25 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with 26 | Ok a -> 27 Log.info (fun m -> m "Loaded authentication for: %s" (Zulip.Auth.email a)); 28 Log.info (fun m -> m "Server: %s" (Zulip.Auth.server_url a)); 29 a 30 | Error e -> 31 Log.err (fun m -> m "Failed to load .zuliprc: %s" (Zulip.error_message e)); 32 Log.app (fun m -> m "\nPlease create a ~/.zuliprc file with:"); 33 Log.app (fun m -> m "[api]"); 34 Log.app (fun m -> m "email=bot@example.com"); 35 Log.app (fun m -> m "key=your-api-key"); 36 Log.app (fun m -> m "site=https://your-domain.zulipchat.com"); 37 exit 1 38 in 39 40 Eio.Switch.run @@ fun sw -> 41 Log.debug (fun m -> m "Creating Zulip client"); 42 let client = Zulip.Client.create ~sw env auth in 43 44 (* Create bot configuration *) 45 let config = Bot_config.create [] in 46 let bot_email = Zulip.Auth.email auth in 47 48 Log.debug (fun m -> m "Creating bot storage for %s" bot_email); 49 let storage = Bot_storage.create client ~bot_email in 50 51 let identity = Bot_handler.Identity.create 52 ~full_name:"Vicuna Bot" 53 ~email:bot_email 54 ~mention_name:"vicuna" 55 in 56 Log.info (fun m -> m "Bot identity created: %s (%s)" 57 (Bot_handler.Identity.full_name identity) 58 (Bot_handler.Identity.email identity)); 59 60 (* Create the bot handler using the Vicuna bot library *) 61 Log.debug (fun m -> m "Creating Vicuna bot handler"); 62 let handler = Vicuna_bot.create_handler config storage identity in 63 64 Log.debug (fun m -> m "Creating bot runner"); 65 let runner = Bot_runner.create ~env ~client ~handler in 66 67 Log.app (fun m -> m "✨ Vicuna bot is running!"); 68 Log.app (fun m -> m "📬 Send me a direct message to get started."); 69 Log.app (fun m -> m "🤖 Commands: 'register', 'whoami', 'whois', 'list', 'help'"); 70 Log.app (fun m -> m "⛔ Press Ctrl+C to stop.\n"); 71 72 (* Run in real-time mode *) 73 Log.info (fun m -> m "Starting real-time event loop"); 74 try 75 Bot_runner.run_realtime runner; 76 Log.info (fun m -> m "Bot runner exited normally") 77 with 78 | Sys.Break -> 79 Log.info (fun m -> m "Received interrupt signal, shutting down"); 80 Bot_runner.shutdown runner 81 | exn -> 82 Log.err (fun m -> m "Bot crashed with exception: %s" (Printexc.to_string exn)); 83 Log.debug (fun m -> m "Backtrace: %s" (Printexc.get_backtrace ())); 84 raise exn 85 86(* Command-line interface *) 87open Cmdliner 88 89let config_file = 90 let doc = "Path to .zuliprc configuration file" in 91 Arg.(value & opt (some string) None & info ["c"; "config"] ~docv:"FILE" ~doc) 92 93let verbosity = 94 let doc = "Increase verbosity. Use multiple times for more verbose output." in 95 Arg.(value & flag_all & info ["v"; "verbose"] ~doc) 96 97let verbosity_term = 98 Term.(const List.length $ verbosity) 99 100(* CLI management commands *) 101let cli_add_user config_file user_id email full_name is_admin env = 102 Logs.set_reporter (Logs_fmt.reporter ()); 103 Logs.set_level (Some Logs.Info); 104 105 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with 106 | Ok a -> a 107 | Error e -> 108 Printf.eprintf "Error loading config: %s\n" (Zulip.error_message e); 109 exit 1 110 in 111 112 Eio.Switch.run @@ fun sw -> 113 let client = Zulip.Client.create ~sw env auth in 114 let bot_email = Zulip.Auth.email auth in 115 let storage = Bot_storage.create client ~bot_email in 116 117 match Vicuna_bot.register_user ~is_admin storage email user_id full_name with 118 | Ok () -> 119 let admin_str = if is_admin then " (admin)" else "" in 120 Printf.printf "✅ User added%s:\n" admin_str; 121 Printf.printf " • Email: %s\n" email; 122 Printf.printf " • Zulip ID: %d\n" user_id; 123 Printf.printf " • Name: %s\n" full_name; 124 exit 0 125 | Error e -> 126 Printf.eprintf "❌ Failed to add user: %s\n" (Zulip.error_message e); 127 exit 1 128 129let cli_remove_user config_file user_id env = 130 Logs.set_reporter (Logs_fmt.reporter ()); 131 Logs.set_level (Some Logs.Info); 132 133 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with 134 | Ok a -> a 135 | Error e -> 136 Printf.eprintf "Error loading config: %s\n" (Zulip.error_message e); 137 exit 1 138 in 139 140 Eio.Switch.run @@ fun sw -> 141 let client = Zulip.Client.create ~sw env auth in 142 let bot_email = Zulip.Auth.email auth in 143 let storage = Bot_storage.create client ~bot_email in 144 145 match Vicuna_bot.delete_user storage user_id with 146 | Ok () -> 147 Printf.printf "✅ User %d removed\n" user_id; 148 exit 0 149 | Error e -> 150 Printf.eprintf "❌ Failed to remove user: %s\n" (Zulip.error_message e); 151 exit 1 152 153let cli_set_admin config_file user_id is_admin env = 154 Logs.set_reporter (Logs_fmt.reporter ()); 155 Logs.set_level (Some Logs.Info); 156 157 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with 158 | Ok a -> a 159 | Error e -> 160 Printf.eprintf "Error loading config: %s\n" (Zulip.error_message e); 161 exit 1 162 in 163 164 Eio.Switch.run @@ fun sw -> 165 let client = Zulip.Client.create ~sw env auth in 166 let bot_email = Zulip.Auth.email auth in 167 let storage = Bot_storage.create client ~bot_email in 168 169 match Vicuna_bot.set_admin storage user_id is_admin with 170 | Ok () -> 171 let action = if is_admin then "granted to" else "removed from" in 172 Printf.printf "✅ Admin privileges %s user %d\n" action user_id; 173 exit 0 174 | Error e -> 175 Printf.eprintf "❌ Failed to set admin: %s\n" (Zulip.error_message e); 176 exit 1 177 178let cli_list_users config_file show_admins_only env = 179 Logs.set_reporter (Logs_fmt.reporter ()); 180 Logs.set_level (Some Logs.Warning); 181 182 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with 183 | Ok a -> a 184 | Error e -> 185 Printf.eprintf "Error loading config: %s\n" (Zulip.error_message e); 186 exit 1 187 in 188 189 Eio.Switch.run @@ fun sw -> 190 let client = Zulip.Client.create ~sw env auth in 191 let bot_email = Zulip.Auth.email auth in 192 let storage = Bot_storage.create client ~bot_email in 193 194 let user_ids = Vicuna_bot.get_all_user_ids storage in 195 let users = List.filter_map (fun id -> 196 match Vicuna_bot.lookup_user_by_id storage id with 197 | Some (user : Vicuna_bot.user_registration) when (not show_admins_only) || user.is_admin -> Some user 198 | _ -> None 199 ) user_ids in 200 201 if users = [] then ( 202 if show_admins_only then 203 Printf.printf "No admin users found.\n" 204 else 205 Printf.printf "No users registered.\n"; 206 exit 0 207 ) else ( 208 let title = if show_admins_only then "Admin users" else "Registered users" in 209 Printf.printf "%s (%d):\n" title (List.length users); 210 List.iter (fun (user : Vicuna_bot.user_registration) -> 211 let admin_badge = if user.is_admin then " 👑" else "" in 212 Printf.printf " • %s (%s) - ID: %d%s\n" 213 user.full_name user.email user.zulip_id admin_badge 214 ) users; 215 exit 0 216 ) 217 218(* Storage management commands *) 219let cli_storage_list config_file show_all env = 220 Logs.set_reporter (Logs_fmt.reporter ()); 221 Logs.set_level (Some Logs.Warning); 222 223 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with 224 | Ok a -> a 225 | Error e -> 226 Printf.eprintf "Error loading config: %s\n" (Zulip.error_message e); 227 exit 1 228 in 229 230 Eio.Switch.run @@ fun sw -> 231 let client = Zulip.Client.create ~sw env auth in 232 let bot_email = Zulip.Auth.email auth in 233 let storage = Bot_storage.create client ~bot_email in 234 235 if show_all then ( 236 (* Show ALL keys including deleted (empty) ones *) 237 match Bot_storage.keys storage with 238 | Ok keys -> 239 if keys = [] then ( 240 Printf.printf "No storage keys found.\n"; 241 exit 0 242 ) else ( 243 Printf.printf "All storage keys including deleted (%d):\n" (List.length keys); 244 List.iter (fun key -> 245 match Vicuna_bot.get_storage_value storage key with 246 | Some value when value = "" -> 247 Printf.printf " • %s [DELETED]\n" key 248 | Some _ -> 249 Printf.printf " • %s\n" key 250 | None -> 251 Printf.printf " • %s [NOT FOUND]\n" key 252 ) keys; 253 exit 0 254 ) 255 | Error e -> 256 Printf.eprintf "❌ Failed to list storage keys: %s\n" (Zulip.error_message e); 257 exit 1 258 ) else ( 259 (* Normal list - only non-empty keys *) 260 match Vicuna_bot.get_storage_keys storage with 261 | Ok keys -> 262 if keys = [] then ( 263 Printf.printf "No storage keys found.\n"; 264 exit 0 265 ) else ( 266 Printf.printf "Storage keys (%d):\n" (List.length keys); 267 List.iter (fun key -> 268 Printf.printf " • %s\n" key 269 ) keys; 270 exit 0 271 ) 272 | Error e -> 273 Printf.eprintf "❌ Failed to list storage keys: %s\n" (Zulip.error_message e); 274 exit 1 275 ) 276 277let cli_storage_view config_file key env = 278 Logs.set_reporter (Logs_fmt.reporter ()); 279 Logs.set_level (Some Logs.Warning); 280 281 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with 282 | Ok a -> a 283 | Error e -> 284 Printf.eprintf "Error loading config: %s\n" (Zulip.error_message e); 285 exit 1 286 in 287 288 Eio.Switch.run @@ fun sw -> 289 let client = Zulip.Client.create ~sw env auth in 290 let bot_email = Zulip.Auth.email auth in 291 let storage = Bot_storage.create client ~bot_email in 292 293 match Vicuna_bot.get_storage_value storage key with 294 | Some value -> 295 Printf.printf "Key: %s\n" key; 296 Printf.printf "Value:\n%s\n" value; 297 exit 0 298 | None -> 299 Printf.eprintf "❌ Key not found: %s\n" key; 300 exit 1 301 302let cli_storage_delete config_file key env = 303 Logs.set_reporter (Logs_fmt.reporter ()); 304 Logs.set_level (Some Logs.Info); 305 306 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with 307 | Ok a -> a 308 | Error e -> 309 Printf.eprintf "Error loading config: %s\n" (Zulip.error_message e); 310 exit 1 311 in 312 313 Eio.Switch.run @@ fun sw -> 314 let client = Zulip.Client.create ~sw env auth in 315 let bot_email = Zulip.Auth.email auth in 316 let storage = Bot_storage.create client ~bot_email in 317 318 match Vicuna_bot.delete_storage_key storage key with 319 | Ok () -> 320 Printf.printf "✅ Deleted storage key: %s\n" key; 321 exit 0 322 | Error e -> 323 Printf.eprintf "❌ Failed to delete storage key: %s\n" (Zulip.error_message e); 324 exit 1 325 326let cli_storage_clear config_file env = 327 Logs.set_reporter (Logs_fmt.reporter ()); 328 Logs.set_level (Some Logs.Info); 329 330 let auth = match Zulip.Auth.from_zuliprc ?path:config_file () with 331 | Ok a -> a 332 | Error e -> 333 Printf.eprintf "Error loading config: %s\n" (Zulip.error_message e); 334 exit 1 335 in 336 337 Eio.Switch.run @@ fun sw -> 338 let client = Zulip.Client.create ~sw env auth in 339 let bot_email = Zulip.Auth.email auth in 340 let storage = Bot_storage.create client ~bot_email in 341 342 (* Get count before clearing *) 343 match Vicuna_bot.get_storage_keys storage with 344 | Error e -> 345 Printf.eprintf "❌ Failed to list storage keys: %s\n" (Zulip.error_message e); 346 exit 1 347 | Ok keys -> 348 let count = List.length keys in 349 if count = 0 then ( 350 Printf.printf "Storage is already empty.\n"; 351 exit 0 352 ) else ( 353 match Vicuna_bot.clear_storage storage with 354 | Ok () -> 355 Printf.printf "✅ Cleared all storage (%d keys deleted)\n" count; 356 exit 0 357 | Error e -> 358 Printf.eprintf "❌ Failed to clear storage: %s\n" (Zulip.error_message e); 359 exit 1 360 ) 361 362(* CLI command definitions *) 363let user_id_arg = 364 let doc = "Zulip user ID" in 365 Arg.(required & pos 0 (some int) None & info [] ~docv:"USER_ID" ~doc) 366 367let email_arg = 368 let doc = "User's email address" in 369 Arg.(required & pos 1 (some string) None & info [] ~docv:"EMAIL" ~doc) 370 371let full_name_arg = 372 let doc = "User's full name" in 373 Arg.(required & pos 2 (some string) None & info [] ~docv:"FULL_NAME" ~doc) 374 375let admin_flag = 376 let doc = "Grant admin privileges" in 377 Arg.(value & flag & info ["admin"] ~doc) 378 379let admins_only_flag = 380 let doc = "Show only admin users" in 381 Arg.(value & flag & info ["admins-only"] ~doc) 382 383let user_add_cmd eio_env = 384 let doc = "Add a user to the bot's registry" in 385 let info = Cmd.info "user-add" ~doc in 386 Cmd.v info Term.(const cli_add_user $ config_file $ user_id_arg $ email_arg $ full_name_arg $ admin_flag $ const eio_env) 387 388let user_remove_cmd eio_env = 389 let doc = "Remove a user from the bot's registry" in 390 let info = Cmd.info "user-remove" ~doc in 391 Cmd.v info Term.(const cli_remove_user $ config_file $ user_id_arg $ const eio_env) 392 393let admin_add_cmd eio_env = 394 let doc = "Grant admin privileges to a user" in 395 let info = Cmd.info "admin-add" ~doc in 396 Cmd.v info Term.(const cli_set_admin $ config_file $ user_id_arg $ const true $ const eio_env) 397 398let admin_remove_cmd eio_env = 399 let doc = "Remove admin privileges from a user" in 400 let info = Cmd.info "admin-remove" ~doc in 401 Cmd.v info Term.(const cli_set_admin $ config_file $ user_id_arg $ const false $ const eio_env) 402 403let user_list_cmd eio_env = 404 let doc = "List all registered users" in 405 let info = Cmd.info "user-list" ~doc in 406 Cmd.v info Term.(const cli_list_users $ config_file $ admins_only_flag $ const eio_env) 407 408(* Storage command arguments *) 409let storage_key_arg = 410 let doc = "Storage key" in 411 Arg.(required & pos 0 (some string) None & info [] ~docv:"KEY" ~doc) 412 413let storage_all_flag = 414 let doc = "Show all keys including deleted ones (with empty values)" in 415 Arg.(value & flag & info ["all"] ~doc) 416 417(* Storage subcommands *) 418let storage_list_cmd eio_env = 419 let doc = "List all storage keys" in 420 let info = Cmd.info "list" ~doc in 421 Cmd.v info Term.(const cli_storage_list $ config_file $ storage_all_flag $ const eio_env) 422 423let storage_view_cmd eio_env = 424 let doc = "View the value of a specific storage key" in 425 let info = Cmd.info "view" ~doc in 426 Cmd.v info Term.(const cli_storage_view $ config_file $ storage_key_arg $ const eio_env) 427 428let storage_delete_cmd eio_env = 429 let doc = "Delete a specific storage key" in 430 let info = Cmd.info "delete" ~doc in 431 Cmd.v info Term.(const cli_storage_delete $ config_file $ storage_key_arg $ const eio_env) 432 433let storage_clear_cmd eio_env = 434 let doc = "Clear all storage (delete all keys)" in 435 let info = Cmd.info "clear" ~doc in 436 Cmd.v info Term.(const cli_storage_clear $ config_file $ const eio_env) 437 438let storage_group eio_env = 439 let doc = "Manage bot storage" in 440 let info = Cmd.info "storage" ~doc in 441 let default_term = Term.(ret (const (`Help (`Auto, None)))) in 442 let cmds = [ 443 storage_list_cmd eio_env; 444 storage_view_cmd eio_env; 445 storage_delete_cmd eio_env; 446 storage_clear_cmd eio_env; 447 ] in 448 Cmd.group info ~default:default_term cmds 449 450let main_group eio_env = 451 let default_info = Cmd.info "vicuna" ~version:"1.0.0" ~doc:"Vicuna - User Registration and Management Bot for Zulip" in 452 let default_term = Term.(const run_vicuna_bot $ config_file $ verbosity_term $ const eio_env) in 453 let cmds = [ 454 user_add_cmd eio_env; 455 user_remove_cmd eio_env; 456 admin_add_cmd eio_env; 457 admin_remove_cmd eio_env; 458 user_list_cmd eio_env; 459 storage_group eio_env; 460 ] in 461 Cmd.group default_info ~default:default_term cmds 462 463let () = 464 (* Initialize the cryptographic RNG for the application *) 465 Mirage_crypto_rng_unix.use_default (); 466 Eio_main.run @@ fun env -> 467 exit (Cmd.eval (main_group env))