init commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
{:paths ["src"]
|
||||
:deps {org.clojure/clojure {:mvn/version "1.12.0"}
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.955"}
|
||||
com.github.seancorfield/honeysql {:mvn/version "2.6.1230"}
|
||||
org.postgresql/postgresql {:mvn/version "42.7.4"}
|
||||
io.nats/jnats {:mvn/version "2.20.5"}
|
||||
org.babashka/http-client {:mvn/version "0.4.22"}
|
||||
org.clojure/data.json {:mvn/version "2.5.1"}}}
|
||||
@@ -0,0 +1,218 @@
|
||||
(ns ajet.chat.shared.api-client
|
||||
"HTTP client SDK for the ajet-chat API.
|
||||
|
||||
All public functions take an explicit context map (ctx) as the first argument.
|
||||
This avoids dynamic vars which don't work with core.async or cross-thread
|
||||
callbacks (NATS handlers, SSE).
|
||||
|
||||
ctx shape:
|
||||
{:base-url \"http://localhost:3001\" ;; API URL (SMs) or Auth GW URL (CLI)
|
||||
:auth-token \"base64url-token\" ;; raw token, SDK prepends \"Bearer \"
|
||||
:trace-id \"uuid\" ;; optional, from X-Trace-Id
|
||||
:user-id \"uuid\" ;; optional, informational
|
||||
:user-role \"admin\"} ;; optional, informational
|
||||
|
||||
Error handling:
|
||||
- HTTP 4xx/5xx → ex-info with {:type :ajet.chat/api-error, :status, :body, :trace-id}
|
||||
- Network errors (connect refused, timeout) → propagate raw from http-client"
|
||||
(:require [babashka.http-client :as http]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Private HTTP layer
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn- api-url
|
||||
"Join base-url and path, ensuring exactly one slash between them."
|
||||
[base-url path]
|
||||
(let [base (if (str/ends-with? base-url "/")
|
||||
(subs base-url 0 (dec (count base-url)))
|
||||
base-url)
|
||||
p (if (str/starts-with? path "/")
|
||||
(subs path 1)
|
||||
path)]
|
||||
(str base "/" p)))
|
||||
|
||||
(defn- build-headers
|
||||
"Build HTTP headers from ctx."
|
||||
[ctx]
|
||||
(cond-> {"Accept" "application/json"}
|
||||
(:auth-token ctx) (assoc "Authorization" (str "Bearer " (:auth-token ctx)))
|
||||
(:trace-id ctx) (assoc "X-Trace-Id" (:trace-id ctx))))
|
||||
|
||||
(defn- encode-json
|
||||
"Encode a Clojure value as a JSON string."
|
||||
[data]
|
||||
(json/write-str data))
|
||||
|
||||
(defn- parse-json
|
||||
"Parse a JSON string into a Clojure map with keyword keys.
|
||||
Returns nil for nil/blank input."
|
||||
[s]
|
||||
(when-not (str/blank? s)
|
||||
(json/read-str s :key-fn keyword)))
|
||||
|
||||
(defn- check-response!
|
||||
"Throw ex-info on 4xx/5xx responses."
|
||||
[response trace-id]
|
||||
(let [status (:status response)]
|
||||
(when (>= status 400)
|
||||
(throw (ex-info (str "API error: HTTP " status)
|
||||
{:type :ajet.chat/api-error
|
||||
:status status
|
||||
:body (parse-json (:body response))
|
||||
:trace-id trace-id})))))
|
||||
|
||||
(defn- request!
|
||||
"Core HTTP dispatch. All public functions route through here.
|
||||
Returns parsed JSON body as a Clojure map."
|
||||
[ctx method path & [{:keys [body query-params]}]]
|
||||
(let [headers (cond-> (build-headers ctx)
|
||||
body (assoc "Content-Type" "application/json"))
|
||||
url (api-url (:base-url ctx) path)
|
||||
trace-id (:trace-id ctx)
|
||||
opts (cond-> {:method method
|
||||
:uri url
|
||||
:headers headers
|
||||
:throw false}
|
||||
body (assoc :body (encode-json body))
|
||||
query-params (assoc :query-params query-params))
|
||||
response (http/request opts)]
|
||||
(check-response! response trace-id)
|
||||
(parse-json (:body response))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Context helper
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn request->ctx
|
||||
"Build an API client ctx from a Ring request and the API base URL.
|
||||
Extracts auth token from Authorization header and trace/user info from
|
||||
custom headers injected by Auth GW."
|
||||
[ring-request api-base-url]
|
||||
(let [headers (:headers ring-request)
|
||||
auth (get headers "authorization")
|
||||
token (when auth
|
||||
(let [parts (str/split auth #"\s+" 2)]
|
||||
(when (= "Bearer" (first parts))
|
||||
(second parts))))]
|
||||
(cond-> {:base-url api-base-url}
|
||||
token (assoc :auth-token token)
|
||||
(get headers "x-trace-id") (assoc :trace-id (get headers "x-trace-id"))
|
||||
(get headers "x-user-id") (assoc :user-id (get headers "x-user-id"))
|
||||
(get headers "x-user-role") (assoc :user-role (get headers "x-user-role")))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Public API — Channels (community-scoped)
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn get-channels
|
||||
"List channels for a community."
|
||||
[ctx community-slug]
|
||||
(request! ctx :get (str "c/" community-slug "/channels")))
|
||||
|
||||
(defn get-channel
|
||||
"Get a single channel by ID within a community."
|
||||
[ctx community-slug channel-id]
|
||||
(request! ctx :get (str "c/" community-slug "/channels/" channel-id)))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Public API — Messages
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn get-messages
|
||||
"Fetch messages for a channel. opts may include :after, :before, :limit, :thread."
|
||||
[ctx channel-id & [opts]]
|
||||
(let [qp (cond-> {}
|
||||
(:after opts) (assoc "after" (str (:after opts)))
|
||||
(:before opts) (assoc "before" (str (:before opts)))
|
||||
(:limit opts) (assoc "limit" (str (:limit opts)))
|
||||
(:thread opts) (assoc "thread" (str (:thread opts))))]
|
||||
(request! ctx :get (str "api/messages/" channel-id)
|
||||
(when (seq qp) {:query-params qp}))))
|
||||
|
||||
(defn send-message
|
||||
"Send a message to a channel. body-map should contain at least :body_md."
|
||||
[ctx channel-id body-map]
|
||||
(request! ctx :post (str "api/messages/" channel-id) {:body body-map}))
|
||||
|
||||
(defn edit-message
|
||||
"Edit a message. body-map should contain :body_md."
|
||||
[ctx message-id body-map]
|
||||
(request! ctx :put (str "api/messages/" message-id) {:body body-map}))
|
||||
|
||||
(defn delete-message
|
||||
"Delete a message."
|
||||
[ctx message-id]
|
||||
(request! ctx :delete (str "api/messages/" message-id)))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Public API — Reactions
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn add-reaction
|
||||
"Add a reaction (emoji) to a message. Idempotent."
|
||||
[ctx message-id emoji]
|
||||
(request! ctx :put (str "api/messages/" message-id "/reactions/" emoji)))
|
||||
|
||||
(defn remove-reaction
|
||||
"Remove a reaction (emoji) from a message."
|
||||
[ctx message-id emoji]
|
||||
(request! ctx :delete (str "api/messages/" message-id "/reactions/" emoji)))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Public API — DMs (global)
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn get-dms
|
||||
"List the current user's DM channels."
|
||||
[ctx]
|
||||
(request! ctx :get "dm"))
|
||||
|
||||
(defn get-or-create-dm
|
||||
"Get or create a DM channel with another user. body-map should contain :user_id."
|
||||
[ctx body-map]
|
||||
(request! ctx :post "dm" {:body body-map}))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Public API — Users
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn get-user
|
||||
"Get a user by ID."
|
||||
[ctx user-id]
|
||||
(request! ctx :get (str "api/users/" user-id)))
|
||||
|
||||
(defn get-me
|
||||
"Get the current authenticated user."
|
||||
[ctx]
|
||||
(request! ctx :get "api/users/me"))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Public API — Notifications
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn get-notifications
|
||||
"Fetch notifications. opts may include :unread, :after, :limit."
|
||||
[ctx & [opts]]
|
||||
(let [qp (cond-> {}
|
||||
(some? (:unread opts)) (assoc "unread" (str (:unread opts)))
|
||||
(:after opts) (assoc "after" (str (:after opts)))
|
||||
(:limit opts) (assoc "limit" (str (:limit opts))))]
|
||||
(request! ctx :get "api/notifications"
|
||||
(when (seq qp) {:query-params qp}))))
|
||||
|
||||
(defn mark-notifications-read
|
||||
"Mark notifications as read. body-map should contain :notification_ids."
|
||||
[ctx body-map]
|
||||
(request! ctx :post "api/notifications/read" {:body body-map}))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Public API — Presence
|
||||
;;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn heartbeat
|
||||
"Send a presence heartbeat."
|
||||
[ctx]
|
||||
(request! ctx :post "api/heartbeat"))
|
||||
@@ -0,0 +1,17 @@
|
||||
(ns ajet.chat.shared.db
|
||||
"Database layer — uses next.jdbc + HoneySQL. PostgreSQL everywhere."
|
||||
(:require [next.jdbc :as jdbc]
|
||||
[honey.sql :as sql]))
|
||||
|
||||
(defn make-datasource
|
||||
"Create a PostgreSQL datasource."
|
||||
[& [{:keys [dbname host port user password]
|
||||
:or {dbname "ajet_chat"
|
||||
host "localhost"
|
||||
port 5432}}]]
|
||||
(jdbc/get-datasource {:dbtype "postgresql"
|
||||
:dbname dbname
|
||||
:host host
|
||||
:port port
|
||||
:user user
|
||||
:password password}))
|
||||
Reference in New Issue
Block a user