3 The REST client
scicloj.zulipdata.client is the library’s lowest level: a thin wrapper over Zulip’s HTTP REST API for the Clojurians instance. Most analyses can use the higher-level pull namespace. The client is useful to understand, and is what you call directly for any endpoint the rest of the library does not cover.
This chapter walks through:
How credentials are resolved.
The four endpoints the library exposes (
whoami,get-me,get-streams,get-messages).The general-purpose
api-getfor any other endpoint.
(ns zulipdata-book.client
(:require
;; Zulipdata client -- Zulip REST API wrapper
[scicloj.zulipdata.client :as client]
;; Kindly -- notebook rendering protocol
[scicloj.kindly.v4.kind :as kind]))Credentials
The client looks for credentials in two places, in order:
- The environment variables
ZULIP_EMAILandZULIP_API_KEY. - The
[api]section of~/.zuliprc(the standard Zulip CLI config file), withemail,key, andsite = https://clojurians.zulipchat.com.
If both are present, the environment variables take precedence. If ~/.zuliprc points at a different site value, the client raises an error rather than calling the wrong host. If neither path yields a complete pair, the first request raises an exception with instructions for fixing it.
You can generate an API key from Settings → Account & privacy → API key in any Clojurians Zulip account — admin rights are not required.
Credentials are resolved lazily and memoized — the file is read at most once per JVM.
A first request
whoami is the simplest possible request: it calls /users/me and returns a small summary of the authenticated identity. It is the right first call after configuring credentials.
(def me (client/whoami))me{:email "user138175@clojurians.zulipchat.com",
:full-name "Daniel Slutsky",
:user-id 138175,
:is-bot false,
:is-admin true,
:role 100}The shape:
(keys me)(:email :full-name :user-id :is-bot :is-admin :role)If you need the full /users/me response — including fields whoami does not include, like timezone or avatar URL — use get-me. We print the count of fields here:
(-> (client/get-me) keys count)19Listing channels
get-streams calls /streams and returns the full Zulip API response, including every channel the authenticated user can see.
(def streams-response (client/get-streams))(-> streams-response :streams count)237A single stream entry, with its raw Zulip field names:
(-> streams-response :streams first keys sort)(:can_add_subscribers_group
:can_administer_channel_group
:can_create_topic_group
:can_delete_any_message_group
:can_delete_own_message_group
:can_move_messages_out_of_channel_group
:can_move_messages_within_channel_group
:can_remove_subscribers_group
:can_resolve_topics_group
:can_send_message_group
:can_subscribe_group
:creator_id
:date_created
:description
:first_message_id
:folder_id
:history_public_to_subscribers
:invite_only
:is_announcement_only
:is_archived
:is_recently_active
:is_web_public
:message_retention_days
:name
:rendered_description
:stream_id
:stream_post_policy
:stream_weekly_traffic
:subscriber_count
:topics_policy)Web-public channels
Among those streams, some are marked web-public: their messages are readable without a Zulip account. The flag lives on the stream entry as :is_web_public.
(def web-public-channels
(->> streams-response
:streams
(filter :is_web_public)
(mapv :name)
sort))(count web-public-channels)26The full list, alphabetised:
web-public-channels("announce"
"beginners"
"bubble-up"
"calva"
"clojars"
"clojure"
"clojure-europe"
"clojure-ohio"
"clojure-uk"
"clojurecivitas"
"clojurescript"
"events"
"general"
"gratitude"
"honeysql"
"jobs"
"news-and-articles"
"off-topic"
"project-announcements"
"scicloj-webpublic"
"slack-archive"
"sql"
"std.lang-dev"
"windows-clojure"
"xtdb"
"zulip")The distinction matters when sharing data — content from a non-web-public channel should not appear in artifacts that leave your machine, while content from web-public channels is suitable for sharing. The book’s tutorial chapters (Tablecloth views, Narrative, Graph views) deliberately use a small web-public sample so the rendered output can show real names, topic strings, and message content without leaking anything login-gated. For analyses on non-web-public channels, see Anonymized views.
Fetching messages
get-messages is the message-history endpoint. It takes a narrow (a vector of operator/operand maps), an anchor (an id, or one of the keywords "newest", "oldest", "first_unread"), and counts of messages to fetch before and after the anchor.
A single message from clojurecivitas, one of the web-public channels we reuse as a sample throughout this book:
(def one-message-response
(client/get-messages
{:narrow [{:operator "channel" :operand "clojurecivitas"}]
:anchor "newest"
:num-before 1
:num-after 0}))(-> one-message-response :messages count)1The whole message map — sender, topic, content, timestamps, reactions, and edit history. The exact set of fields varies slightly across messages; the rest of the library normalises this shape into the views described in Tablecloth views.
(-> one-message-response :messages first){:display_recipient "clojurecivitas",
:content
"These days, we are exploring Babashka for data analysis and using Babqua for interactive data visualization. This time, we are looking into @**Gert Goet**'s Clojure Events Calendar Feed.",
:sender_id 138175,
:client "website",
:submessages [],
:type "stream",
:sender_realm_str "clojurians",
:stream_id 528764,
:content_type "text/x-markdown",
:id 592473831,
:is_me_message false,
:sender_email "user138175@clojurians.zulipchat.com",
:sender_full_name "Daniel Slutsky",
:recipient_id 1681040,
:timestamp 1777747330,
:flags ["read"],
:subject "Datavis in Babashka: analysing our calendar feed",
:reactions
[{:emoji_name "eyes",
:emoji_code "1f440",
:reaction_type "unicode_emoji",
:user_id 137562}],
:topic_links [],
:avatar_url
"https://avatars.zulip.com/9096/ae339dcb9dd0c56484977aeb0b0236822df7e6da.png"}The response also indicates whether the window touches the start or end of the channel’s history — this is what pull/pull-channel! uses to decide when to stop walking forward:
(select-keys one-message-response
[:found_anchor :found_oldest :found_newest]){:found_anchor false, :found_oldest false, :found_newest true}The general-purpose endpoint
api-get is the thin function the others are built on. Use it for any endpoint not covered above — pass a path (relative to the API base) and an optional query-params map.
The base-url def is the prefix that path arguments are appended to. It points at the Clojurians instance:
client/base-url"https://clojurians.zulipchat.com/api/v1"A direct call. /server_settings is unauthenticated metadata — a harmless example.
(-> (client/api-get "/server_settings")
(select-keys [:realm_name :realm_uri :zulip_version])){:realm_name "Clojurians",
:realm_uri "https://clojurians.zulipchat.com",
:zulip_version "12.0"}Reliability: timeouts and retries
The client wraps every request in a small retry loop with longer waits between retries (up to four retries) for network errors and timeouts. A single request times out after ninety seconds. This is invisible from the outside — calls succeed or, after exhausting retries, throw — but it is worth knowing when explaining occasional slow responses.
Where to go next
Pulling and caching channels —
scicloj.zulipdata.pullbuilds on this client to walk a channel’s full history in resumable, cached windows.Tablecloth views —
scicloj.zulipdata.viewsprojects raw messages into purpose-built datasets.API Reference — every public function in one chapter, with docstrings and a worked example each.