Zulip bots with Eio

fill out protocol

+3116 -205
+170 -8
lib/zulip/channel.ml
··· 1 1 type t = { 2 2 name : string; 3 + stream_id : int option; 3 4 description : string; 4 5 invite_only : bool; 6 + is_web_public : bool; 5 7 history_public_to_subscribers : bool; 8 + is_default : bool; 9 + message_retention_days : int option option; 10 + first_message_id : int option; 11 + date_created : float option; 12 + stream_post_policy : int; 6 13 } 7 14 8 - let create ~name ~description ?(invite_only = false) 9 - ?(history_public_to_subscribers = true) () = 10 - { name; description; invite_only; history_public_to_subscribers } 15 + let create ~name ?stream_id ?(description = "") ?(invite_only = false) 16 + ?(is_web_public = false) ?(history_public_to_subscribers = true) 17 + ?(is_default = false) ?message_retention_days ?first_message_id 18 + ?date_created ?(stream_post_policy = 1) () = 19 + { 20 + name; 21 + stream_id; 22 + description; 23 + invite_only; 24 + is_web_public; 25 + history_public_to_subscribers; 26 + is_default; 27 + message_retention_days; 28 + first_message_id; 29 + date_created; 30 + stream_post_policy; 31 + } 11 32 12 33 let name t = t.name 34 + let stream_id t = t.stream_id 13 35 let description t = t.description 14 36 let invite_only t = t.invite_only 37 + let is_web_public t = t.is_web_public 15 38 let history_public_to_subscribers t = t.history_public_to_subscribers 39 + let is_default t = t.is_default 40 + let message_retention_days t = t.message_retention_days 41 + let first_message_id t = t.first_message_id 42 + let date_created t = t.date_created 43 + let stream_post_policy t = t.stream_post_policy 16 44 17 45 let pp fmt t = 18 - Format.fprintf fmt "Channel{name=%s, description=%s}" t.name t.description 46 + Format.fprintf fmt "Channel{name=%s, stream_id=%s, description=%s}" 47 + t.name 48 + (match t.stream_id with Some id -> string_of_int id | None -> "none") 49 + t.description 50 + 51 + module Subscription = struct 52 + type channel = t 53 + 54 + type t = { 55 + channel : channel; 56 + color : string option; 57 + is_muted : bool; 58 + pin_to_top : bool; 59 + desktop_notifications : bool option; 60 + audible_notifications : bool option; 61 + push_notifications : bool option; 62 + email_notifications : bool option; 63 + wildcard_mentions_notify : bool option; 64 + } 65 + 66 + let channel t = t.channel 67 + let color t = t.color 68 + let is_muted t = t.is_muted 69 + let pin_to_top t = t.pin_to_top 70 + let desktop_notifications t = t.desktop_notifications 71 + let audible_notifications t = t.audible_notifications 72 + let push_notifications t = t.push_notifications 73 + let email_notifications t = t.email_notifications 74 + let wildcard_mentions_notify t = t.wildcard_mentions_notify 75 + 76 + let jsont = 77 + let kind = "Subscription" in 78 + let doc = "A Zulip channel subscription" in 79 + let make name stream_id description invite_only is_web_public 80 + history_public_to_subscribers is_default message_retention_days 81 + first_message_id date_created stream_post_policy color is_muted 82 + pin_to_top desktop_notifications audible_notifications 83 + push_notifications email_notifications wildcard_mentions_notify = 84 + let channel = 85 + { 86 + name; 87 + stream_id; 88 + description; 89 + invite_only; 90 + is_web_public; 91 + history_public_to_subscribers; 92 + is_default; 93 + message_retention_days; 94 + first_message_id; 95 + date_created; 96 + stream_post_policy; 97 + } 98 + in 99 + { 100 + channel; 101 + color; 102 + is_muted; 103 + pin_to_top; 104 + desktop_notifications; 105 + audible_notifications; 106 + push_notifications; 107 + email_notifications; 108 + wildcard_mentions_notify; 109 + } 110 + in 111 + Jsont.Object.map ~kind ~doc make 112 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun t -> t.channel.name) 113 + |> Jsont.Object.opt_mem "stream_id" Jsont.int ~enc:(fun t -> 114 + t.channel.stream_id) 115 + |> Jsont.Object.mem "description" Jsont.string 116 + ~dec_absent:"" 117 + ~enc:(fun t -> t.channel.description) 118 + |> Jsont.Object.mem "invite_only" Jsont.bool 119 + ~dec_absent:false 120 + ~enc:(fun t -> t.channel.invite_only) 121 + |> Jsont.Object.mem "is_web_public" Jsont.bool 122 + ~dec_absent:false 123 + ~enc:(fun t -> t.channel.is_web_public) 124 + |> Jsont.Object.mem "history_public_to_subscribers" Jsont.bool 125 + ~dec_absent:true 126 + ~enc:(fun t -> t.channel.history_public_to_subscribers) 127 + |> Jsont.Object.mem "is_default" Jsont.bool 128 + ~dec_absent:false 129 + ~enc:(fun t -> t.channel.is_default) 130 + |> Jsont.Object.opt_mem "message_retention_days" (Jsont.option Jsont.int) 131 + ~enc:(fun t -> t.channel.message_retention_days) 132 + |> Jsont.Object.opt_mem "first_message_id" Jsont.int ~enc:(fun t -> 133 + t.channel.first_message_id) 134 + |> Jsont.Object.opt_mem "date_created" Jsont.number ~enc:(fun t -> 135 + t.channel.date_created) 136 + |> Jsont.Object.mem "stream_post_policy" Jsont.int 137 + ~dec_absent:1 138 + ~enc:(fun t -> t.channel.stream_post_policy) 139 + |> Jsont.Object.opt_mem "color" Jsont.string ~enc:color 140 + |> Jsont.Object.mem "is_muted" Jsont.bool ~dec_absent:false ~enc:is_muted 141 + |> Jsont.Object.mem "pin_to_top" Jsont.bool ~dec_absent:false ~enc:pin_to_top 142 + |> Jsont.Object.opt_mem "desktop_notifications" Jsont.bool 143 + ~enc:desktop_notifications 144 + |> Jsont.Object.opt_mem "audible_notifications" Jsont.bool 145 + ~enc:audible_notifications 146 + |> Jsont.Object.opt_mem "push_notifications" Jsont.bool 147 + ~enc:push_notifications 148 + |> Jsont.Object.opt_mem "email_notifications" Jsont.bool 149 + ~enc:email_notifications 150 + |> Jsont.Object.opt_mem "wildcard_mentions_notify" Jsont.bool 151 + ~enc:wildcard_mentions_notify 152 + |> Jsont.Object.finish 153 + end 19 154 20 155 (* Jsont codec for channel *) 21 156 let jsont = 22 157 let kind = "Channel" in 23 158 let doc = "A Zulip channel (stream)" in 24 - let make name description invite_only history_public_to_subscribers = 25 - { name; description; invite_only; history_public_to_subscribers } 159 + let make name stream_id description invite_only is_web_public 160 + history_public_to_subscribers is_default message_retention_days 161 + first_message_id date_created stream_post_policy = 162 + { 163 + name; 164 + stream_id; 165 + description; 166 + invite_only; 167 + is_web_public; 168 + history_public_to_subscribers; 169 + is_default; 170 + message_retention_days; 171 + first_message_id; 172 + date_created; 173 + stream_post_policy; 174 + } 26 175 in 27 176 Jsont.Object.map ~kind ~doc make 28 177 |> Jsont.Object.mem "name" Jsont.string ~enc:name 29 - |> Jsont.Object.mem "description" Jsont.string ~enc:description 30 - |> Jsont.Object.mem "invite_only" Jsont.bool ~enc:invite_only 178 + |> Jsont.Object.opt_mem "stream_id" Jsont.int ~enc:stream_id 179 + |> Jsont.Object.mem "description" Jsont.string ~dec_absent:"" ~enc:description 180 + |> Jsont.Object.mem "invite_only" Jsont.bool ~dec_absent:false ~enc:invite_only 181 + |> Jsont.Object.mem "is_web_public" Jsont.bool 182 + ~dec_absent:false 183 + ~enc:is_web_public 31 184 |> Jsont.Object.mem "history_public_to_subscribers" Jsont.bool 185 + ~dec_absent:true 32 186 ~enc:history_public_to_subscribers 187 + |> Jsont.Object.mem "is_default" Jsont.bool ~dec_absent:false ~enc:is_default 188 + |> Jsont.Object.opt_mem "message_retention_days" (Jsont.option Jsont.int) 189 + ~enc:message_retention_days 190 + |> Jsont.Object.opt_mem "first_message_id" Jsont.int ~enc:first_message_id 191 + |> Jsont.Object.opt_mem "date_created" Jsont.number ~enc:date_created 192 + |> Jsont.Object.mem "stream_post_policy" Jsont.int 193 + ~dec_absent:1 194 + ~enc:stream_post_policy 33 195 |> Jsont.Object.finish
+109 -3
lib/zulip/channel.mli
··· 1 1 (** Zulip channels (streams). 2 2 3 3 This module represents channel/stream information from the Zulip API. 4 - Use {!jsont} with Bytesrw-eio for wire serialization. *) 4 + Use {!jsont} with Bytesrw-eio for wire serialization. 5 + 6 + Note: Zulip uses "stream" in the API but "channel" in the UI. 7 + This library uses "channel" to match the current Zulip terminology. *) 8 + 9 + (** {1 Channel Type} *) 5 10 6 11 type t 12 + (** A Zulip channel/stream. *) 13 + 14 + (** {1 Construction} *) 7 15 8 16 val create : 9 17 name:string -> 10 - description:string -> 18 + ?stream_id:int -> 19 + ?description:string -> 11 20 ?invite_only:bool -> 21 + ?is_web_public:bool -> 12 22 ?history_public_to_subscribers:bool -> 23 + ?is_default:bool -> 24 + ?message_retention_days:int option -> 25 + ?first_message_id:int -> 26 + ?date_created:float -> 27 + ?stream_post_policy:int -> 13 28 unit -> 14 29 t 30 + (** Create a channel record. 31 + 32 + @param name Channel name (required) 33 + @param stream_id Server-assigned channel ID 34 + @param description Channel description 35 + @param invite_only Whether the channel is private 36 + @param is_web_public Whether the channel is web-public 37 + @param history_public_to_subscribers Whether history is visible to new subscribers 38 + @param is_default Whether this is a default channel for new users 39 + @param message_retention_days Message retention policy (None = forever) 40 + @param first_message_id ID of the first message in the channel 41 + @param date_created Unix timestamp of creation 42 + @param stream_post_policy Who can post (1=any, 2=admins, 3=full members, 4=moderators) *) 43 + 44 + (** {1 Accessors} *) 15 45 16 46 val name : t -> string 47 + (** Channel name. *) 48 + 49 + val stream_id : t -> int option 50 + (** Server-assigned channel ID. *) 51 + 17 52 val description : t -> string 53 + (** Channel description. *) 54 + 18 55 val invite_only : t -> bool 56 + (** Whether the channel is private (invite-only). *) 57 + 58 + val is_web_public : t -> bool 59 + (** Whether the channel is web-public (visible without authentication). *) 60 + 19 61 val history_public_to_subscribers : t -> bool 62 + (** Whether new subscribers can see message history. *) 20 63 21 - (** Jsont codec for the channel type *) 64 + val is_default : t -> bool 65 + (** Whether new users are automatically subscribed. *) 66 + 67 + val message_retention_days : t -> int option option 68 + (** Message retention policy. [None] if not set (use organization default), 69 + [Some None] for unlimited retention, [Some (Some n)] for n days. *) 70 + 71 + val first_message_id : t -> int option 72 + (** ID of the first message in the channel. *) 73 + 74 + val date_created : t -> float option 75 + (** Unix timestamp when the channel was created. *) 76 + 77 + val stream_post_policy : t -> int 78 + (** Who can post to the channel. 79 + 1 = any member, 2 = admins only, 3 = full members, 4 = moderators only. *) 80 + 81 + (** {1 Subscription Info} 82 + 83 + When retrieved via subscriptions API, channels include additional 84 + subscription-specific fields. *) 85 + 86 + module Subscription : sig 87 + type channel := t 88 + 89 + type t 90 + (** A channel subscription with user-specific settings. *) 91 + 92 + val channel : t -> channel 93 + (** The underlying channel. *) 94 + 95 + val color : t -> string option 96 + (** User's color preference for the channel (hex string). *) 97 + 98 + val is_muted : t -> bool 99 + (** Whether the user has muted this channel. *) 100 + 101 + val pin_to_top : t -> bool 102 + (** Whether the channel is pinned. *) 103 + 104 + val desktop_notifications : t -> bool option 105 + (** Desktop notification setting (None = use global default). *) 106 + 107 + val audible_notifications : t -> bool option 108 + (** Sound notification setting. *) 109 + 110 + val push_notifications : t -> bool option 111 + (** Push notification setting. *) 112 + 113 + val email_notifications : t -> bool option 114 + (** Email notification setting. *) 115 + 116 + val wildcard_mentions_notify : t -> bool option 117 + (** Whether to notify on @all/@everyone mentions. *) 118 + 119 + val jsont : t Jsont.t 120 + (** Jsont codec for subscription. *) 121 + end 122 + 123 + (** {1 JSON Codec} *) 124 + 22 125 val jsont : t Jsont.t 126 + (** Jsont codec for the channel type. *) 127 + 128 + (** {1 Pretty Printing} *) 23 129 24 130 val pp : Format.formatter -> t -> unit
+468 -41
lib/zulip/channels.ml
··· 1 - let create_channel client channel = 2 - let body = Encode.to_form_urlencoded Channel.jsont channel in 3 - let content_type = "application/x-www-form-urlencoded" in 1 + let list client = 2 + let response_codec = 3 + Jsont.Object.( 4 + map ~kind:"StreamsResponse" (fun streams -> streams) 5 + |> mem "streams" (Jsont.list Channel.jsont) ~enc:(fun x -> x) 6 + |> finish) 7 + in 8 + let json = Client.request client ~method_:`GET ~path:"/api/v1/streams" () in 9 + match Encode.from_json response_codec json with 10 + | Ok channels -> channels 11 + | Error msg -> 12 + Error.raise_with_context 13 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 14 + "parsing channels list" 15 + 16 + let list_all client ?include_public ?include_web_public ?include_subscribed 17 + ?include_all_active ?include_default ?include_owner_subscribed () = 18 + let params = 19 + List.filter_map Fun.id 20 + [ 21 + Option.map (fun v -> ("include_public", string_of_bool v)) include_public; 22 + Option.map 23 + (fun v -> ("include_web_public", string_of_bool v)) 24 + include_web_public; 25 + Option.map 26 + (fun v -> ("include_subscribed", string_of_bool v)) 27 + include_subscribed; 28 + Option.map 29 + (fun v -> ("include_all_active", string_of_bool v)) 30 + include_all_active; 31 + Option.map 32 + (fun v -> ("include_default", string_of_bool v)) 33 + include_default; 34 + Option.map 35 + (fun v -> ("include_owner_subscribed", string_of_bool v)) 36 + include_owner_subscribed; 37 + ] 38 + in 39 + let response_codec = 40 + Jsont.Object.( 41 + map ~kind:"StreamsResponse" (fun streams -> streams) 42 + |> mem "streams" (Jsont.list Channel.jsont) ~enc:(fun x -> x) 43 + |> finish) 44 + in 45 + let json = 46 + Client.request client ~method_:`GET ~path:"/api/v1/streams" ~params () 47 + in 48 + match Encode.from_json response_codec json with 49 + | Ok channels -> channels 50 + | Error msg -> 51 + Error.raise_with_context 52 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 53 + "parsing channels list" 54 + 55 + let get_id client ~name = 56 + let encoded_name = Uri.pct_encode name in 57 + let response_codec = 58 + Jsont.Object.( 59 + map ~kind:"StreamIdResponse" (fun id -> id) 60 + |> mem "stream_id" Jsont.int ~enc:(fun x -> x) 61 + |> finish) 62 + in 63 + let json = 64 + Client.request client ~method_:`GET 65 + ~path:("/api/v1/get_stream_id?stream=" ^ encoded_name) 66 + () 67 + in 68 + match Encode.from_json response_codec json with 69 + | Ok id -> id 70 + | Error msg -> 71 + Error.raise_with_context 72 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 73 + "getting stream id for %s" name 74 + 75 + let get_by_id client ~stream_id = 76 + let response_codec = 77 + Jsont.Object.( 78 + map ~kind:"StreamResponse" (fun stream -> stream) 79 + |> mem "stream" Channel.jsont ~enc:(fun x -> x) 80 + |> finish) 81 + in 82 + let json = 83 + Client.request client ~method_:`GET 84 + ~path:("/api/v1/streams/" ^ string_of_int stream_id) 85 + () 86 + in 87 + match Encode.from_json response_codec json with 88 + | Ok channel -> channel 89 + | Error msg -> 90 + Error.raise_with_context 91 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 92 + "getting stream %d" stream_id 93 + 94 + type create_options = { 95 + name : string; 96 + description : string option; 97 + invite_only : bool option; 98 + is_web_public : bool option; 99 + history_public_to_subscribers : bool option; 100 + message_retention_days : int option option; 101 + can_remove_subscribers_group : int option; 102 + } 103 + 104 + let create client opts = 105 + let make_string s = Jsont.String (s, Jsont.Meta.none) in 106 + let subs = 107 + Jsont.Array 108 + ([ 109 + Jsont.Object 110 + (List.filter_map Fun.id 111 + [ 112 + Some (("name", Jsont.Meta.none), make_string opts.name); 113 + Option.map (fun d -> (("description", Jsont.Meta.none), make_string d)) opts.description; 114 + ], Jsont.Meta.none); 115 + ], Jsont.Meta.none) 116 + in 117 + let params = 118 + [ ("subscriptions", Encode.to_json_string Jsont.json subs) ] 119 + @ List.filter_map Fun.id 120 + [ 121 + Option.map 122 + (fun v -> ("invite_only", string_of_bool v)) 123 + opts.invite_only; 124 + Option.map 125 + (fun v -> ("is_web_public", string_of_bool v)) 126 + opts.is_web_public; 127 + Option.map 128 + (fun v -> ("history_public_to_subscribers", string_of_bool v)) 129 + opts.history_public_to_subscribers; 130 + ] 131 + in 132 + let response_codec = 133 + Jsont.Object.( 134 + map ~kind:"CreateResponse" (fun created -> created) 135 + |> mem "subscribed" Jsont.json ~enc:(fun _ -> Jsont.Object ([], Jsont.Meta.none)) 136 + |> finish) 137 + in 138 + let json = 139 + Client.request client ~method_:`POST ~path:"/api/v1/users/me/subscriptions" 140 + ~params () 141 + in 142 + ignore (Encode.from_json response_codec json); 143 + (* Return the stream_id - we need to look it up *) 144 + get_id client ~name:opts.name 145 + 146 + let create_simple client ~name ?description ?invite_only () = 147 + create client 148 + { 149 + name; 150 + description; 151 + invite_only; 152 + is_web_public = None; 153 + history_public_to_subscribers = None; 154 + message_retention_days = None; 155 + can_remove_subscribers_group = None; 156 + } 157 + 158 + let update client ~stream_id ?description ?new_name ?is_private ?is_web_public 159 + ?history_public_to_subscribers ?message_retention_days ?stream_post_policy 160 + () = 161 + let params = 162 + List.filter_map Fun.id 163 + [ 164 + Option.map (fun v -> ("description", v)) description; 165 + Option.map (fun v -> ("new_name", v)) new_name; 166 + Option.map (fun v -> ("is_private", string_of_bool v)) is_private; 167 + Option.map (fun v -> ("is_web_public", string_of_bool v)) is_web_public; 168 + Option.map 169 + (fun v -> ("history_public_to_subscribers", string_of_bool v)) 170 + history_public_to_subscribers; 171 + Option.map 172 + (fun v -> 173 + ( "message_retention_days", 174 + match v with None -> "unlimited" | Some d -> string_of_int d )) 175 + message_retention_days; 176 + Option.map 177 + (fun v -> ("stream_post_policy", string_of_int v)) 178 + stream_post_policy; 179 + ] 180 + in 4 181 let _response = 5 - Client.request client ~method_:`POST ~path:"/api/v1/streams" ~body 6 - ~content_type () 182 + Client.request client ~method_:`PATCH 183 + ~path:("/api/v1/streams/" ^ string_of_int stream_id) 184 + ~params () 7 185 in 8 186 () 9 187 10 - let delete client ~name = 11 - let encoded_name = Uri.pct_encode name in 188 + let delete client ~stream_id = 12 189 let _response = 13 190 Client.request client ~method_:`DELETE 14 - ~path:("/api/v1/streams/" ^ encoded_name) 191 + ~path:("/api/v1/streams/" ^ string_of_int stream_id) 15 192 () 16 193 in 17 194 () 18 195 19 - let list client = 20 - (* Define response codec *) 196 + let archive = delete 197 + 198 + let add_default client ~stream_id = 199 + let params = [ ("stream_id", string_of_int stream_id) ] in 200 + let _response = 201 + Client.request client ~method_:`POST ~path:"/api/v1/default_streams" ~params 202 + () 203 + in 204 + () 205 + 206 + let remove_default client ~stream_id = 207 + let params = [ ("stream_id", string_of_int stream_id) ] in 208 + let _response = 209 + Client.request client ~method_:`DELETE ~path:"/api/v1/default_streams" 210 + ~params () 211 + in 212 + () 213 + 214 + type subscription_request = { 215 + name : string; 216 + color : string option; 217 + description : string option; 218 + } 219 + 220 + let subscribe client ~subscriptions ?principals ?authorization_errors_fatal 221 + ?announce ?invite_only ?history_public_to_subscribers () = 222 + let make_string s = Jsont.String (s, Jsont.Meta.none) in 223 + let subs_json = 224 + Jsont.Array 225 + (List.map 226 + (fun s -> 227 + Jsont.Object 228 + (List.filter_map Fun.id 229 + [ 230 + Some (("name", Jsont.Meta.none), make_string s.name); 231 + Option.map (fun c -> (("color", Jsont.Meta.none), make_string c)) s.color; 232 + Option.map (fun d -> (("description", Jsont.Meta.none), make_string d)) s.description; 233 + ], Jsont.Meta.none)) 234 + subscriptions, Jsont.Meta.none) 235 + in 236 + let params = 237 + [ ("subscriptions", Encode.to_json_string Jsont.json subs_json) ] 238 + @ List.filter_map Fun.id 239 + [ 240 + Option.map 241 + (fun p -> 242 + ( "principals", 243 + match p with 244 + | `Emails emails -> 245 + Encode.to_json_string (Jsont.list Jsont.string) emails 246 + | `User_ids ids -> 247 + Encode.to_json_string (Jsont.list Jsont.int) ids )) 248 + principals; 249 + Option.map 250 + (fun v -> ("authorization_errors_fatal", string_of_bool v)) 251 + authorization_errors_fatal; 252 + Option.map (fun v -> ("announce", string_of_bool v)) announce; 253 + Option.map (fun v -> ("invite_only", string_of_bool v)) invite_only; 254 + Option.map 255 + (fun v -> ("history_public_to_subscribers", string_of_bool v)) 256 + history_public_to_subscribers; 257 + ] 258 + in 259 + Client.request client ~method_:`POST ~path:"/api/v1/users/me/subscriptions" 260 + ~params () 261 + 262 + let subscribe_simple client ~channels = 263 + let subscriptions = 264 + List.map (fun name -> { name; color = None; description = None }) channels 265 + in 266 + let _ = subscribe client ~subscriptions () in 267 + () 268 + 269 + let unsubscribe client ~subscriptions ?principals () = 270 + let params = 271 + [ ("subscriptions", Encode.to_json_string (Jsont.list Jsont.string) subscriptions) ] 272 + @ List.filter_map Fun.id 273 + [ 274 + Option.map 275 + (fun p -> 276 + ( "principals", 277 + match p with 278 + | `Emails emails -> 279 + Encode.to_json_string (Jsont.list Jsont.string) emails 280 + | `User_ids ids -> 281 + Encode.to_json_string (Jsont.list Jsont.int) ids )) 282 + principals; 283 + ] 284 + in 285 + Client.request client ~method_:`DELETE ~path:"/api/v1/users/me/subscriptions" 286 + ~params () 287 + 288 + let unsubscribe_simple client ~channels = 289 + let _ = unsubscribe client ~subscriptions:channels () in 290 + () 291 + 292 + let get_subscriptions client = 21 293 let response_codec = 22 294 Jsont.Object.( 23 - map ~kind:"StreamsResponse" (fun streams -> streams) 24 - |> mem "streams" (Jsont.list Channel.jsont) ~enc:(fun x -> x) 295 + map ~kind:"SubscriptionsResponse" (fun subs -> subs) 296 + |> mem "subscriptions" (Jsont.list Channel.Subscription.jsont) 297 + ~enc:(fun x -> x) 25 298 |> finish) 26 299 in 27 - let json = Client.request client ~method_:`GET ~path:"/api/v1/streams" () in 300 + let json = 301 + Client.request client ~method_:`GET ~path:"/api/v1/users/me/subscriptions" 302 + () 303 + in 28 304 match Encode.from_json response_codec json with 29 - | Ok channels -> channels 305 + | Ok subs -> subs 30 306 | Error msg -> 31 307 Error.raise_with_context 32 308 (Error.make ~code:(Other "json_parse") ~message:msg ()) 33 - "parsing channels list" 34 - 35 - (* Request types with jsont codecs *) 36 - module Subscribe_request = struct 37 - type t = { subscriptions : string list } 309 + "parsing subscriptions" 38 310 39 - let codec = 311 + let get_subscription_status client ~user_id ~stream_id = 312 + let response_codec = 40 313 Jsont.Object.( 41 - map ~kind:"SubscribeRequest" (fun subscriptions -> { subscriptions }) 42 - |> mem "subscriptions" (Jsont.list Jsont.string) 43 - ~enc:(fun r -> r.subscriptions) 314 + map ~kind:"SubscriptionStatusResponse" (fun status -> status) 315 + |> mem "is_subscribed" Jsont.bool ~enc:(fun x -> x) 44 316 |> finish) 45 - end 317 + in 318 + let json = 319 + Client.request client ~method_:`GET 320 + ~path: 321 + ("/api/v1/users/" ^ string_of_int user_id ^ "/subscriptions/" 322 + ^ string_of_int stream_id) 323 + () 324 + in 325 + match Encode.from_json response_codec json with 326 + | Ok status -> status 327 + | Error msg -> 328 + Error.raise_with_context 329 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 330 + "checking subscription status" 46 331 47 - module Unsubscribe_request = struct 48 - type t = { delete : string list } 332 + let update_subscription_settings client ~stream_id ?color ?is_muted ?pin_to_top 333 + ?desktop_notifications ?audible_notifications ?push_notifications 334 + ?email_notifications ?wildcard_mentions_notify () = 335 + let data = 336 + [ 337 + {|[{"stream_id":|} 338 + ^ string_of_int stream_id 339 + ^ (List.filter_map Fun.id 340 + [ 341 + Option.map (fun v -> Printf.sprintf {|,"property":"color","value":"%s"|} v) color; 342 + Option.map 343 + (fun v -> Printf.sprintf {|,"property":"is_muted","value":%b|} v) 344 + is_muted; 345 + Option.map 346 + (fun v -> Printf.sprintf {|,"property":"pin_to_top","value":%b|} v) 347 + pin_to_top; 348 + Option.map 349 + (fun v -> 350 + Printf.sprintf {|,"property":"desktop_notifications","value":%b|} v) 351 + desktop_notifications; 352 + Option.map 353 + (fun v -> 354 + Printf.sprintf {|,"property":"audible_notifications","value":%b|} v) 355 + audible_notifications; 356 + Option.map 357 + (fun v -> 358 + Printf.sprintf {|,"property":"push_notifications","value":%b|} v) 359 + push_notifications; 360 + Option.map 361 + (fun v -> 362 + Printf.sprintf {|,"property":"email_notifications","value":%b|} v) 363 + email_notifications; 364 + Option.map 365 + (fun v -> 366 + Printf.sprintf 367 + {|,"property":"wildcard_mentions_notify","value":%b|} v) 368 + wildcard_mentions_notify; 369 + ] 370 + |> String.concat "") 371 + ^ "}]"; 372 + ] 373 + in 374 + let params = [ ("subscription_data", String.concat "" data) ] in 375 + let _response = 376 + Client.request client ~method_:`POST 377 + ~path:"/api/v1/users/me/subscriptions/properties" ~params () 378 + in 379 + () 49 380 50 - let codec = 381 + module Topic = struct 382 + type t = { name : string; max_id : int } 383 + 384 + let name t = t.name 385 + let max_id t = t.max_id 386 + 387 + let jsont = 51 388 Jsont.Object.( 52 - map ~kind:"UnsubscribeRequest" (fun delete -> { delete }) 53 - |> mem "delete" (Jsont.list Jsont.string) ~enc:(fun r -> r.delete) 389 + map ~kind:"Topic" (fun name max_id -> { name; max_id }) 390 + |> mem "name" Jsont.string ~enc:name 391 + |> mem "max_id" Jsont.int ~enc:max_id 54 392 |> finish) 55 393 end 56 394 57 - let subscribe client ~channels = 58 - let req = Subscribe_request.{ subscriptions = channels } in 59 - let body = Encode.to_form_urlencoded Subscribe_request.codec req in 60 - let content_type = "application/x-www-form-urlencoded" in 395 + let get_topics client ~stream_id = 396 + let response_codec = 397 + Jsont.Object.( 398 + map ~kind:"TopicsResponse" (fun topics -> topics) 399 + |> mem "topics" (Jsont.list Topic.jsont) ~enc:(fun x -> x) 400 + |> finish) 401 + in 402 + let json = 403 + Client.request client ~method_:`GET 404 + ~path:("/api/v1/users/me/" ^ string_of_int stream_id ^ "/topics") 405 + () 406 + in 407 + match Encode.from_json response_codec json with 408 + | Ok topics -> topics 409 + | Error msg -> 410 + Error.raise_with_context 411 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 412 + "getting topics for stream %d" stream_id 413 + 414 + let delete_topic client ~stream_id ~topic = 415 + let params = [ ("topic_name", topic) ] in 61 416 let _response = 62 - Client.request client ~method_:`POST ~path:"/api/v1/users/me/subscriptions" 63 - ~body ~content_type () 417 + Client.request client ~method_:`POST 418 + ~path:("/api/v1/streams/" ^ string_of_int stream_id ^ "/delete_topic") 419 + ~params () 64 420 in 65 421 () 66 422 67 - let unsubscribe client ~channels = 68 - let req = Unsubscribe_request.{ delete = channels } in 69 - let body = Encode.to_form_urlencoded Unsubscribe_request.codec req in 70 - let content_type = "application/x-www-form-urlencoded" in 423 + type mute_op = Mute | Unmute 424 + 425 + let set_topic_mute client ~stream_id ~topic ~op = 426 + let params = 427 + [ 428 + ("stream_id", string_of_int stream_id); 429 + ("topic", topic); 430 + ("op", match op with Mute -> "add" | Unmute -> "remove"); 431 + ] 432 + in 71 433 let _response = 72 - Client.request client ~method_:`DELETE 73 - ~path:"/api/v1/users/me/subscriptions" ~body ~content_type () 434 + Client.request client ~method_:`PATCH 435 + ~path:"/api/v1/users/me/subscriptions/muted_topics" ~params () 74 436 in 75 437 () 438 + 439 + let get_muted_topics client = 440 + let response_codec = 441 + Jsont.Object.( 442 + map ~kind:"MutedTopicsResponse" (fun topics -> topics) 443 + |> mem "muted_topics" 444 + (Jsont.list 445 + (Jsont.Object.( 446 + map ~kind:"MutedTopic" (fun stream_id topic _ts -> 447 + (stream_id, topic)) 448 + |> mem "stream_id" Jsont.int ~enc:fst 449 + |> mem "topic_name" Jsont.string ~enc:snd 450 + |> mem "date_muted" Jsont.int ~dec_absent:0 ~enc:(fun _ -> 0) 451 + |> finish))) 452 + ~enc:(fun x -> x) 453 + |> finish) 454 + in 455 + let json = 456 + Client.request client ~method_:`GET 457 + ~path:"/api/v1/users/me/subscriptions/muted_topics" () 458 + in 459 + match Encode.from_json response_codec json with 460 + | Ok topics -> topics 461 + | Error msg -> 462 + Error.raise_with_context 463 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 464 + "getting muted topics" 465 + 466 + let get_subscribers client ~stream_id = 467 + let response_codec = 468 + Jsont.Object.( 469 + map ~kind:"SubscribersResponse" (fun subs -> subs) 470 + |> mem "subscribers" (Jsont.list Jsont.int) ~enc:(fun x -> x) 471 + |> finish) 472 + in 473 + let json = 474 + Client.request client ~method_:`GET 475 + ~path:("/api/v1/streams/" ^ string_of_int stream_id ^ "/members") 476 + () 477 + in 478 + match Encode.from_json response_codec json with 479 + | Ok subs -> subs 480 + | Error msg -> 481 + Error.raise_with_context 482 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 483 + "getting subscribers for stream %d" stream_id 484 + 485 + let get_email_address client ~stream_id = 486 + let response_codec = 487 + Jsont.Object.( 488 + map ~kind:"EmailAddressResponse" (fun email -> email) 489 + |> mem "email" Jsont.string ~enc:(fun x -> x) 490 + |> finish) 491 + in 492 + let json = 493 + Client.request client ~method_:`GET 494 + ~path:("/api/v1/streams/" ^ string_of_int stream_id ^ "/email_address") 495 + () 496 + in 497 + match Encode.from_json response_codec json with 498 + | Ok email -> email 499 + | Error msg -> 500 + Error.raise_with_context 501 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 502 + "getting email address for stream %d" stream_id
+216 -10
lib/zulip/channels.mli
··· 3 3 All functions raise [Eio.Io] with [Error.E error] on failure. 4 4 Context is automatically added indicating the operation being performed. *) 5 5 6 - val create_channel : Client.t -> Channel.t -> unit 7 - (** Create a new channel (stream). 6 + (** {1 Listing Channels} *) 7 + 8 + val list : Client.t -> Channel.t list 9 + (** List all channels in the organization. 10 + @raise Eio.Io on failure *) 11 + 12 + val list_all : 13 + Client.t -> 14 + ?include_public:bool -> 15 + ?include_web_public:bool -> 16 + ?include_subscribed:bool -> 17 + ?include_all_active:bool -> 18 + ?include_default:bool -> 19 + ?include_owner_subscribed:bool -> 20 + unit -> 21 + Channel.t list 22 + (** List channels with filtering options. 23 + 24 + @param include_public Include public channels (default: true) 25 + @param include_web_public Include web-public channels 26 + @param include_subscribed Include subscribed channels 27 + @param include_all_active Include all active channels (admin only) 28 + @param include_default Include default channels 29 + @param include_owner_subscribed Include channels the owner is subscribed to 30 + @raise Eio.Io on failure *) 31 + 32 + (** {1 Channel Lookup} *) 33 + 34 + val get_id : Client.t -> name:string -> int 35 + (** Get the stream ID for a channel by name. 36 + @raise Eio.Io if the channel doesn't exist or on failure *) 37 + 38 + val get_by_id : Client.t -> stream_id:int -> Channel.t 39 + (** Get channel information by ID. 40 + @raise Eio.Io on failure *) 41 + 42 + (** {1 Creating Channels} *) 43 + 44 + (** Options for creating a channel. *) 45 + type create_options = { 46 + name : string; (** Channel name (required) *) 47 + description : string option; (** Channel description *) 48 + invite_only : bool option; (** Whether the channel is private *) 49 + is_web_public : bool option; (** Whether the channel is web-public *) 50 + history_public_to_subscribers : bool option; 51 + (** Whether history is visible to new subscribers *) 52 + message_retention_days : int option option; 53 + (** Message retention (None = default, Some None = forever) *) 54 + can_remove_subscribers_group : int option; 55 + (** User group that can remove subscribers *) 56 + } 57 + 58 + val create : Client.t -> create_options -> int 59 + (** Create a new channel. 60 + @return The stream_id of the created channel 61 + @raise Eio.Io on failure *) 62 + 63 + val create_simple : 64 + Client.t -> name:string -> ?description:string -> ?invite_only:bool -> unit -> int 65 + (** Create a new channel with common options. 66 + @return The stream_id of the created channel 8 67 @raise Eio.Io on failure *) 9 68 10 - val delete : Client.t -> name:string -> unit 11 - (** Delete a channel by name. 69 + (** {1 Updating Channels} *) 70 + 71 + val update : 72 + Client.t -> 73 + stream_id:int -> 74 + ?description:string -> 75 + ?new_name:string -> 76 + ?is_private:bool -> 77 + ?is_web_public:bool -> 78 + ?history_public_to_subscribers:bool -> 79 + ?message_retention_days:int option -> 80 + ?stream_post_policy:int -> 81 + unit -> 82 + unit 83 + (** Update channel properties. 12 84 @raise Eio.Io on failure *) 13 85 14 - val list : Client.t -> Channel.t list 15 - (** List all channels in the organization. 86 + (** {1 Deleting Channels} *) 87 + 88 + val delete : Client.t -> stream_id:int -> unit 89 + (** Delete a channel by ID. 16 90 @raise Eio.Io on failure *) 17 91 18 - val subscribe : Client.t -> channels:string list -> unit 19 - (** Subscribe to one or more channels. 92 + val archive : Client.t -> stream_id:int -> unit 93 + (** Archive a channel (soft delete). 20 94 @raise Eio.Io on failure *) 21 95 22 - val unsubscribe : Client.t -> channels:string list -> unit 23 - (** Unsubscribe from one or more channels. 96 + (** {1 Default Channels} *) 97 + 98 + val add_default : Client.t -> stream_id:int -> unit 99 + (** Add a channel to the list of default channels for new users. 100 + @raise Eio.Io on failure *) 101 + 102 + val remove_default : Client.t -> stream_id:int -> unit 103 + (** Remove a channel from the list of default channels. 104 + @raise Eio.Io on failure *) 105 + 106 + (** {1 Subscriptions} *) 107 + 108 + (** Subscription request for a single channel. *) 109 + type subscription_request = { 110 + name : string; (** Channel name *) 111 + color : string option; (** Color preference (hex string) *) 112 + description : string option; (** Description (for new channels) *) 113 + } 114 + 115 + val subscribe : 116 + Client.t -> 117 + subscriptions:subscription_request list -> 118 + ?principals:[ `Emails of string list | `User_ids of int list ] -> 119 + ?authorization_errors_fatal:bool -> 120 + ?announce:bool -> 121 + ?invite_only:bool -> 122 + ?history_public_to_subscribers:bool -> 123 + unit -> 124 + Jsont.json 125 + (** Subscribe users to channels. 126 + 127 + @param subscriptions List of channels to subscribe to 128 + @param principals Users to subscribe (default: current user) 129 + @param authorization_errors_fatal Whether to abort on permission errors 130 + @param announce Whether to announce new subscriptions 131 + @param invite_only For new channels: whether they should be private 132 + @param history_public_to_subscribers For new channels: history visibility 133 + @return JSON with "subscribed", "already_subscribed", and "unauthorized" fields 134 + @raise Eio.Io on failure *) 135 + 136 + val subscribe_simple : Client.t -> channels:string list -> unit 137 + (** Subscribe the current user to channels by name. 138 + @raise Eio.Io on failure *) 139 + 140 + val unsubscribe : 141 + Client.t -> 142 + subscriptions:string list -> 143 + ?principals:[ `Emails of string list | `User_ids of int list ] -> 144 + unit -> 145 + Jsont.json 146 + (** Unsubscribe users from channels. 147 + 148 + @param subscriptions List of channel names to unsubscribe from 149 + @param principals Users to unsubscribe (default: current user) 150 + @return JSON with "removed" and "not_removed" fields 151 + @raise Eio.Io on failure *) 152 + 153 + val unsubscribe_simple : Client.t -> channels:string list -> unit 154 + (** Unsubscribe the current user from channels by name. 155 + @raise Eio.Io on failure *) 156 + 157 + val get_subscriptions : Client.t -> Channel.Subscription.t list 158 + (** Get the current user's channel subscriptions. 159 + @raise Eio.Io on failure *) 160 + 161 + val get_subscription_status : Client.t -> user_id:int -> stream_id:int -> bool 162 + (** Check if a user is subscribed to a channel. 163 + @raise Eio.Io on failure *) 164 + 165 + val update_subscription_settings : 166 + Client.t -> 167 + stream_id:int -> 168 + ?color:string -> 169 + ?is_muted:bool -> 170 + ?pin_to_top:bool -> 171 + ?desktop_notifications:bool -> 172 + ?audible_notifications:bool -> 173 + ?push_notifications:bool -> 174 + ?email_notifications:bool -> 175 + ?wildcard_mentions_notify:bool -> 176 + unit -> 177 + unit 178 + (** Update subscription settings for a channel. 179 + @raise Eio.Io on failure *) 180 + 181 + (** {1 Topics} *) 182 + 183 + (** A topic within a channel. *) 184 + module Topic : sig 185 + type t 186 + 187 + val name : t -> string 188 + (** Topic name. *) 189 + 190 + val max_id : t -> int 191 + (** ID of the latest message in the topic. *) 192 + 193 + val jsont : t Jsont.t 194 + end 195 + 196 + val get_topics : Client.t -> stream_id:int -> Topic.t list 197 + (** Get all topics in a channel. 198 + @raise Eio.Io on failure *) 199 + 200 + val delete_topic : Client.t -> stream_id:int -> topic:string -> unit 201 + (** Delete a topic and all its messages. 202 + Requires admin privileges. 203 + @raise Eio.Io on failure *) 204 + 205 + (** {1 Topic Muting} *) 206 + 207 + type mute_op = 208 + | Mute (** Mute the topic *) 209 + | Unmute (** Unmute the topic *) 210 + 211 + val set_topic_mute : 212 + Client.t -> stream_id:int -> topic:string -> op:mute_op -> unit 213 + (** Mute or unmute a topic. 214 + @raise Eio.Io on failure *) 215 + 216 + val get_muted_topics : Client.t -> (int * string) list 217 + (** Get list of muted topics as (stream_id, topic_name) pairs. 218 + @raise Eio.Io on failure *) 219 + 220 + (** {1 Subscribers} *) 221 + 222 + val get_subscribers : Client.t -> stream_id:int -> int list 223 + (** Get list of user IDs subscribed to a channel. 224 + @raise Eio.Io on failure *) 225 + 226 + (** {1 Email Address} *) 227 + 228 + val get_email_address : Client.t -> stream_id:int -> string 229 + (** Get the email address for posting to a channel. 24 230 @raise Eio.Io on failure *)
+2 -1
lib/zulip/dune
··· 1 1 (library 2 2 (public_name zulip) 3 3 (name zulip) 4 - (libraries eio requests jsont jsont.bytesrw uri base64 logs)) 4 + (libraries eio requests jsont jsont.bytesrw uri base64 logs) 5 + (modules_without_implementation presence server typing user_group))
+98 -20
lib/zulip/event_queue.ml
··· 3 3 4 4 module Log = (val Logs.src_log src : Logs.LOG) 5 5 6 - type t = { id : string } 6 + type t = { id : string; mutable last_event_id : int } 7 7 8 8 (* Request/response codecs *) 9 9 module Register_request = struct 10 10 type t = { event_types : string list option } 11 11 12 - let codec = 12 + let _codec = 13 13 Jsont.Object.( 14 14 map ~kind:"RegisterRequest" (fun event_types -> { event_types }) 15 15 |> opt_mem "event_types" (Jsont.list Jsont.string) ··· 18 18 end 19 19 20 20 module Register_response = struct 21 - type t = { queue_id : string } 21 + type t = { queue_id : string; last_event_id : int } 22 22 23 23 let codec = 24 24 Jsont.Object.( 25 - map ~kind:"RegisterResponse" (fun queue_id -> { queue_id }) 25 + map ~kind:"RegisterResponse" (fun queue_id last_event_id -> 26 + { queue_id; last_event_id }) 26 27 |> mem "queue_id" Jsont.string ~enc:(fun r -> r.queue_id) 28 + |> mem "last_event_id" Jsont.int ~dec_absent:(-1) ~enc:(fun r -> 29 + r.last_event_id) 27 30 |> finish) 28 31 end 29 32 30 - let register client ?event_types () = 33 + let register client ?event_types ?narrow ?all_public_streams ?include_subscribers 34 + ?client_capabilities ?fetch_event_types ?client_gravatar ?slim_presence () = 31 35 let event_types_str = 32 36 Option.map (List.map Event_type.to_string) event_types 33 37 in 34 - let req = Register_request.{ event_types = event_types_str } in 35 - let body = Encode.to_form_urlencoded Register_request.codec req in 36 - let content_type = "application/x-www-form-urlencoded" in 38 + let fetch_event_types_str = 39 + Option.map (List.map Event_type.to_string) fetch_event_types 40 + in 41 + let params = 42 + List.filter_map Fun.id 43 + [ 44 + Option.map 45 + (fun types -> 46 + ("event_types", Encode.to_json_string (Jsont.list Jsont.string) types)) 47 + event_types_str; 48 + Option.map 49 + (fun n -> ("narrow", Encode.to_json_string Narrow.list_jsont n)) 50 + narrow; 51 + Option.map 52 + (fun v -> ("all_public_streams", string_of_bool v)) 53 + all_public_streams; 54 + Option.map 55 + (fun v -> ("include_subscribers", string_of_bool v)) 56 + include_subscribers; 57 + Option.map 58 + (fun v -> ("client_capabilities", Encode.to_json_string Jsont.json v)) 59 + client_capabilities; 60 + Option.map 61 + (fun types -> 62 + ( "fetch_event_types", 63 + Encode.to_json_string (Jsont.list Jsont.string) types )) 64 + fetch_event_types_str; 65 + Option.map 66 + (fun v -> ("client_gravatar", string_of_bool v)) 67 + client_gravatar; 68 + Option.map (fun v -> ("slim_presence", string_of_bool v)) slim_presence; 69 + ] 70 + in 37 71 38 72 (match event_types_str with 39 73 | Some types -> ··· 42 76 | None -> ()); 43 77 44 78 let json = 45 - Client.request client ~method_:`POST ~path:"/api/v1/register" ~body 46 - ~content_type () 79 + Client.request client ~method_:`POST ~path:"/api/v1/register" ~params () 47 80 in 48 81 match Encode.from_json Register_response.codec json with 49 - | Ok response -> { id = response.queue_id } 82 + | Ok response -> 83 + { id = response.queue_id; last_event_id = response.last_event_id } 50 84 | Error msg -> 51 85 Error.raise_with_context 52 86 (Error.make ~code:(Other "json_parse") ~message:msg ()) 53 87 "parsing register response" 54 88 55 89 let id t = t.id 90 + let last_event_id t = t.last_event_id 56 91 57 92 (* Events response codec - events field is optional (may not be present) *) 58 93 module Events_response = struct 59 94 type t = { events : Event.t list } 60 95 61 96 let codec = 62 - (* Use keep_unknown pattern to handle the whole object and extract events manually *) 63 97 let make raw_json = 64 98 match raw_json with 65 99 | Jsont.Object (fields, _) -> 66 100 let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in 67 101 (match List.assoc_opt "events" assoc with 68 102 | Some (Jsont.Array (items, _)) -> 69 - (* Parse each event, skipping failures *) 70 103 let events = 71 104 List.fold_left 72 105 (fun acc item -> ··· 87 120 |> Jsont.Object.finish 88 121 end 89 122 90 - let get_events t client ?last_event_id () = 123 + let get_events t client ?last_event_id ?dont_block () = 124 + let event_id = 125 + match last_event_id with Some id -> id | None -> t.last_event_id 126 + in 91 127 let params = 92 - [ ("queue_id", t.id) ] 93 - @ 94 - match last_event_id with 95 - | None -> [] 96 - | Some event_id -> [ ("last_event_id", string_of_int event_id) ] 128 + [ ("queue_id", t.id); ("last_event_id", string_of_int event_id) ] 129 + @ (match dont_block with 130 + | Some true -> [ ("dont_block", "true") ] 131 + | _ -> []) 97 132 in 98 133 let json = 99 134 Client.request client ~method_:`GET ~path:"/api/v1/events" ~params () ··· 102 137 | Ok response -> 103 138 Log.debug (fun m -> 104 139 m "Got %d events from API" (List.length response.events)); 140 + (* Update internal last_event_id *) 141 + (match response.events with 142 + | [] -> () 143 + | events -> 144 + let max_id = 145 + List.fold_left (fun acc e -> max acc (Event.id e)) event_id events 146 + in 147 + t.last_event_id <- max_id); 105 148 response.events 106 149 | Error msg -> 107 150 Log.warn (fun m -> m "Failed to parse events response: %s" msg); ··· 116 159 in 117 160 () 118 161 119 - let pp fmt t = Format.fprintf fmt "EventQueue{id=%s}" t.id 162 + let call_on_each_event client ?event_types ?narrow ~callback () = 163 + let queue = register client ?event_types ?narrow () in 164 + let rec loop () = 165 + let events = get_events queue client () in 166 + List.iter 167 + (fun event -> 168 + (* Filter out heartbeat events *) 169 + match Event.type_ event with Event_type.Heartbeat -> () | _ -> callback event) 170 + events; 171 + loop () 172 + in 173 + loop () 174 + 175 + let call_on_each_message client ?narrow ~callback () = 176 + call_on_each_event client ~event_types:[ Event_type.Message ] ?narrow 177 + ~callback:(fun event -> 178 + match Event.type_ event with 179 + | Event_type.Message -> callback (Event.data event) 180 + | _ -> ()) 181 + () 182 + 183 + let events t client = 184 + let rec next () = 185 + let events = get_events t client ~dont_block:true () in 186 + match events with 187 + | [] -> 188 + (* No events available, wait a bit and try again *) 189 + Seq.Cons (List.hd (get_events t client ()), next) 190 + | e :: rest -> 191 + (* Return events one by one *) 192 + let rest_seq = List.to_seq rest in 193 + Seq.Cons (e, fun () -> Seq.append rest_seq next ()) 194 + in 195 + next 196 + 197 + let pp fmt t = Format.fprintf fmt "EventQueue{id=%s, last_event_id=%d}" t.id t.last_event_id
+99 -5
lib/zulip/event_queue.mli
··· 1 1 (** Event queue for receiving Zulip events in real-time. 2 2 3 + Event queues provide real-time notifications of changes in Zulip. 4 + Register a queue to receive events, then poll for updates. 5 + 3 6 All functions raise [Eio.Io] with [Error.E error] on failure. *) 4 7 8 + (** {1 Event Queue Type} *) 9 + 5 10 type t 11 + (** An opaque event queue handle. *) 6 12 7 - val register : Client.t -> ?event_types:Event_type.t list -> unit -> t 13 + (** {1 Registration} *) 14 + 15 + val register : 16 + Client.t -> 17 + ?event_types:Event_type.t list -> 18 + ?narrow:Narrow.t list -> 19 + ?all_public_streams:bool -> 20 + ?include_subscribers:bool -> 21 + ?client_capabilities:Jsont.json -> 22 + ?fetch_event_types:Event_type.t list -> 23 + ?client_gravatar:bool -> 24 + ?slim_presence:bool -> 25 + unit -> 26 + t 8 27 (** Register a new event queue with the server. 9 - @param event_types Optional list of event types to subscribe to 28 + 29 + @param event_types Event types to subscribe to (default: all) 30 + @param narrow Narrow filter for message events 31 + @param all_public_streams Include events from all public streams 32 + @param include_subscribers Include subscriber lists in stream events 33 + @param client_capabilities Client capability flags 34 + @param fetch_event_types Types to return initial data for 35 + @param client_gravatar Whether client handles gravatar URLs 36 + @param slim_presence Use compact presence format 10 37 @raise Eio.Io on failure *) 38 + 39 + (** {1 Queue Properties} *) 11 40 12 41 val id : t -> string 13 42 (** Get the queue ID. *) 14 43 15 - val get_events : t -> Client.t -> ?last_event_id:int -> unit -> Event.t list 44 + val last_event_id : t -> int 45 + (** Get the last event ID received. *) 46 + 47 + (** {1 Polling for Events} *) 48 + 49 + val get_events : 50 + t -> Client.t -> ?last_event_id:int -> ?dont_block:bool -> unit -> Event.t list 16 51 (** Get events from the queue. 17 - @param last_event_id Optional event ID to resume from 18 - @raise Eio.Io on failure *) 52 + 53 + @param last_event_id Event ID to resume from (default: use queue's state) 54 + @param dont_block Return immediately even if no events (default: long-poll) 55 + @raise Eio.Io on failure 56 + 57 + Note: This function updates the queue's internal last_event_id. *) 58 + 59 + (** {1 Cleanup} *) 19 60 20 61 val delete : t -> Client.t -> unit 21 62 (** Delete the event queue from the server. 22 63 @raise Eio.Io on failure *) 64 + 65 + (** {1 High-Level Event Processing} 66 + 67 + These functions provide convenient callback-based patterns for 68 + processing events. They handle queue management and reconnection 69 + automatically. *) 70 + 71 + val call_on_each_event : 72 + Client.t -> 73 + ?event_types:Event_type.t list -> 74 + ?narrow:Narrow.t list -> 75 + callback:(Event.t -> unit) -> 76 + unit -> 77 + unit 78 + (** Process events with a callback. 79 + 80 + Registers a queue and continuously polls for events, calling the 81 + callback for each event. Automatically handles reconnection if 82 + the queue expires. 83 + 84 + This function runs indefinitely until cancelled via [Eio.Cancel]. 85 + 86 + @param event_types Event types to subscribe to 87 + @param narrow Narrow filter for message events 88 + @param callback Function called for each event 89 + 90 + Note: Heartbeat events are filtered out automatically. *) 91 + 92 + val call_on_each_message : 93 + Client.t -> 94 + ?narrow:Narrow.t list -> 95 + callback:(Jsont.json -> unit) -> 96 + unit -> 97 + unit 98 + (** Process message events with a callback. 99 + 100 + Convenience wrapper around [call_on_each_event] that filters 101 + for message events and extracts the message data. 102 + 103 + @param narrow Narrow filter for messages 104 + @param callback Function called with each message's JSON data *) 105 + 106 + (** {1 Event Stream} 107 + 108 + For use with Eio's streaming patterns. *) 109 + 110 + val events : t -> Client.t -> Event.t Seq.t 111 + (** Create a lazy sequence of events from the queue. 112 + The sequence polls the server as needed. 113 + 114 + Note: This sequence is infinite - use [Seq.take] or similar to limit. *) 115 + 116 + (** {1 Pretty Printing} *) 23 117 24 118 val pp : Format.formatter -> t -> unit
+53 -6
lib/zulip/event_type.ml
··· 1 - type t = 1 + type t = 2 2 | Message 3 - | Subscription 4 - | User_activity 3 + | Heartbeat 4 + | Presence 5 + | Typing 6 + | Reaction 7 + | Subscription 8 + | Stream 9 + | Realm 10 + | Realm_user 11 + | Realm_emoji 12 + | Realm_linkifiers 13 + | User_group 14 + | User_status 15 + | Update_message 16 + | Delete_message 17 + | Update_message_flags 18 + | Restart 5 19 | Other of string 6 20 7 21 let to_string = function 8 22 | Message -> "message" 23 + | Heartbeat -> "heartbeat" 24 + | Presence -> "presence" 25 + | Typing -> "typing" 26 + | Reaction -> "reaction" 9 27 | Subscription -> "subscription" 10 - | User_activity -> "user_activity" 28 + | Stream -> "stream" 29 + | Realm -> "realm" 30 + | Realm_user -> "realm_user" 31 + | Realm_emoji -> "realm_emoji" 32 + | Realm_linkifiers -> "realm_linkifiers" 33 + | User_group -> "user_group" 34 + | User_status -> "user_status" 35 + | Update_message -> "update_message" 36 + | Delete_message -> "delete_message" 37 + | Update_message_flags -> "update_message_flags" 38 + | Restart -> "restart" 11 39 | Other s -> s 12 40 13 41 let of_string = function 14 42 | "message" -> Message 43 + | "heartbeat" -> Heartbeat 44 + | "presence" -> Presence 45 + | "typing" -> Typing 46 + | "reaction" -> Reaction 15 47 | "subscription" -> Subscription 16 - | "user_activity" -> User_activity 48 + | "stream" -> Stream 49 + | "realm" -> Realm 50 + | "realm_user" -> Realm_user 51 + | "realm_emoji" -> Realm_emoji 52 + | "realm_linkifiers" -> Realm_linkifiers 53 + | "user_group" -> User_group 54 + | "user_status" -> User_status 55 + | "update_message" -> Update_message 56 + | "delete_message" -> Delete_message 57 + | "update_message_flags" -> Update_message_flags 58 + | "restart" -> Restart 17 59 | s -> Other s 18 60 19 - let pp fmt t = Format.fprintf fmt "%s" (to_string t) 61 + let pp fmt t = Format.fprintf fmt "%s" (to_string t) 62 + 63 + let jsont = 64 + let encode t = to_string t in 65 + let decode s = of_string s in 66 + Jsont.string |> Jsont.map ~dec:decode ~enc:encode
+41 -6
lib/zulip/event_type.mli
··· 1 - type t = 2 - | Message 3 - | Subscription 4 - | User_activity 5 - | Other of string 1 + (** Zulip event types. 2 + 3 + This module defines the event types that can be received from the 4 + Zulip event queue. These correspond to the "type" field in event 5 + objects returned by the /events endpoint. *) 6 + 7 + (** {1 Event Types} *) 8 + 9 + type t = 10 + | Message (** New message received *) 11 + | Heartbeat (** Keep-alive heartbeat (internal protocol) *) 12 + | Presence (** User presence update *) 13 + | Typing (** User typing notification *) 14 + | Reaction (** Emoji reaction added/removed *) 15 + | Subscription (** Stream subscription change *) 16 + | Stream (** Stream created/deleted/updated *) 17 + | Realm (** Realm settings changed *) 18 + | Realm_user (** User added/removed/updated in realm *) 19 + | Realm_emoji (** Custom emoji added/removed *) 20 + | Realm_linkifiers (** Linkifier rules changed *) 21 + | User_group (** User group modified *) 22 + | User_status (** User status (away/active) changed *) 23 + | Update_message (** Message edited *) 24 + | Delete_message (** Message deleted *) 25 + | Update_message_flags (** Message flags (read/starred) changed *) 26 + | Restart (** Server restart notification *) 27 + | Other of string (** Unknown/custom event type *) 28 + 29 + (** {1 Conversion} *) 6 30 7 31 val to_string : t -> string 32 + (** Convert an event type to its wire format string. *) 33 + 8 34 val of_string : string -> t 9 - val pp : Format.formatter -> t -> unit 35 + (** Parse an event type from its wire format string. 36 + Unknown types are wrapped in [Other]. *) 37 + 38 + (** {1 Pretty Printing} *) 39 + 40 + val pp : Format.formatter -> t -> unit 41 + 42 + (** {1 JSON Codec} *) 43 + 44 + val jsont : t Jsont.t
+58
lib/zulip/message_flag.ml
··· 1 + type modifiable = [ `Read | `Starred | `Collapsed ] 2 + 3 + type t = 4 + [ modifiable 5 + | `Mentioned 6 + | `Wildcard_mentioned 7 + | `Has_alert_word 8 + | `Historical 9 + ] 10 + 11 + let to_string = function 12 + | `Read -> "read" 13 + | `Starred -> "starred" 14 + | `Collapsed -> "collapsed" 15 + | `Mentioned -> "mentioned" 16 + | `Wildcard_mentioned -> "wildcard_mentioned" 17 + | `Has_alert_word -> "has_alert_word" 18 + | `Historical -> "historical" 19 + 20 + let of_string = function 21 + | "read" -> Some `Read 22 + | "starred" -> Some `Starred 23 + | "collapsed" -> Some `Collapsed 24 + | "mentioned" -> Some `Mentioned 25 + | "wildcard_mentioned" -> Some `Wildcard_mentioned 26 + | "has_alert_word" -> Some `Has_alert_word 27 + | "historical" -> Some `Historical 28 + | _ -> None 29 + 30 + let modifiable_of_string = function 31 + | "read" -> Some `Read 32 + | "starred" -> Some `Starred 33 + | "collapsed" -> Some `Collapsed 34 + | _ -> None 35 + 36 + type op = Add | Remove 37 + 38 + let op_to_string = function Add -> "add" | Remove -> "remove" 39 + 40 + let pp fmt t = Format.fprintf fmt "%s" (to_string t) 41 + 42 + let jsont = 43 + let encode t = to_string t in 44 + let decode s = 45 + match of_string s with 46 + | Some t -> t 47 + | None -> failwith ("Unknown message flag: " ^ s) 48 + in 49 + Jsont.string |> Jsont.map ~dec:decode ~enc:encode 50 + 51 + let modifiable_jsont = 52 + let encode t = to_string t in 53 + let decode s = 54 + match modifiable_of_string s with 55 + | Some t -> t 56 + | None -> failwith ("Unknown modifiable message flag: " ^ s) 57 + in 58 + Jsont.string |> Jsont.map ~dec:decode ~enc:encode
+51
lib/zulip/message_flag.mli
··· 1 + (** Message flags in Zulip. 2 + 3 + Message flags indicate read/unread status, starred messages, 4 + mentions, and other message properties. *) 5 + 6 + (** {1 Flag Types} *) 7 + 8 + type modifiable = 9 + [ `Read (** Message has been read *) 10 + | `Starred (** Message is starred/bookmarked *) 11 + | `Collapsed (** Message content is collapsed *) 12 + ] 13 + (** Flags that can be directly modified by the user. *) 14 + 15 + type t = 16 + [ modifiable 17 + | `Mentioned (** User was @-mentioned in the message *) 18 + | `Wildcard_mentioned (** User was mentioned via @all/@everyone *) 19 + | `Has_alert_word (** Message contains one of user's alert words *) 20 + | `Historical (** Message predates user joining the stream *) 21 + ] 22 + (** All possible message flags. *) 23 + 24 + (** {1 Conversion} *) 25 + 26 + val to_string : [< t ] -> string 27 + (** Convert a flag to its wire format string. *) 28 + 29 + val of_string : string -> t option 30 + (** Parse a flag from its wire format string. *) 31 + 32 + val modifiable_of_string : string -> modifiable option 33 + (** Parse a modifiable flag from its wire format string. *) 34 + 35 + (** {1 Flag Update Operations} *) 36 + 37 + type op = 38 + | Add (** Add the flag to messages *) 39 + | Remove (** Remove the flag from messages *) 40 + 41 + val op_to_string : op -> string 42 + 43 + (** {1 Pretty Printing} *) 44 + 45 + val pp : Format.formatter -> t -> unit 46 + 47 + (** {1 JSON Codec} *) 48 + 49 + val jsont : t Jsont.t 50 + 51 + val modifiable_jsont : modifiable Jsont.t
+187 -27
lib/zulip/messages.ml
··· 1 1 let send client message = 2 - (* Use form-urlencoded encoding for the message *) 3 2 let body = Encode.to_form_urlencoded Message.jsont message in 4 3 let content_type = "application/x-www-form-urlencoded" in 5 4 let response = ··· 13 12 (Error.make ~code:(Other "json_parse") ~message:msg ()) 14 13 "parsing message response" 15 14 16 - let edit client ~message_id ?content ?topic () = 15 + let get client ~message_id = 16 + Client.request client ~method_:`GET 17 + ~path:("/api/v1/messages/" ^ string_of_int message_id) 18 + () 19 + 20 + let get_raw client ~message_id = 21 + let response_codec = 22 + Jsont.Object.( 23 + map ~kind:"RawMessageResponse" (fun content -> content) 24 + |> mem "raw_content" Jsont.string ~enc:(fun x -> x) 25 + |> finish) 26 + in 27 + let json = 28 + Client.request client ~method_:`GET 29 + ~path:("/api/v1/messages/" ^ string_of_int message_id) 30 + ~params:[ ("apply_markdown", "false") ] 31 + () 32 + in 33 + match Encode.from_json response_codec json with 34 + | Ok content -> content 35 + | Error msg -> 36 + Error.raise_with_context 37 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 38 + "getting raw message %d" message_id 39 + 40 + type anchor = Newest | Oldest | First_unread | Message_id of int 41 + 42 + let anchor_to_string = function 43 + | Newest -> "newest" 44 + | Oldest -> "oldest" 45 + | First_unread -> "first_unread" 46 + | Message_id id -> string_of_int id 47 + 48 + let get_messages client ?anchor ?num_before ?num_after ?narrow ?include_anchor 49 + () = 50 + let params = 51 + List.filter_map Fun.id 52 + [ 53 + Option.map (fun a -> ("anchor", anchor_to_string a)) anchor; 54 + Option.map (fun n -> ("num_before", string_of_int n)) num_before; 55 + Option.map (fun n -> ("num_after", string_of_int n)) num_after; 56 + Option.map 57 + (fun n -> ("narrow", Encode.to_json_string Narrow.list_jsont n)) 58 + narrow; 59 + Option.map 60 + (fun v -> ("include_anchor", string_of_bool v)) 61 + include_anchor; 62 + ] 63 + in 64 + Client.request client ~method_:`GET ~path:"/api/v1/messages" ~params () 65 + 66 + let check_messages_match_narrow client ~message_ids ~narrow = 67 + let params = 68 + [ 69 + ("msg_ids", Encode.to_json_string (Jsont.list Jsont.int) message_ids); 70 + ("narrow", Encode.to_json_string Narrow.list_jsont narrow); 71 + ] 72 + in 73 + Client.request client ~method_:`GET ~path:"/api/v1/messages/matches_narrow" 74 + ~params () 75 + 76 + let get_history client ~message_id = 77 + Client.request client ~method_:`GET 78 + ~path:("/api/v1/messages/" ^ string_of_int message_id ^ "/history") 79 + () 80 + 81 + type propagate_mode = Change_one | Change_later | Change_all 82 + 83 + let propagate_mode_to_string = function 84 + | Change_one -> "change_one" 85 + | Change_later -> "change_later" 86 + | Change_all -> "change_all" 87 + 88 + let edit client ~message_id ?content ?topic ?stream_id ?propagate_mode 89 + ?send_notification_to_old_thread ?send_notification_to_new_thread () = 17 90 let params = 18 - (("message_id", string_of_int message_id) 19 - :: (match content with Some c -> [ ("content", c) ] | None -> [])) 20 - @ match topic with Some t -> [ ("topic", t) ] | None -> [] 91 + List.filter_map Fun.id 92 + [ 93 + Option.map (fun c -> ("content", c)) content; 94 + Option.map (fun t -> ("topic", t)) topic; 95 + Option.map (fun id -> ("stream_id", string_of_int id)) stream_id; 96 + Option.map 97 + (fun m -> ("propagate_mode", propagate_mode_to_string m)) 98 + propagate_mode; 99 + Option.map 100 + (fun v -> ("send_notification_to_old_thread", string_of_bool v)) 101 + send_notification_to_old_thread; 102 + Option.map 103 + (fun v -> ("send_notification_to_new_thread", string_of_bool v)) 104 + send_notification_to_new_thread; 105 + ] 21 106 in 22 107 let _response = 23 108 Client.request client ~method_:`PATCH ··· 34 119 in 35 120 () 36 121 37 - let get client ~message_id = 38 - Client.request client ~method_:`GET 39 - ~path:("/api/v1/messages/" ^ string_of_int message_id) 40 - () 122 + let update_flags client ~messages ~op ~flag = 123 + let op_str = Message_flag.op_to_string op in 124 + let flag_str = Message_flag.to_string flag in 125 + let params = 126 + [ 127 + ("messages", Encode.to_json_string (Jsont.list Jsont.int) messages); 128 + ("op", op_str); 129 + ("flag", flag_str); 130 + ] 131 + in 132 + let _response = 133 + Client.request client ~method_:`POST ~path:"/api/v1/messages/flags" ~params 134 + () 135 + in 136 + () 41 137 42 - let get_messages client ?anchor ?num_before ?num_after ?narrow () = 138 + let mark_all_as_read client = 139 + let _response = 140 + Client.request client ~method_:`POST ~path:"/api/v1/mark_all_as_read" () 141 + in 142 + () 143 + 144 + let mark_stream_as_read client ~stream_id = 145 + let params = [ ("stream_id", string_of_int stream_id) ] in 146 + let _response = 147 + Client.request client ~method_:`POST ~path:"/api/v1/mark_stream_as_read" 148 + ~params () 149 + in 150 + () 151 + 152 + let mark_topic_as_read client ~stream_id ~topic = 43 153 let params = 44 - ((match anchor with Some a -> [ ("anchor", a) ] | None -> []) 45 - @ (match num_before with 46 - | Some n -> [ ("num_before", string_of_int n) ] 47 - | None -> []) 48 - @ (match num_after with 49 - | Some n -> [ ("num_after", string_of_int n) ] 50 - | None -> [])) 51 - @ 52 - match narrow with 53 - | Some n -> 54 - List.mapi (fun i s -> ("narrow[" ^ string_of_int i ^ "]", s)) n 55 - | None -> [] 154 + [ ("stream_id", string_of_int stream_id); ("topic_name", topic) ] 155 + in 156 + let _response = 157 + Client.request client ~method_:`POST ~path:"/api/v1/mark_topic_as_read" 158 + ~params () 56 159 in 57 - Client.request client ~method_:`GET ~path:"/api/v1/messages" ~params () 160 + () 161 + 162 + type emoji_type = Unicode_emoji | Realm_emoji | Zulip_extra_emoji 163 + 164 + let emoji_type_to_string = function 165 + | Unicode_emoji -> "unicode_emoji" 166 + | Realm_emoji -> "realm_emoji" 167 + | Zulip_extra_emoji -> "zulip_extra_emoji" 58 168 59 - let add_reaction client ~message_id ~emoji_name = 60 - let params = [ ("emoji_name", emoji_name) ] in 169 + let add_reaction client ~message_id ~emoji_name ?emoji_code ?reaction_type () = 170 + let params = 171 + [ ("emoji_name", emoji_name) ] 172 + @ List.filter_map Fun.id 173 + [ 174 + Option.map (fun c -> ("emoji_code", c)) emoji_code; 175 + Option.map 176 + (fun t -> ("reaction_type", emoji_type_to_string t)) 177 + reaction_type; 178 + ] 179 + in 61 180 let _response = 62 181 Client.request client ~method_:`POST 63 182 ~path:("/api/v1/messages/" ^ string_of_int message_id ^ "/reactions") ··· 65 184 in 66 185 () 67 186 68 - let remove_reaction client ~message_id ~emoji_name = 69 - let params = [ ("emoji_name", emoji_name) ] in 187 + let remove_reaction client ~message_id ~emoji_name ?emoji_code ?reaction_type () 188 + = 189 + let params = 190 + [ ("emoji_name", emoji_name) ] 191 + @ List.filter_map Fun.id 192 + [ 193 + Option.map (fun c -> ("emoji_code", c)) emoji_code; 194 + Option.map 195 + (fun t -> ("reaction_type", emoji_type_to_string t)) 196 + reaction_type; 197 + ] 198 + in 70 199 let _response = 71 200 Client.request client ~method_:`DELETE 72 201 ~path:("/api/v1/messages/" ^ string_of_int message_id ^ "/reactions") ··· 74 203 in 75 204 () 76 205 206 + let render client ~content = 207 + let params = [ ("content", content) ] in 208 + let response_codec = 209 + Jsont.Object.( 210 + map ~kind:"RenderResponse" (fun rendered -> rendered) 211 + |> mem "rendered" Jsont.string ~enc:(fun x -> x) 212 + |> finish) 213 + in 214 + let json = 215 + Client.request client ~method_:`POST ~path:"/api/v1/messages/render" ~params 216 + () 217 + in 218 + match Encode.from_json response_codec json with 219 + | Ok rendered -> rendered 220 + | Error msg -> 221 + Error.raise_with_context 222 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 223 + "rendering message" 224 + 77 225 let upload_file _client ~filename:_ = 78 226 (* TODO: Implement file upload using multipart/form-data *) 79 227 Error.raise 80 228 (Error.make ~code:(Other "not_implemented") 81 229 ~message:"File upload not yet implemented" ()) 230 + 231 + let get_scheduled client = 232 + Client.request client ~method_:`GET ~path:"/api/v1/scheduled_messages" () 233 + 234 + let delete_scheduled client ~scheduled_message_id = 235 + let _response = 236 + Client.request client ~method_:`DELETE 237 + ~path: 238 + ("/api/v1/scheduled_messages/" ^ string_of_int scheduled_message_id) 239 + () 240 + in 241 + ()
+154 -20
lib/zulip/messages.mli
··· 3 3 All functions raise [Eio.Io] with [Error.E error] on failure. 4 4 Context is automatically added indicating the operation being performed. *) 5 5 6 + (** {1 Sending Messages} *) 7 + 6 8 val send : Client.t -> Message.t -> Message_response.t 7 9 (** Send a message. 8 10 @raise Eio.Io on failure *) 9 11 10 - val edit : 11 - Client.t -> message_id:int -> ?content:string -> ?topic:string -> unit -> unit 12 - (** Edit a message's content or topic. 13 - @raise Eio.Io on failure *) 14 - 15 - val delete : Client.t -> message_id:int -> unit 16 - (** Delete a message. 17 - @raise Eio.Io on failure *) 12 + (** {1 Reading Messages} *) 18 13 19 14 val get : Client.t -> message_id:int -> Jsont.json 20 15 (** Get a single message by ID. 16 + Returns the full message object. 21 17 @raise Eio.Io on failure *) 22 18 19 + val get_raw : Client.t -> message_id:int -> string 20 + (** Get the raw Markdown content of a message. 21 + @raise Eio.Io on failure *) 22 + 23 + (** Anchor points for message pagination. *) 24 + type anchor = 25 + | Newest (** Start from the newest message *) 26 + | Oldest (** Start from the oldest message *) 27 + | First_unread (** Start from first unread message *) 28 + | Message_id of int (** Start from a specific message ID *) 29 + 23 30 val get_messages : 24 31 Client.t -> 25 - ?anchor:string -> 32 + ?anchor:anchor -> 26 33 ?num_before:int -> 27 34 ?num_after:int -> 28 - ?narrow:string list -> 35 + ?narrow:Narrow.t list -> 36 + ?include_anchor:bool -> 29 37 unit -> 30 38 Jsont.json 31 39 (** Get multiple messages with optional filtering. 40 + 41 + @param anchor Where to start fetching (default: [Newest]) 42 + @param num_before Number of messages before anchor (default: 0) 43 + @param num_after Number of messages after anchor (default: 0) 44 + @param narrow Filter criteria (see {!Narrow}) 45 + @param include_anchor Include the anchor message in results 32 46 @raise Eio.Io on failure *) 33 47 34 - val add_reaction : Client.t -> message_id:int -> emoji_name:string -> unit 48 + val check_messages_match_narrow : 49 + Client.t -> message_ids:int list -> narrow:Narrow.t list -> Jsont.json 50 + (** Check if messages match a narrow filter. 51 + Returns which of the given messages match the narrow. 52 + @raise Eio.Io on failure *) 53 + 54 + (** {1 Message History} *) 55 + 56 + val get_history : Client.t -> message_id:int -> Jsont.json 57 + (** Get the edit history of a message. 58 + @raise Eio.Io on failure *) 59 + 60 + (** {1 Editing Messages} *) 61 + 62 + (** Propagation mode for topic/stream changes. *) 63 + type propagate_mode = 64 + | Change_one (** Only change this message *) 65 + | Change_later (** Change this and subsequent messages *) 66 + | Change_all (** Change all messages in the topic *) 67 + 68 + val edit : 69 + Client.t -> 70 + message_id:int -> 71 + ?content:string -> 72 + ?topic:string -> 73 + ?stream_id:int -> 74 + ?propagate_mode:propagate_mode -> 75 + ?send_notification_to_old_thread:bool -> 76 + ?send_notification_to_new_thread:bool -> 77 + unit -> 78 + unit 79 + (** Edit a message's content, topic, or stream. 80 + 81 + @param content New message content 82 + @param topic New topic name 83 + @param stream_id Move message to this stream 84 + @param propagate_mode How to handle topic/stream changes for other messages 85 + @param send_notification_to_old_thread Notify in old location 86 + @param send_notification_to_new_thread Notify in new location 87 + @raise Eio.Io on failure *) 88 + 89 + (** {1 Deleting Messages} *) 90 + 91 + val delete : Client.t -> message_id:int -> unit 92 + (** Delete a message. 93 + @raise Eio.Io on failure *) 94 + 95 + (** {1 Message Flags} *) 96 + 97 + val update_flags : 98 + Client.t -> 99 + messages:int list -> 100 + op:Message_flag.op -> 101 + flag:Message_flag.modifiable -> 102 + unit 103 + (** Update flags on a list of messages. 104 + 105 + @param messages List of message IDs to update 106 + @param op Whether to add or remove the flag 107 + @param flag The flag to update 108 + @raise Eio.Io on failure 109 + 110 + {b Example:} 111 + {[ 112 + (* Mark messages as read *) 113 + Messages.update_flags client 114 + ~messages:[123; 456; 789] 115 + ~op:Add 116 + ~flag:`Read 117 + ]} *) 118 + 119 + val mark_all_as_read : Client.t -> unit 120 + (** Mark all messages as read. 121 + @raise Eio.Io on failure *) 122 + 123 + val mark_stream_as_read : Client.t -> stream_id:int -> unit 124 + (** Mark all messages in a stream as read. 125 + @raise Eio.Io on failure *) 126 + 127 + val mark_topic_as_read : Client.t -> stream_id:int -> topic:string -> unit 128 + (** Mark all messages in a topic as read. 129 + @raise Eio.Io on failure *) 130 + 131 + (** {1 Reactions} *) 132 + 133 + (** Type of emoji for reactions. *) 134 + type emoji_type = 135 + | Unicode_emoji (** Standard Unicode emoji *) 136 + | Realm_emoji (** Custom organization emoji *) 137 + | Zulip_extra_emoji (** Zulip-specific emoji *) 138 + 139 + val add_reaction : 140 + Client.t -> 141 + message_id:int -> 142 + emoji_name:string -> 143 + ?emoji_code:string -> 144 + ?reaction_type:emoji_type -> 145 + unit -> 146 + unit 35 147 (** Add an emoji reaction to a message. 36 148 37 - @param client The Zulip client 38 - @param message_id The message ID to react to 39 - @param emoji_name The emoji name (e.g., "thumbs_up", "heart", "rocket") 149 + @param emoji_name The emoji name (e.g., "thumbs_up", "heart") 150 + @param emoji_code The emoji code (optional, required for realm emoji) 151 + @param reaction_type The type of emoji (default: [Unicode_emoji]) 40 152 @raise Eio.Io on failure 41 153 42 154 {b Example:} 43 155 {[ 44 - Messages.add_reaction client ~message_id:12345 ~emoji_name:"thumbs_up" 156 + Messages.add_reaction client ~message_id:12345 ~emoji_name:"thumbs_up" () 45 157 ]} *) 46 158 47 - val remove_reaction : Client.t -> message_id:int -> emoji_name:string -> unit 159 + val remove_reaction : 160 + Client.t -> 161 + message_id:int -> 162 + emoji_name:string -> 163 + ?emoji_code:string -> 164 + ?reaction_type:emoji_type -> 165 + unit -> 166 + unit 48 167 (** Remove an emoji reaction from a message. 168 + @raise Eio.Io on failure *) 49 169 50 - @param client The Zulip client 51 - @param message_id The message ID 52 - @param emoji_name The emoji name to remove 170 + (** {1 Rendering} *) 171 + 172 + val render : Client.t -> content:string -> string 173 + (** Render message content as HTML. 174 + Useful for previewing how a message will appear. 175 + @return The rendered HTML 53 176 @raise Eio.Io on failure *) 177 + 178 + (** {1 File Uploads} *) 54 179 55 180 val upload_file : Client.t -> filename:string -> string 56 181 (** Upload a file to Zulip. 57 182 58 - @param client The Zulip client 59 183 @param filename The path to the file to upload 60 184 @return The Zulip URL for the uploaded file 61 185 @raise Eio.Io on failure ··· 67 191 ~content:("Check out this image: " ^ uri) () in 68 192 Messages.send client msg 69 193 ]} *) 194 + 195 + (** {1 Scheduled Messages} *) 196 + 197 + val get_scheduled : Client.t -> Jsont.json 198 + (** Get all scheduled messages for the current user. 199 + @raise Eio.Io on failure *) 200 + 201 + val delete_scheduled : Client.t -> scheduled_message_id:int -> unit 202 + (** Delete a scheduled message. 203 + @raise Eio.Io on failure *)
+116
lib/zulip/narrow.ml
··· 1 + type t = { 2 + operator : string; 3 + operand : [ `String of string | `Int of int | `Strings of string list ]; 4 + negated : bool; 5 + } 6 + 7 + let make ?(negated = false) operator operand = { operator; operand; negated } 8 + 9 + let stream name = make "stream" (`String name) 10 + let stream_id id = make "stream" (`Int id) 11 + let topic name = make "topic" (`String name) 12 + let channel = stream 13 + 14 + let sender email = make "sender" (`String email) 15 + let sender_id id = make "sender" (`Int id) 16 + 17 + type is_operand = 18 + [ `Alerted | `Dm | `Mentioned | `Private | `Resolved | `Starred | `Unread ] 19 + 20 + let is_operand_to_string = function 21 + | `Alerted -> "alerted" 22 + | `Dm -> "dm" 23 + | `Mentioned -> "mentioned" 24 + | `Private -> "private" 25 + | `Resolved -> "resolved" 26 + | `Starred -> "starred" 27 + | `Unread -> "unread" 28 + 29 + let is operand = make "is" (`String (is_operand_to_string operand)) 30 + 31 + type has_operand = [ `Attachment | `Image | `Link | `Reaction ] 32 + 33 + let has_operand_to_string = function 34 + | `Attachment -> "attachment" 35 + | `Image -> "image" 36 + | `Link -> "link" 37 + | `Reaction -> "reaction" 38 + 39 + let has operand = make "has" (`String (has_operand_to_string operand)) 40 + 41 + let search query = make "search" (`String query) 42 + 43 + let id msg_id = make "id" (`Int msg_id) 44 + let near msg_id = make "near" (`Int msg_id) 45 + 46 + let dm emails = make "dm" (`Strings emails) 47 + let dm_including email = make "dm-including" (`String email) 48 + let group_pm_with = dm_including 49 + 50 + let not_ filter = { filter with negated = true } 51 + 52 + let to_json filters = 53 + let meta = Jsont.Meta.none in 54 + let make_string s = Jsont.String (s, meta) in 55 + let make_member name value = ((name, meta), value) in 56 + Jsont.Array 57 + (List.map 58 + (fun f -> 59 + let operand_json = 60 + match f.operand with 61 + | `String s -> make_string s 62 + | `Int i -> Jsont.Number (float_of_int i, meta) 63 + | `Strings ss -> Jsont.Array (List.map make_string ss, meta) 64 + in 65 + let fields = 66 + [ make_member "operator" (make_string f.operator); 67 + make_member "operand" operand_json ] 68 + in 69 + let fields = 70 + if f.negated then make_member "negated" (Jsont.Bool (true, meta)) :: fields else fields 71 + in 72 + Jsont.Object (fields, meta)) 73 + filters, meta) 74 + 75 + let operand_to_json = function 76 + | `String s -> Jsont.String (s, Jsont.Meta.none) 77 + | `Int i -> Jsont.Number (float_of_int i, Jsont.Meta.none) 78 + | `Strings ss -> 79 + Jsont.Array 80 + (List.map (fun s -> Jsont.String (s, Jsont.Meta.none)) ss, Jsont.Meta.none) 81 + 82 + let operand_of_json = function 83 + | Jsont.String (s, _) -> `String s 84 + | Jsont.Number (f, _) -> `Int (int_of_float f) 85 + | Jsont.Array (items, _) -> 86 + `Strings 87 + (List.filter_map 88 + (function Jsont.String (s, _) -> Some s | _ -> None) 89 + items) 90 + | _ -> `String "" 91 + 92 + let operand_jsont = 93 + Jsont.json |> Jsont.map ~dec:operand_of_json ~enc:operand_to_json 94 + 95 + let jsont = 96 + let kind = "Narrow" in 97 + let doc = "A narrow filter" in 98 + let make operator operand negated = { operator; operand; negated } in 99 + Jsont.Object.map ~kind ~doc make 100 + |> Jsont.Object.mem "operator" Jsont.string ~enc:(fun t -> t.operator) 101 + |> Jsont.Object.mem "operand" operand_jsont ~enc:(fun t -> t.operand) 102 + |> Jsont.Object.mem "negated" Jsont.bool ~dec_absent:false ~enc:(fun t -> 103 + t.negated) 104 + |> Jsont.Object.finish 105 + 106 + let list_jsont = Jsont.list jsont 107 + 108 + let pp fmt t = 109 + let neg = if t.negated then "-" else "" in 110 + let operand = 111 + match t.operand with 112 + | `String s -> s 113 + | `Int i -> string_of_int i 114 + | `Strings ss -> String.concat "," ss 115 + in 116 + Format.fprintf fmt "%s%s:%s" neg t.operator operand
+112
lib/zulip/narrow.mli
··· 1 + (** Type-safe narrow filters for message queries. 2 + 3 + Narrow filters constrain which messages are returned by the 4 + [Messages.get_messages] endpoint. This module provides a type-safe 5 + interface for constructing these filters. 6 + 7 + Example: 8 + {[ 9 + let narrow = Narrow.[ 10 + stream "general"; 11 + topic "greetings"; 12 + is `Unread; 13 + ] in 14 + Messages.get_messages client ~narrow () 15 + ]} *) 16 + 17 + (** {1 Filter Type} *) 18 + 19 + type t 20 + (** A single narrow filter clause. *) 21 + 22 + (** {1 Stream/Channel Filters} *) 23 + 24 + val stream : string -> t 25 + (** [stream name] filters to messages in the given stream/channel. *) 26 + 27 + val stream_id : int -> t 28 + (** [stream_id id] filters to messages in the stream with the given ID. *) 29 + 30 + val topic : string -> t 31 + (** [topic name] filters to messages with the given topic/subject. *) 32 + 33 + val channel : string -> t 34 + (** Alias for [stream]. *) 35 + 36 + (** {1 Sender Filters} *) 37 + 38 + val sender : string -> t 39 + (** [sender email] filters to messages from the given sender. *) 40 + 41 + val sender_id : int -> t 42 + (** [sender_id id] filters to messages from the sender with the given user ID. *) 43 + 44 + (** {1 Message Property Filters} *) 45 + 46 + type is_operand = 47 + [ `Alerted (** Messages containing alert words *) 48 + | `Dm (** Direct messages (private messages) *) 49 + | `Mentioned (** Messages where user is mentioned *) 50 + | `Private (** Alias for [`Dm] *) 51 + | `Resolved (** Topics marked as resolved *) 52 + | `Starred (** Starred messages *) 53 + | `Unread (** Unread messages *) 54 + ] 55 + 56 + val is : is_operand -> t 57 + (** [is operand] filters by message property. *) 58 + 59 + type has_operand = 60 + [ `Attachment (** Messages with file attachments *) 61 + | `Image (** Messages containing images *) 62 + | `Link (** Messages containing links *) 63 + | `Reaction (** Messages with emoji reactions *) 64 + ] 65 + 66 + val has : has_operand -> t 67 + (** [has operand] filters to messages that have the given content type. *) 68 + 69 + (** {1 Search} *) 70 + 71 + val search : string -> t 72 + (** [search query] full-text search within messages. *) 73 + 74 + (** {1 Message ID Filters} *) 75 + 76 + val id : int -> t 77 + (** [id msg_id] filters to the specific message with the given ID. *) 78 + 79 + val near : int -> t 80 + (** [near msg_id] centers results around the given message ID. *) 81 + 82 + (** {1 Direct Message Filters} *) 83 + 84 + val dm : string list -> t 85 + (** [dm emails] filters to direct messages with exactly these participants. *) 86 + 87 + val dm_including : string -> t 88 + (** [dm_including email] filters to direct messages that include this user. *) 89 + 90 + val group_pm_with : string -> t 91 + (** [group_pm_with email] filters to group DMs including this user (deprecated, use [dm_including]). *) 92 + 93 + (** {1 Negation} *) 94 + 95 + val not_ : t -> t 96 + (** [not_ filter] negates a filter. 97 + Example: [not_ (stream "general")] excludes the "general" stream. *) 98 + 99 + (** {1 Encoding} *) 100 + 101 + val to_json : t list -> Jsont.json 102 + (** Encode a list of filters to JSON for the API request. *) 103 + 104 + val jsont : t Jsont.t 105 + (** Jsont codec for a single filter. *) 106 + 107 + val list_jsont : t list Jsont.t 108 + (** Jsont codec for a list of filters. *) 109 + 110 + (** {1 Pretty Printing} *) 111 + 112 + val pp : Format.formatter -> t -> unit
+75
lib/zulip/presence.mli
··· 1 + (** User presence information for the Zulip API. 2 + 3 + Track online/offline status of users in the organization. *) 4 + 5 + (** {1 Presence Types} *) 6 + 7 + type status = 8 + | Active (** User is currently active *) 9 + | Idle (** User is idle *) 10 + | Offline (** User is offline *) 11 + 12 + type client_presence = { 13 + status : status; 14 + timestamp : float; 15 + client : string; (** Client name (e.g., "website", "ZulipMobile") *) 16 + pushable : bool; (** Whether push notifications can be sent *) 17 + } 18 + (** Presence information from a single client. *) 19 + 20 + type user_presence = { 21 + aggregated : client_presence option; (** Aggregated presence across clients *) 22 + clients : (string * client_presence) list; (** Per-client presence *) 23 + } 24 + (** A user's presence information. *) 25 + 26 + (** {1 Querying Presence} *) 27 + 28 + val get_user : Client.t -> user_id:int -> user_presence 29 + (** Get presence information for a specific user. 30 + @raise Eio.Io on failure *) 31 + 32 + val get_user_by_email : Client.t -> email:string -> user_presence 33 + (** Get presence information for a user by email. 34 + @raise Eio.Io on failure *) 35 + 36 + val get_all : Client.t -> (int * user_presence) list 37 + (** Get presence information for all users in the organization. 38 + Returns a list of (user_id, presence) pairs. 39 + @raise Eio.Io on failure *) 40 + 41 + (** {1 Updating Presence} *) 42 + 43 + val update : 44 + Client.t -> 45 + status:status -> 46 + ?ping_only:bool -> 47 + ?new_user_input:bool -> 48 + unit -> 49 + unit 50 + (** Update the current user's presence status. 51 + 52 + @param status The presence status to set 53 + @param ping_only Only send a ping without changing status 54 + @param new_user_input Whether there was new user input 55 + @raise Eio.Io on failure *) 56 + 57 + (** {1 JSON Codecs} *) 58 + 59 + val status_jsont : status Jsont.t 60 + 61 + val client_presence_jsont : client_presence Jsont.t 62 + 63 + val user_presence_jsont : user_presence Jsont.t 64 + 65 + (** {1 Conversion} *) 66 + 67 + val status_to_string : status -> string 68 + 69 + val status_of_string : string -> status option 70 + 71 + (** {1 Pretty Printing} *) 72 + 73 + val pp_status : Format.formatter -> status -> unit 74 + 75 + val pp_user_presence : Format.formatter -> user_presence -> unit
+176
lib/zulip/server.mli
··· 1 + (** Server information and settings for the Zulip API. 2 + 3 + This module provides access to server-level information including 4 + version, feature level, and authentication methods. *) 5 + 6 + (** {1 Server Settings} *) 7 + 8 + type authentication_method = { 9 + password : bool; (** Password authentication enabled *) 10 + dev : bool; (** Development authentication enabled *) 11 + email : bool; (** Email authentication enabled *) 12 + ldap : bool; (** LDAP authentication enabled *) 13 + remoteuser : bool; (** Remote user authentication enabled *) 14 + github : bool; (** GitHub OAuth enabled *) 15 + azuread : bool; (** Azure AD OAuth enabled *) 16 + gitlab : bool; (** GitLab OAuth enabled *) 17 + apple : bool; (** Apple OAuth enabled *) 18 + google : bool; (** Google OAuth enabled *) 19 + saml : bool; (** SAML SSO enabled *) 20 + openid_connect : bool; (** OpenID Connect enabled *) 21 + } 22 + (** Enabled authentication methods on the server. *) 23 + 24 + type external_authentication_method = { 25 + name : string; (** Method name *) 26 + display_name : string; (** Display name for UI *) 27 + display_icon : string option; (** Icon URL *) 28 + login_url : string; (** Login URL *) 29 + signup_url : string; (** Signup URL *) 30 + } 31 + (** External authentication method configuration. *) 32 + 33 + type t = { 34 + zulip_version : string; (** Server version string *) 35 + zulip_feature_level : int; (** API feature level *) 36 + zulip_merge_base : string option; (** Git merge base (for dev servers) *) 37 + push_notifications_enabled : bool; (** Push notifications available *) 38 + is_incompatible : bool; (** Client incompatible with server *) 39 + email_auth_enabled : bool; (** Email auth enabled *) 40 + require_email_format_usernames : bool; (** Usernames must be emails *) 41 + realm_uri : string; (** Organization URL *) 42 + realm_name : string; (** Organization name *) 43 + realm_icon : string; (** Organization icon URL *) 44 + realm_description : string; (** Organization description *) 45 + realm_web_public_access_enabled : bool; (** Web public access enabled *) 46 + authentication_methods : authentication_method; 47 + external_authentication_methods : external_authentication_method list; 48 + } 49 + (** Server settings response. *) 50 + 51 + val get_settings : Client.t -> t 52 + (** Get server settings. 53 + @raise Eio.Io on failure *) 54 + 55 + val get_settings_json : Client.t -> Jsont.json 56 + (** Get server settings as raw JSON. 57 + @raise Eio.Io on failure *) 58 + 59 + (** {1 Feature Level Checks} *) 60 + 61 + val feature_level : Client.t -> int 62 + (** Get the server's feature level. 63 + Useful for checking API compatibility. 64 + @raise Eio.Io on failure *) 65 + 66 + val supports_feature : Client.t -> level:int -> bool 67 + (** Check if the server supports a given feature level. 68 + @raise Eio.Io on failure *) 69 + 70 + (** {1 Linkifiers} *) 71 + 72 + type linkifier = { 73 + id : int; (** Linkifier ID *) 74 + pattern : string; (** Regex pattern *) 75 + url_template : string; (** URL template *) 76 + } 77 + (** A realm linkifier (auto-link rule). *) 78 + 79 + val get_linkifiers : Client.t -> linkifier list 80 + (** Get all linkifiers for the organization. 81 + @raise Eio.Io on failure *) 82 + 83 + val add_linkifier : Client.t -> pattern:string -> url_template:string -> int 84 + (** Add a new linkifier. 85 + @return The ID of the created linkifier 86 + @raise Eio.Io on failure *) 87 + 88 + val update_linkifier : 89 + Client.t -> filter_id:int -> pattern:string -> url_template:string -> unit 90 + (** Update an existing linkifier. 91 + @raise Eio.Io on failure *) 92 + 93 + val delete_linkifier : Client.t -> filter_id:int -> unit 94 + (** Delete a linkifier. 95 + @raise Eio.Io on failure *) 96 + 97 + (** {1 Custom Emoji} *) 98 + 99 + type emoji = { 100 + id : string; (** Emoji ID *) 101 + name : string; (** Emoji name *) 102 + source_url : string; (** Source image URL *) 103 + deactivated : bool; (** Whether emoji is deactivated *) 104 + author_id : int option; (** User ID of uploader *) 105 + } 106 + (** A custom realm emoji. *) 107 + 108 + val get_emoji : Client.t -> emoji list 109 + (** Get all custom emoji for the organization. 110 + @raise Eio.Io on failure *) 111 + 112 + val upload_emoji : Client.t -> name:string -> filename:string -> unit 113 + (** Upload a new custom emoji. 114 + @raise Eio.Io on failure *) 115 + 116 + val deactivate_emoji : Client.t -> name:string -> unit 117 + (** Deactivate a custom emoji. 118 + @raise Eio.Io on failure *) 119 + 120 + (** {1 Profile Fields} *) 121 + 122 + type profile_field_type = 123 + | Short_text (** Single line text *) 124 + | Long_text (** Multi-line text *) 125 + | Choice (** Select from options *) 126 + | Date (** Date picker *) 127 + | Link (** URL *) 128 + | User (** User reference *) 129 + | External_account (** External account link *) 130 + | Pronouns (** Pronouns field *) 131 + 132 + type profile_field = { 133 + id : int; 134 + field_type : profile_field_type; 135 + order : int; 136 + name : string; 137 + hint : string; 138 + field_data : Jsont.json; 139 + display_in_profile_summary : bool option; 140 + } 141 + (** A custom profile field definition. *) 142 + 143 + val get_profile_fields : Client.t -> profile_field list 144 + (** Get all custom profile fields. 145 + @raise Eio.Io on failure *) 146 + 147 + val create_profile_field : 148 + Client.t -> 149 + field_type:profile_field_type -> 150 + name:string -> 151 + ?hint:string -> 152 + ?field_data:Jsont.json -> 153 + unit -> 154 + int 155 + (** Create a new custom profile field. 156 + @return The ID of the created field 157 + @raise Eio.Io on failure *) 158 + 159 + val update_profile_field : 160 + Client.t -> 161 + field_id:int -> 162 + ?name:string -> 163 + ?hint:string -> 164 + ?field_data:Jsont.json -> 165 + unit -> 166 + unit 167 + (** Update a custom profile field. 168 + @raise Eio.Io on failure *) 169 + 170 + val delete_profile_field : Client.t -> field_id:int -> unit 171 + (** Delete a custom profile field. 172 + @raise Eio.Io on failure *) 173 + 174 + val reorder_profile_fields : Client.t -> order:int list -> unit 175 + (** Reorder custom profile fields. 176 + @raise Eio.Io on failure *)
+44
lib/zulip/typing.mli
··· 1 + (** Typing notifications for the Zulip API. 2 + 3 + Send typing start/stop notifications to indicate that the user 4 + is composing a message. *) 5 + 6 + (** {1 Typing Status Operations} *) 7 + 8 + type op = 9 + | Start (** User started typing *) 10 + | Stop (** User stopped typing *) 11 + 12 + (** {1 Direct Messages} *) 13 + 14 + val set_dm : 15 + Client.t -> op:op -> user_ids:int list -> unit 16 + (** Set typing status for a direct message conversation. 17 + 18 + @param op Whether typing has started or stopped 19 + @param user_ids List of user IDs in the conversation 20 + @raise Eio.Io on failure *) 21 + 22 + (** {1 Channel Messages} *) 23 + 24 + val set_channel : 25 + Client.t -> op:op -> stream_id:int -> topic:string -> unit 26 + (** Set typing status in a channel topic. 27 + 28 + @param op Whether typing has started or stopped 29 + @param stream_id The channel's stream ID 30 + @param topic The topic name 31 + @raise Eio.Io on failure *) 32 + 33 + (** {1 Legacy API} *) 34 + 35 + val set : 36 + Client.t -> 37 + op:op -> 38 + to_:[ `User_ids of int list | `Stream of int * string ] -> 39 + unit 40 + (** Set typing status (unified interface). 41 + 42 + @param op Whether typing has started or stopped 43 + @param to_ Either user IDs for DM or (stream_id, topic) for channel 44 + @raise Eio.Io on failure *)
+85 -7
lib/zulip/user.ml
··· 5 5 delivery_email : string option; 6 6 is_active : bool; 7 7 is_admin : bool; 8 + is_owner : bool; 9 + is_guest : bool; 10 + is_billing_admin : bool; 8 11 is_bot : bool; 12 + bot_type : int option; 13 + bot_owner_id : int option; 14 + avatar_url : string option; 15 + avatar_version : int option; 16 + timezone : string option; 17 + date_joined : string option; 18 + role : int option; 9 19 } 10 20 21 + let role_owner = 100 22 + let role_admin = 200 23 + let role_moderator = 300 24 + let role_member = 400 25 + let role_guest = 600 26 + 11 27 let create ~email ~full_name ?user_id ?delivery_email ?(is_active = true) 12 - ?(is_admin = false) ?(is_bot = false) () = 13 - { email; full_name; user_id; delivery_email; is_active; is_admin; is_bot } 28 + ?(is_admin = false) ?(is_owner = false) ?(is_guest = false) 29 + ?(is_billing_admin = false) ?(is_bot = false) ?bot_type ?bot_owner_id 30 + ?avatar_url ?avatar_version ?timezone ?date_joined ?role () = 31 + { 32 + email; 33 + full_name; 34 + user_id; 35 + delivery_email; 36 + is_active; 37 + is_admin; 38 + is_owner; 39 + is_guest; 40 + is_billing_admin; 41 + is_bot; 42 + bot_type; 43 + bot_owner_id; 44 + avatar_url; 45 + avatar_version; 46 + timezone; 47 + date_joined; 48 + role; 49 + } 14 50 15 51 let email t = t.email 16 52 let full_name t = t.full_name ··· 18 54 let delivery_email t = t.delivery_email 19 55 let is_active t = t.is_active 20 56 let is_admin t = t.is_admin 57 + let is_owner t = t.is_owner 58 + let is_guest t = t.is_guest 59 + let is_billing_admin t = t.is_billing_admin 21 60 let is_bot t = t.is_bot 61 + let bot_type t = t.bot_type 62 + let bot_owner_id t = t.bot_owner_id 63 + let avatar_url t = t.avatar_url 64 + let avatar_version t = t.avatar_version 65 + let timezone t = t.timezone 66 + let date_joined t = t.date_joined 67 + let role t = t.role 22 68 23 69 (* Jsont codec for user *) 24 70 let jsont = 25 71 let kind = "User" in 26 72 let doc = "A Zulip user" in 27 - let make email full_name user_id delivery_email is_active is_admin is_bot = 28 - { email; full_name; user_id; delivery_email; is_active; is_admin; is_bot } 73 + let make email full_name user_id delivery_email is_active is_admin is_owner 74 + is_guest is_billing_admin is_bot bot_type bot_owner_id avatar_url 75 + avatar_version timezone date_joined role = 76 + { 77 + email; 78 + full_name; 79 + user_id; 80 + delivery_email; 81 + is_active; 82 + is_admin; 83 + is_owner; 84 + is_guest; 85 + is_billing_admin; 86 + is_bot; 87 + bot_type; 88 + bot_owner_id; 89 + avatar_url; 90 + avatar_version; 91 + timezone; 92 + date_joined; 93 + role; 94 + } 29 95 in 30 96 Jsont.Object.map ~kind ~doc make 31 97 |> Jsont.Object.mem "email" Jsont.string ~enc:email 32 98 |> Jsont.Object.mem "full_name" Jsont.string ~enc:full_name 33 99 |> Jsont.Object.opt_mem "user_id" Jsont.int ~enc:user_id 34 100 |> Jsont.Object.opt_mem "delivery_email" Jsont.string ~enc:delivery_email 35 - |> Jsont.Object.mem "is_active" Jsont.bool ~enc:is_active 36 - |> Jsont.Object.mem "is_admin" Jsont.bool ~enc:is_admin 37 - |> Jsont.Object.mem "is_bot" Jsont.bool ~enc:is_bot 101 + |> Jsont.Object.mem "is_active" Jsont.bool ~dec_absent:true ~enc:is_active 102 + |> Jsont.Object.mem "is_admin" Jsont.bool ~dec_absent:false ~enc:is_admin 103 + |> Jsont.Object.mem "is_owner" Jsont.bool ~dec_absent:false ~enc:is_owner 104 + |> Jsont.Object.mem "is_guest" Jsont.bool ~dec_absent:false ~enc:is_guest 105 + |> Jsont.Object.mem "is_billing_admin" Jsont.bool 106 + ~dec_absent:false 107 + ~enc:is_billing_admin 108 + |> Jsont.Object.mem "is_bot" Jsont.bool ~dec_absent:false ~enc:is_bot 109 + |> Jsont.Object.opt_mem "bot_type" Jsont.int ~enc:bot_type 110 + |> Jsont.Object.opt_mem "bot_owner_id" Jsont.int ~enc:bot_owner_id 111 + |> Jsont.Object.opt_mem "avatar_url" Jsont.string ~enc:avatar_url 112 + |> Jsont.Object.opt_mem "avatar_version" Jsont.int ~enc:avatar_version 113 + |> Jsont.Object.opt_mem "timezone" Jsont.string ~enc:timezone 114 + |> Jsont.Object.opt_mem "date_joined" Jsont.string ~enc:date_joined 115 + |> Jsont.Object.opt_mem "role" Jsont.int ~enc:role 38 116 |> Jsont.Object.finish 39 117 40 118 let pp fmt t =
+73 -1
lib/zulip/user.mli
··· 3 3 This module represents user information from the Zulip API. 4 4 Use {!jsont} with Bytesrw-eio for wire serialization. *) 5 5 6 + (** {1 User Type} *) 7 + 6 8 type t 9 + (** A Zulip user. *) 10 + 11 + (** {1 Construction} *) 7 12 8 13 val create : 9 14 email:string -> ··· 12 17 ?delivery_email:string -> 13 18 ?is_active:bool -> 14 19 ?is_admin:bool -> 20 + ?is_owner:bool -> 21 + ?is_guest:bool -> 22 + ?is_billing_admin:bool -> 15 23 ?is_bot:bool -> 24 + ?bot_type:int -> 25 + ?bot_owner_id:int -> 26 + ?avatar_url:string -> 27 + ?avatar_version:int -> 28 + ?timezone:string -> 29 + ?date_joined:string -> 30 + ?role:int -> 16 31 unit -> 17 32 t 18 33 34 + (** {1 Accessors} *) 35 + 19 36 val email : t -> string 37 + (** User's email address (may be delivery_email if visible). *) 38 + 20 39 val full_name : t -> string 40 + (** User's full display name. *) 41 + 21 42 val user_id : t -> int option 43 + (** Server-assigned user ID. *) 44 + 22 45 val delivery_email : t -> string option 46 + (** User's actual email (if visible to current user). *) 47 + 23 48 val is_active : t -> bool 49 + (** Whether the user account is active. *) 50 + 24 51 val is_admin : t -> bool 52 + (** Whether the user is an organization admin. *) 53 + 54 + val is_owner : t -> bool 55 + (** Whether the user is an organization owner. *) 56 + 57 + val is_guest : t -> bool 58 + (** Whether the user is a guest. *) 59 + 60 + val is_billing_admin : t -> bool 61 + (** Whether the user is a billing admin. *) 62 + 25 63 val is_bot : t -> bool 64 + (** Whether the user is a bot. *) 65 + 66 + val bot_type : t -> int option 67 + (** Bot type (1=generic, 2=incoming webhook, 3=outgoing webhook, 4=embedded). *) 26 68 27 - (** Jsont codec for the user type *) 69 + val bot_owner_id : t -> int option 70 + (** User ID of the bot's owner. *) 71 + 72 + val avatar_url : t -> string option 73 + (** URL for the user's avatar. *) 74 + 75 + val avatar_version : t -> int option 76 + (** Version number of the avatar (for cache busting). *) 77 + 78 + val timezone : t -> string option 79 + (** User's timezone string. *) 80 + 81 + val date_joined : t -> string option 82 + (** ISO 8601 datetime when user joined. *) 83 + 84 + val role : t -> int option 85 + (** User's role (100=owner, 200=admin, 300=moderator, 400=member, 600=guest). *) 86 + 87 + (** {1 Role Constants} *) 88 + 89 + val role_owner : int 90 + val role_admin : int 91 + val role_moderator : int 92 + val role_member : int 93 + val role_guest : int 94 + 95 + (** {1 JSON Codec} *) 96 + 28 97 val jsont : t Jsont.t 98 + (** Jsont codec for the user type. *) 99 + 100 + (** {1 Pretty Printing} *) 29 101 30 102 val pp : Format.formatter -> t -> unit
+100
lib/zulip/user_group.mli
··· 1 + (** User groups for the Zulip API. 2 + 3 + User groups allow organizing users and setting permissions. *) 4 + 5 + (** {1 User Group Type} *) 6 + 7 + type t = { 8 + id : int; (** Group ID *) 9 + name : string; (** Group name *) 10 + description : string; (** Group description *) 11 + members : int list; (** User IDs of group members *) 12 + direct_subgroup_ids : int list; (** IDs of direct subgroups *) 13 + is_system_group : bool; (** Whether this is a system-managed group *) 14 + can_mention_group : int; (** Group ID that can mention this group *) 15 + } 16 + (** A user group. *) 17 + 18 + (** {1 Listing Groups} *) 19 + 20 + val list : Client.t -> t list 21 + (** Get all user groups in the organization. 22 + @raise Eio.Io on failure *) 23 + 24 + (** {1 Creating Groups} *) 25 + 26 + val create : 27 + Client.t -> 28 + name:string -> 29 + description:string -> 30 + members:int list -> 31 + ?can_mention_group:int -> 32 + unit -> 33 + int 34 + (** Create a new user group. 35 + @return The ID of the created group 36 + @raise Eio.Io on failure *) 37 + 38 + (** {1 Updating Groups} *) 39 + 40 + val update : 41 + Client.t -> 42 + group_id:int -> 43 + ?name:string -> 44 + ?description:string -> 45 + ?can_mention_group:int -> 46 + unit -> 47 + unit 48 + (** Update a user group's properties. 49 + @raise Eio.Io on failure *) 50 + 51 + val update_members : 52 + Client.t -> group_id:int -> ?add:int list -> ?remove:int list -> unit -> unit 53 + (** Update user group membership. 54 + 55 + @param add User IDs to add to the group 56 + @param remove User IDs to remove from the group 57 + @raise Eio.Io on failure *) 58 + 59 + val update_subgroups : 60 + Client.t -> group_id:int -> ?add:int list -> ?remove:int list -> unit -> unit 61 + (** Update user group subgroups. 62 + 63 + @param add Subgroup IDs to add 64 + @param remove Subgroup IDs to remove 65 + @raise Eio.Io on failure *) 66 + 67 + (** {1 Deleting Groups} *) 68 + 69 + val delete : Client.t -> group_id:int -> unit 70 + (** Delete a user group. 71 + @raise Eio.Io on failure *) 72 + 73 + (** {1 Membership Queries} *) 74 + 75 + val get_members : Client.t -> group_id:int -> int list 76 + (** Get the members of a user group. 77 + @raise Eio.Io on failure *) 78 + 79 + val is_member : Client.t -> group_id:int -> user_id:int -> bool 80 + (** Check if a user is a member of a group. 81 + @raise Eio.Io on failure *) 82 + 83 + (** {1 Subgroup Queries} *) 84 + 85 + val get_subgroups : Client.t -> group_id:int -> int list 86 + (** Get the direct subgroups of a user group. 87 + @raise Eio.Io on failure *) 88 + 89 + val is_subgroup : Client.t -> group_id:int -> subgroup_id:int -> bool 90 + (** Check if a group is a subgroup of another. 91 + @raise Eio.Io on failure *) 92 + 93 + (** {1 JSON Codec} *) 94 + 95 + val jsont : t Jsont.t 96 + (** Jsont codec for user groups. *) 97 + 98 + (** {1 Pretty Printing} *) 99 + 100 + val pp : Format.formatter -> t -> unit
+365 -36
lib/zulip/users.ml
··· 1 1 let list client = 2 - (* Define response codec *) 3 2 let response_codec = 4 3 Jsont.Object.( 5 4 map ~kind:"UsersResponse" (fun members -> members) ··· 14 13 (Error.make ~code:(Other "json_parse") ~message:msg ()) 15 14 "parsing users list" 16 15 17 - let get client ~email = 18 - (* Define a codec for the response that wraps the user in a "user" field *) 19 - let user_response_codec = 16 + let list_all client ?client_gravatar ?include_custom_profile_fields () = 17 + let params = 18 + List.filter_map Fun.id 19 + [ 20 + Option.map 21 + (fun v -> ("client_gravatar", string_of_bool v)) 22 + client_gravatar; 23 + Option.map 24 + (fun v -> ("include_custom_profile_fields", string_of_bool v)) 25 + include_custom_profile_fields; 26 + ] 27 + in 28 + let response_codec = 20 29 Jsont.Object.( 21 - map ~kind:"UserResponse" (fun user -> user) 22 - |> mem "user" User.jsont ~enc:(fun x -> x) 30 + map ~kind:"UsersResponse" (fun members -> members) 31 + |> mem "members" (Jsont.list User.jsont) ~enc:(fun x -> x) 23 32 |> finish) 24 33 in 25 34 let json = 35 + Client.request client ~method_:`GET ~path:"/api/v1/users" ~params () 36 + in 37 + match Encode.from_json response_codec json with 38 + | Ok users -> users 39 + | Error msg -> 40 + Error.raise_with_context 41 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 42 + "parsing users list" 43 + 44 + let user_response_codec = 45 + Jsont.Object.( 46 + map ~kind:"UserResponse" (fun user -> user) 47 + |> mem "user" User.jsont ~enc:(fun x -> x) 48 + |> finish) 49 + 50 + let get client ~email = 51 + let json = 26 52 Client.request client ~method_:`GET ~path:("/api/v1/users/" ^ email) () 27 53 in 28 54 match Encode.from_json user_response_codec json with 29 55 | Ok user -> user 30 - | Error _ -> 31 - (* Fallback: try parsing the whole response as a user *) 32 - (match Encode.from_json User.jsont json with 56 + | Error _ -> ( 57 + match Encode.from_json User.jsont json with 33 58 | Ok user -> user 34 59 | Error msg -> 35 60 Error.raise_with_context 36 61 (Error.make ~code:(Other "json_parse") ~message:msg ()) 37 62 "parsing user %s" email) 38 63 39 - let get_by_id client ~user_id = 40 - (* Define a codec for the response that wraps the user in a "user" field *) 41 - let user_response_codec = 42 - Jsont.Object.( 43 - map ~kind:"UserResponse" (fun user -> user) 44 - |> mem "user" User.jsont ~enc:(fun x -> x) 45 - |> finish) 64 + let get_by_id client ~user_id ?include_custom_profile_fields () = 65 + let params = 66 + List.filter_map Fun.id 67 + [ 68 + Option.map 69 + (fun v -> ("include_custom_profile_fields", string_of_bool v)) 70 + include_custom_profile_fields; 71 + ] 46 72 in 47 73 let json = 48 74 Client.request client ~method_:`GET 49 75 ~path:("/api/v1/users/" ^ string_of_int user_id) 50 - () 76 + ~params () 51 77 in 52 78 match Encode.from_json user_response_codec json with 53 79 | Ok user -> user 54 - | Error _ -> 55 - (* Fallback: try parsing the whole response as a user *) 56 - (match Encode.from_json User.jsont json with 80 + | Error _ -> ( 81 + match Encode.from_json User.jsont json with 57 82 | Ok user -> user 58 83 | Error msg -> 59 84 Error.raise_with_context 60 85 (Error.make ~code:(Other "json_parse") ~message:msg ()) 61 86 "parsing user id %d" user_id) 62 87 63 - (* Request type for create_user *) 64 - module Create_user_request = struct 65 - type t = { email : string; full_name : string } 88 + let me client = 89 + let json = Client.request client ~method_:`GET ~path:"/api/v1/users/me" () in 90 + match Encode.from_json User.jsont json with 91 + | Ok user -> user 92 + | Error msg -> 93 + Error.raise_with_context 94 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 95 + "parsing current user" 96 + 97 + let me_pointer client = 98 + let response_codec = 99 + Jsont.Object.( 100 + map ~kind:"PointerResponse" (fun pointer -> pointer) 101 + |> mem "pointer" Jsont.int ~enc:(fun x -> x) 102 + |> finish) 103 + in 104 + let json = 105 + Client.request client ~method_:`GET ~path:"/api/v1/users/me/pointer" () 106 + in 107 + match Encode.from_json response_codec json with 108 + | Ok pointer -> pointer 109 + | Error msg -> 110 + Error.raise_with_context 111 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 112 + "getting pointer" 113 + 114 + let update_me_pointer client ~pointer = 115 + let params = [ ("pointer", string_of_int pointer) ] in 116 + let _response = 117 + Client.request client ~method_:`POST ~path:"/api/v1/users/me/pointer" 118 + ~params () 119 + in 120 + () 121 + 122 + let create client ~email ~full_name ~password = 123 + let params = 124 + [ ("email", email); ("full_name", full_name); ("password", password) ] 125 + in 126 + let _response = 127 + Client.request client ~method_:`POST ~path:"/api/v1/users" ~params () 128 + in 129 + () 130 + 131 + let update client ~user_id ?full_name ?role () = 132 + let params = 133 + List.filter_map Fun.id 134 + [ 135 + Option.map (fun v -> ("full_name", v)) full_name; 136 + Option.map (fun v -> ("role", string_of_int v)) role; 137 + ] 138 + in 139 + let _response = 140 + Client.request client ~method_:`PATCH 141 + ~path:("/api/v1/users/" ^ string_of_int user_id) 142 + ~params () 143 + in 144 + () 145 + 146 + let deactivate client ~user_id = 147 + let _response = 148 + Client.request client ~method_:`DELETE 149 + ~path:("/api/v1/users/" ^ string_of_int user_id) 150 + () 151 + in 152 + () 153 + 154 + let reactivate client ~user_id = 155 + let _response = 156 + Client.request client ~method_:`POST 157 + ~path:("/api/v1/users/" ^ string_of_int user_id ^ "/reactivate") 158 + () 159 + in 160 + () 161 + 162 + let get_alert_words client = 163 + let response_codec = 164 + Jsont.Object.( 165 + map ~kind:"AlertWordsResponse" (fun words -> words) 166 + |> mem "alert_words" (Jsont.list Jsont.string) ~enc:(fun x -> x) 167 + |> finish) 168 + in 169 + let json = 170 + Client.request client ~method_:`GET ~path:"/api/v1/users/me/alert_words" () 171 + in 172 + match Encode.from_json response_codec json with 173 + | Ok words -> words 174 + | Error msg -> 175 + Error.raise_with_context 176 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 177 + "getting alert words" 178 + 179 + let add_alert_words client ~words = 180 + let params = 181 + [ ("alert_words", Encode.to_json_string (Jsont.list Jsont.string) words) ] 182 + in 183 + let response_codec = 184 + Jsont.Object.( 185 + map ~kind:"AlertWordsResponse" (fun words -> words) 186 + |> mem "alert_words" (Jsont.list Jsont.string) ~enc:(fun x -> x) 187 + |> finish) 188 + in 189 + let json = 190 + Client.request client ~method_:`POST ~path:"/api/v1/users/me/alert_words" 191 + ~params () 192 + in 193 + match Encode.from_json response_codec json with 194 + | Ok words -> words 195 + | Error msg -> 196 + Error.raise_with_context 197 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 198 + "adding alert words" 199 + 200 + let remove_alert_words client ~words = 201 + let params = 202 + [ ("alert_words", Encode.to_json_string (Jsont.list Jsont.string) words) ] 203 + in 204 + let response_codec = 205 + Jsont.Object.( 206 + map ~kind:"AlertWordsResponse" (fun words -> words) 207 + |> mem "alert_words" (Jsont.list Jsont.string) ~enc:(fun x -> x) 208 + |> finish) 209 + in 210 + let json = 211 + Client.request client ~method_:`DELETE ~path:"/api/v1/users/me/alert_words" 212 + ~params () 213 + in 214 + match Encode.from_json response_codec json with 215 + | Ok words -> words 216 + | Error msg -> 217 + Error.raise_with_context 218 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 219 + "removing alert words" 220 + 221 + type status_emoji = { 222 + emoji_name : string; 223 + emoji_code : string option; 224 + reaction_type : string option; 225 + } 226 + 227 + let get_status client ~user_id = 228 + Client.request client ~method_:`GET 229 + ~path:("/api/v1/users/" ^ string_of_int user_id ^ "/status") 230 + () 66 231 67 - let codec = 232 + let update_status client ?status_text ?away ?emoji () = 233 + let params = 234 + List.filter_map Fun.id 235 + [ 236 + Option.map (fun v -> ("status_text", v)) status_text; 237 + Option.map (fun v -> ("away", string_of_bool v)) away; 238 + Option.map (fun e -> ("emoji_name", e.emoji_name)) emoji; 239 + Option.bind emoji (fun e -> 240 + Option.map (fun c -> ("emoji_code", c)) e.emoji_code); 241 + Option.bind emoji (fun e -> 242 + Option.map (fun t -> ("reaction_type", t)) e.reaction_type); 243 + ] 244 + in 245 + let _response = 246 + Client.request client ~method_:`POST ~path:"/api/v1/users/me/status" ~params 247 + () 248 + in 249 + () 250 + 251 + let get_settings client = 252 + Client.request client ~method_:`GET ~path:"/api/v1/settings" () 253 + 254 + let update_settings client ?full_name ?email ?old_password ?new_password 255 + ?twenty_four_hour_time ?dense_mode ?starred_message_counts 256 + ?fluid_layout_width ?high_contrast_mode ?color_scheme ?translate_emoticons 257 + ?default_language ?default_view ?escape_navigates_to_default_view 258 + ?left_side_userlist ?emojiset ?demote_inactive_streams ?timezone 259 + ?enable_stream_desktop_notifications ?enable_stream_email_notifications 260 + ?enable_stream_push_notifications ?enable_stream_audible_notifications 261 + ?notification_sound ?enable_desktop_notifications ?enable_sounds 262 + ?email_notifications_batching_period_seconds 263 + ?enable_offline_email_notifications ?enable_offline_push_notifications 264 + ?enable_online_push_notifications ?enable_digest_emails 265 + ?enable_marketing_emails ?enable_login_emails 266 + ?message_content_in_email_notifications 267 + ?pm_content_in_desktop_notifications ?wildcard_mentions_notify 268 + ?desktop_icon_count_display ?realm_name_in_notifications ?presence_enabled 269 + ?enter_sends () = 270 + let params = 271 + List.filter_map Fun.id 272 + [ 273 + Option.map (fun v -> ("full_name", v)) full_name; 274 + Option.map (fun v -> ("email", v)) email; 275 + Option.map (fun v -> ("old_password", v)) old_password; 276 + Option.map (fun v -> ("new_password", v)) new_password; 277 + Option.map 278 + (fun v -> ("twenty_four_hour_time", string_of_bool v)) 279 + twenty_four_hour_time; 280 + Option.map (fun v -> ("dense_mode", string_of_bool v)) dense_mode; 281 + Option.map 282 + (fun v -> ("starred_message_counts", string_of_bool v)) 283 + starred_message_counts; 284 + Option.map 285 + (fun v -> ("fluid_layout_width", string_of_bool v)) 286 + fluid_layout_width; 287 + Option.map 288 + (fun v -> ("high_contrast_mode", string_of_bool v)) 289 + high_contrast_mode; 290 + Option.map (fun v -> ("color_scheme", string_of_int v)) color_scheme; 291 + Option.map 292 + (fun v -> ("translate_emoticons", string_of_bool v)) 293 + translate_emoticons; 294 + Option.map (fun v -> ("default_language", v)) default_language; 295 + Option.map (fun v -> ("default_view", v)) default_view; 296 + Option.map 297 + (fun v -> ("escape_navigates_to_default_view", string_of_bool v)) 298 + escape_navigates_to_default_view; 299 + Option.map 300 + (fun v -> ("left_side_userlist", string_of_bool v)) 301 + left_side_userlist; 302 + Option.map (fun v -> ("emojiset", v)) emojiset; 303 + Option.map 304 + (fun v -> ("demote_inactive_streams", string_of_int v)) 305 + demote_inactive_streams; 306 + Option.map (fun v -> ("timezone", v)) timezone; 307 + Option.map 308 + (fun v -> ("enable_stream_desktop_notifications", string_of_bool v)) 309 + enable_stream_desktop_notifications; 310 + Option.map 311 + (fun v -> ("enable_stream_email_notifications", string_of_bool v)) 312 + enable_stream_email_notifications; 313 + Option.map 314 + (fun v -> ("enable_stream_push_notifications", string_of_bool v)) 315 + enable_stream_push_notifications; 316 + Option.map 317 + (fun v -> ("enable_stream_audible_notifications", string_of_bool v)) 318 + enable_stream_audible_notifications; 319 + Option.map (fun v -> ("notification_sound", v)) notification_sound; 320 + Option.map 321 + (fun v -> ("enable_desktop_notifications", string_of_bool v)) 322 + enable_desktop_notifications; 323 + Option.map (fun v -> ("enable_sounds", string_of_bool v)) enable_sounds; 324 + Option.map 325 + (fun v -> 326 + ("email_notifications_batching_period_seconds", string_of_int v)) 327 + email_notifications_batching_period_seconds; 328 + Option.map 329 + (fun v -> ("enable_offline_email_notifications", string_of_bool v)) 330 + enable_offline_email_notifications; 331 + Option.map 332 + (fun v -> ("enable_offline_push_notifications", string_of_bool v)) 333 + enable_offline_push_notifications; 334 + Option.map 335 + (fun v -> ("enable_online_push_notifications", string_of_bool v)) 336 + enable_online_push_notifications; 337 + Option.map 338 + (fun v -> ("enable_digest_emails", string_of_bool v)) 339 + enable_digest_emails; 340 + Option.map 341 + (fun v -> ("enable_marketing_emails", string_of_bool v)) 342 + enable_marketing_emails; 343 + Option.map 344 + (fun v -> ("enable_login_emails", string_of_bool v)) 345 + enable_login_emails; 346 + Option.map 347 + (fun v -> ("message_content_in_email_notifications", string_of_bool v)) 348 + message_content_in_email_notifications; 349 + Option.map 350 + (fun v -> ("pm_content_in_desktop_notifications", string_of_bool v)) 351 + pm_content_in_desktop_notifications; 352 + Option.map 353 + (fun v -> ("wildcard_mentions_notify", string_of_bool v)) 354 + wildcard_mentions_notify; 355 + Option.map 356 + (fun v -> ("desktop_icon_count_display", string_of_int v)) 357 + desktop_icon_count_display; 358 + Option.map 359 + (fun v -> ("realm_name_in_notifications", string_of_bool v)) 360 + realm_name_in_notifications; 361 + Option.map 362 + (fun v -> ("presence_enabled", string_of_bool v)) 363 + presence_enabled; 364 + Option.map (fun v -> ("enter_sends", string_of_bool v)) enter_sends; 365 + ] 366 + in 367 + Client.request client ~method_:`PATCH ~path:"/api/v1/settings" ~params () 368 + 369 + let get_attachments client = 370 + Client.request client ~method_:`GET ~path:"/api/v1/attachments" () 371 + 372 + let delete_attachment client ~attachment_id = 373 + let _response = 374 + Client.request client ~method_:`DELETE 375 + ~path:("/api/v1/attachments/" ^ string_of_int attachment_id) 376 + () 377 + in 378 + () 379 + 380 + let get_muted_users client = 381 + let response_codec = 68 382 Jsont.Object.( 69 - map ~kind:"CreateUserRequest" (fun email full_name -> { email; full_name }) 70 - |> mem "email" Jsont.string ~enc:(fun r -> r.email) 71 - |> mem "full_name" Jsont.string ~enc:(fun r -> r.full_name) 383 + map ~kind:"MutedUsersResponse" (fun users -> users) 384 + |> mem "muted_users" 385 + (Jsont.list 386 + (Jsont.Object.( 387 + map ~kind:"MutedUser" (fun id _ts -> id) 388 + |> mem "id" Jsont.int ~enc:(fun x -> x) 389 + |> mem "timestamp" Jsont.int ~dec_absent:0 ~enc:(fun _ -> 0) 390 + |> finish))) 391 + ~enc:(fun x -> x) 72 392 |> finish) 73 - end 393 + in 394 + let json = 395 + Client.request client ~method_:`GET ~path:"/api/v1/users/me/muted_users" () 396 + in 397 + match Encode.from_json response_codec json with 398 + | Ok users -> users 399 + | Error msg -> 400 + Error.raise_with_context 401 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 402 + "getting muted users" 74 403 75 - let create_user client ~email ~full_name = 76 - let req = Create_user_request.{ email; full_name } in 77 - let body = Encode.to_form_urlencoded Create_user_request.codec req in 78 - let content_type = "application/x-www-form-urlencoded" in 404 + let mute_user client ~user_id = 79 405 let _response = 80 - Client.request client ~method_:`POST ~path:"/api/v1/users" ~body 81 - ~content_type () 406 + Client.request client ~method_:`POST 407 + ~path:("/api/v1/users/me/muted_users/" ^ string_of_int user_id) 408 + () 82 409 in 83 410 () 84 411 85 - let deactivate client ~email = 412 + let unmute_user client ~user_id = 86 413 let _response = 87 - Client.request client ~method_:`DELETE ~path:("/api/v1/users/" ^ email) () 414 + Client.request client ~method_:`DELETE 415 + ~path:("/api/v1/users/me/muted_users/" ^ string_of_int user_id) 416 + () 88 417 in 89 418 ()
+176 -4
lib/zulip/users.mli
··· 3 3 All functions raise [Eio.Io] with [Error.E error] on failure. 4 4 Context is automatically added indicating the operation being performed. *) 5 5 6 + (** {1 Listing Users} *) 7 + 6 8 val list : Client.t -> User.t list 7 9 (** List all users in the organization. 8 10 @raise Eio.Io on failure *) 9 11 12 + val list_all : 13 + Client.t -> 14 + ?client_gravatar:bool -> 15 + ?include_custom_profile_fields:bool -> 16 + unit -> 17 + User.t list 18 + (** List all users with additional options. 19 + 20 + @param client_gravatar Whether to include gravatar URLs 21 + @param include_custom_profile_fields Whether to include custom profile data 22 + @raise Eio.Io on failure *) 23 + 24 + (** {1 User Lookup} *) 25 + 10 26 val get : Client.t -> email:string -> User.t 11 27 (** Get a user by email address. 12 28 @raise Eio.Io on failure *) 13 29 14 - val get_by_id : Client.t -> user_id:int -> User.t 30 + val get_by_id : 31 + Client.t -> user_id:int -> ?include_custom_profile_fields:bool -> unit -> User.t 15 32 (** Get a user by their numeric ID. 16 33 @raise Eio.Io on failure *) 17 34 18 - val create_user : Client.t -> email:string -> full_name:string -> unit 35 + (** {1 Current User} *) 36 + 37 + val me : Client.t -> User.t 38 + (** Get the currently authenticated user's profile. 39 + @raise Eio.Io on failure *) 40 + 41 + val me_pointer : Client.t -> int 42 + (** Get the current user's message read pointer. 43 + @raise Eio.Io on failure *) 44 + 45 + val update_me_pointer : Client.t -> pointer:int -> unit 46 + (** Update the current user's message read pointer. 47 + @raise Eio.Io on failure *) 48 + 49 + (** {1 Creating Users} *) 50 + 51 + val create : 52 + Client.t -> 53 + email:string -> 54 + full_name:string -> 55 + password:string -> 56 + unit 19 57 (** Create a new user. 20 58 @raise Eio.Io on failure *) 21 59 22 - val deactivate : Client.t -> email:string -> unit 23 - (** Deactivate a user account. 60 + (** {1 Updating Users} *) 61 + 62 + val update : 63 + Client.t -> 64 + user_id:int -> 65 + ?full_name:string -> 66 + ?role:int -> 67 + unit -> 68 + unit 69 + (** Update a user's profile. 70 + @raise Eio.Io on failure *) 71 + 72 + (** {1 Deactivating/Reactivating Users} *) 73 + 74 + val deactivate : Client.t -> user_id:int -> unit 75 + (** Deactivate a user account by user ID. 76 + @raise Eio.Io on failure *) 77 + 78 + val reactivate : Client.t -> user_id:int -> unit 79 + (** Reactivate a deactivated user account. 80 + @raise Eio.Io on failure *) 81 + 82 + (** {1 Alert Words} *) 83 + 84 + val get_alert_words : Client.t -> string list 85 + (** Get the current user's alert words. 86 + @raise Eio.Io on failure *) 87 + 88 + val add_alert_words : Client.t -> words:string list -> string list 89 + (** Add alert words for the current user. 90 + @return The updated list of alert words 91 + @raise Eio.Io on failure *) 92 + 93 + val remove_alert_words : Client.t -> words:string list -> string list 94 + (** Remove alert words for the current user. 95 + @return The updated list of alert words 96 + @raise Eio.Io on failure *) 97 + 98 + (** {1 User Status} *) 99 + 100 + (** User status types. *) 101 + type status_emoji = { 102 + emoji_name : string; 103 + emoji_code : string option; 104 + reaction_type : string option; 105 + } 106 + 107 + val get_status : Client.t -> user_id:int -> Jsont.json 108 + (** Get a user's status. 109 + @raise Eio.Io on failure *) 110 + 111 + val update_status : 112 + Client.t -> 113 + ?status_text:string -> 114 + ?away:bool -> 115 + ?emoji:status_emoji -> 116 + unit -> 117 + unit 118 + (** Update the current user's status. 119 + @raise Eio.Io on failure *) 120 + 121 + (** {1 User Settings} *) 122 + 123 + val get_settings : Client.t -> Jsont.json 124 + (** Get the current user's settings. 125 + @raise Eio.Io on failure *) 126 + 127 + val update_settings : 128 + Client.t -> 129 + ?full_name:string -> 130 + ?email:string -> 131 + ?old_password:string -> 132 + ?new_password:string -> 133 + ?twenty_four_hour_time:bool -> 134 + ?dense_mode:bool -> 135 + ?starred_message_counts:bool -> 136 + ?fluid_layout_width:bool -> 137 + ?high_contrast_mode:bool -> 138 + ?color_scheme:int -> 139 + ?translate_emoticons:bool -> 140 + ?default_language:string -> 141 + ?default_view:string -> 142 + ?escape_navigates_to_default_view:bool -> 143 + ?left_side_userlist:bool -> 144 + ?emojiset:string -> 145 + ?demote_inactive_streams:int -> 146 + ?timezone:string -> 147 + ?enable_stream_desktop_notifications:bool -> 148 + ?enable_stream_email_notifications:bool -> 149 + ?enable_stream_push_notifications:bool -> 150 + ?enable_stream_audible_notifications:bool -> 151 + ?notification_sound:string -> 152 + ?enable_desktop_notifications:bool -> 153 + ?enable_sounds:bool -> 154 + ?email_notifications_batching_period_seconds:int -> 155 + ?enable_offline_email_notifications:bool -> 156 + ?enable_offline_push_notifications:bool -> 157 + ?enable_online_push_notifications:bool -> 158 + ?enable_digest_emails:bool -> 159 + ?enable_marketing_emails:bool -> 160 + ?enable_login_emails:bool -> 161 + ?message_content_in_email_notifications:bool -> 162 + ?pm_content_in_desktop_notifications:bool -> 163 + ?wildcard_mentions_notify:bool -> 164 + ?desktop_icon_count_display:int -> 165 + ?realm_name_in_notifications:bool -> 166 + ?presence_enabled:bool -> 167 + ?enter_sends:bool -> 168 + unit -> 169 + Jsont.json 170 + (** Update the current user's settings. 171 + @return JSON with the updated settings 172 + @raise Eio.Io on failure *) 173 + 174 + (** {1 Attachments} *) 175 + 176 + val get_attachments : Client.t -> Jsont.json 177 + (** Get all attachments uploaded by the current user. 178 + @raise Eio.Io on failure *) 179 + 180 + val delete_attachment : Client.t -> attachment_id:int -> unit 181 + (** Delete an attachment. 182 + @raise Eio.Io on failure *) 183 + 184 + (** {1 Muted Users} *) 185 + 186 + val get_muted_users : Client.t -> int list 187 + (** Get the list of muted user IDs. 188 + @raise Eio.Io on failure *) 189 + 190 + val mute_user : Client.t -> user_id:int -> unit 191 + (** Mute a user. 192 + @raise Eio.Io on failure *) 193 + 194 + val unmute_user : Client.t -> user_id:int -> unit 195 + (** Unmute a user. 24 196 @raise Eio.Io on failure *)
+6
lib/zulip/zulip.ml
··· 9 9 module Message = Message 10 10 module Message_type = Message_type 11 11 module Message_response = Message_response 12 + module Message_flag = Message_flag 12 13 module Messages = Messages 14 + module Narrow = Narrow 13 15 module Channel = Channel 14 16 module Channels = Channels 15 17 module User = User 16 18 module Users = Users 19 + module User_group = User_group 20 + module Presence = Presence 17 21 module Event = Event 18 22 module Event_type = Event_type 19 23 module Event_queue = Event_queue 24 + module Typing = Typing 25 + module Server = Server 20 26 module Encode = Encode
+82 -10
lib/zulip/zulip.mli
··· 3 3 This module provides a comprehensive interface to the Zulip REST API, 4 4 with support for messages, channels, users, and real-time events. 5 5 6 + {1 Quick Start} 7 + 8 + {[ 9 + (* Create a client *) 10 + let auth = Zulip.Auth.from_zuliprc () in 11 + Zulip.Client.with_client env auth (fun client -> 12 + (* Send a message *) 13 + let msg = Zulip.Message.create 14 + ~type_:`Channel ~to_:["general"] 15 + ~content:"Hello from OCaml!" ~topic:"greetings" () 16 + in 17 + let _ = Zulip.Messages.send client msg in 18 + 19 + (* Get recent messages *) 20 + let narrow = Zulip.Narrow.[stream "general"; topic "greetings"] in 21 + let messages = Zulip.Messages.get_messages client 22 + ~anchor:Newest ~num_before:10 ~narrow () 23 + in 24 + (* ... *) 25 + ) 26 + ]} 27 + 6 28 {1 Error Handling} 7 29 8 30 Errors are raised as [Eio.Io] exceptions with [Error.E error], ··· 27 49 28 50 (** {1 Core Types} *) 29 51 30 - (** JSON type used throughout the API *) 31 52 type json = Jsont.json 53 + (** JSON type used throughout the API. *) 32 54 33 - (** {1 Submodules} *) 55 + (** {1 Error Handling} *) 34 56 35 - (** API error handling *) 36 57 module Error = Error 58 + (** API error types and handling. *) 37 59 38 - (** Authentication management *) 60 + (** {1 Authentication} *) 61 + 39 62 module Auth = Auth 63 + (** Authentication credentials management. *) 40 64 41 - (** Client for making API requests *) 65 + (** {1 Client} *) 66 + 42 67 module Client = Client 68 + (** HTTP client for making API requests. *) 43 69 44 - (** Message types and operations *) 70 + (** {1 Messages} *) 71 + 45 72 module Message = Message 73 + (** Outgoing message construction. *) 46 74 47 75 module Message_type = Message_type 76 + (** Message type (channel vs direct). *) 77 + 48 78 module Message_response = Message_response 79 + (** Response from sending a message. *) 80 + 81 + module Message_flag = Message_flag 82 + (** Message flags (read, starred, etc.). *) 83 + 49 84 module Messages = Messages 85 + (** Message operations (send, get, edit, delete, reactions). *) 50 86 51 - (** Channel (stream) operations *) 87 + (** {1 Narrow Filters} *) 88 + 89 + module Narrow = Narrow 90 + (** Type-safe message query filters. *) 91 + 92 + (** {1 Channels (Streams)} *) 93 + 52 94 module Channel = Channel 95 + (** Channel/stream type and properties. *) 53 96 54 97 module Channels = Channels 98 + (** Channel operations (create, subscribe, topics). *) 55 99 56 - (** User management *) 100 + (** {1 Users} *) 101 + 57 102 module User = User 103 + (** User type and properties. *) 58 104 59 105 module Users = Users 106 + (** User operations (list, get, create, status). *) 60 107 61 - (** Event handling *) 108 + module User_group = User_group 109 + (** User groups and membership. *) 110 + 111 + (** {1 Presence} *) 112 + 113 + module Presence = Presence 114 + (** User presence/online status. *) 115 + 116 + (** {1 Events} *) 117 + 62 118 module Event = Event 119 + (** Event records from the event queue. *) 63 120 64 121 module Event_type = Event_type 122 + (** Event type enumeration. *) 123 + 65 124 module Event_queue = Event_queue 125 + (** Event queue management and polling. *) 66 126 67 - (** JSON encoding/decoding utilities *) 127 + (** {1 Typing Notifications} *) 128 + 129 + module Typing = Typing 130 + (** Typing start/stop notifications. *) 131 + 132 + (** {1 Server Information} *) 133 + 134 + module Server = Server 135 + (** Server settings, linkifiers, emoji, profile fields. *) 136 + 137 + (** {1 Utilities} *) 138 + 68 139 module Encode = Encode 140 + (** JSON encoding/decoding utilities. *)