Zulip bots with Eio

Add Zulip standard [api] section config support and daily changelog

ocaml-zulip: Support Zulip's native config format which uses [api] section
with "key" field instead of [bot] section with "api_key". Config loading
now tries [bot], then [api], then bare format.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+31 -4
+31 -4
lib/zulip_bot/config.ml
··· 54 54 |> opt_mem "usage" Init.string ~enc:(fun c -> c.ini_usage) 55 55 |> skip_unknown |> finish) 56 56 57 + (** Codec for Zulip's standard [api] section format (uses "key" not "api_key") *) 58 + let zulip_api_section_codec = 59 + Init.Section.( 60 + obj (fun site email key -> 61 + { 62 + ini_site = site; 63 + ini_email = email; 64 + ini_api_key = key; 65 + ini_description = None; 66 + ini_usage = None; 67 + }) 68 + |> mem "site" Init.string ~enc:(fun c -> c.ini_site) 69 + |> mem "email" Init.string ~enc:(fun c -> c.ini_email) 70 + |> mem "key" Init.string ~enc:(fun c -> c.ini_api_key) 71 + |> skip_unknown |> finish) 72 + 57 73 (** Document codec that accepts a [bot] section or bare options at top level *) 58 74 let ini_doc_codec = 59 75 Init.Document.( ··· 61 77 |> section "bot" ini_section_codec ~enc:Fun.id 62 78 |> skip_unknown |> finish) 63 79 80 + (** Document codec for Zulip's standard [api] section format *) 81 + let zulip_api_doc_codec = 82 + Init.Document.( 83 + obj (fun api -> api) 84 + |> section "api" zulip_api_section_codec ~enc:Fun.id 85 + |> skip_unknown |> finish) 86 + 64 87 (** Codec for configs without section headers (bare key=value pairs) *) 65 88 let bare_section_codec = 66 89 Init.Document.( ··· 74 97 let xdg = Xdge.create fs app in 75 98 let config_file = Eio.Path.(Xdge.config_dir xdg / "zulip.config") in 76 99 Log.debug (fun m -> m "Looking for config at: %a" Eio.Path.pp config_file); 77 - (* Try parsing with [bot] section first, fall back to bare config *) 100 + (* Try parsing with [bot] section, then Zulip [api] section, then bare config *) 78 101 let ini_config = 79 102 match Init_eio.decode_path ini_doc_codec config_file with 80 103 | Ok c -> c 81 104 | Error _ -> ( 82 - (* Try bare config format (no section headers) *) 83 - match Init_eio.decode_path bare_section_codec config_file with 105 + (* Try Zulip's standard [api] section format *) 106 + match Init_eio.decode_path zulip_api_doc_codec config_file with 84 107 | Ok c -> c 85 - | Error e -> raise (Init_eio.err e)) 108 + | Error _ -> ( 109 + (* Try bare config format (no section headers) *) 110 + match Init_eio.decode_path bare_section_codec config_file with 111 + | Ok c -> c 112 + | Error e -> raise (Init_eio.err e))) 86 113 in 87 114 { 88 115 name;