XMPP
XMPP client library with STARTTLS, SASL authentication, roster, presence, and MUC support.
(import (sigil xmpp))Connecting
(define conn (make-xmpp-connection
server: "example.com"
jid: "bot@example.com"
password: "secret"
resource: "bot"))
;; Install roster, presence, and MUC tracking
(xmpp-install-features conn)
;; Connect (TCP -> STARTTLS -> SASL -> resource bind)
(xmpp-connect conn)
;; Announce availability
(xmpp-send-presence conn)
;; Run the event loop (blocks until disconnected)
(xmpp-run conn)xmpp-connect handles the full handshake: TCP connection, STARTTLS upgrade, SASL authentication (SCRAM-SHA-1 or PLAIN), and resource binding. Returns #t on success, #f on failure.
xmpp-run processes incoming stanzas. Inside a with-async context it cooperates with other tasks via await-readable; otherwise it blocks with socket-select.
Sending Messages
;; Chat message
(xmpp-send conn
(xmpp-message to: "friend@example.com" body: "Hello!"))
;; Message with subject
(xmpp-send conn
(xmpp-message to: "friend@example.com"
body: "Check this out"
subject: "Interesting"))Handling Events (Callbacks)
Register handlers for specific event types with xmpp-on:
(xmpp-on conn 'message
(lambda (stanza)
(let ((body (message-body stanza))
(from (stanza-from stanza)))
(when (and body from)
(display (string-append from ": " body "\n"))))))
(xmpp-on conn 'presence
(lambda (stanza)
(display (string-append (stanza-from stanza) " is now "
(or (stanza-attr stanza 'type) "available") "\n"))))
(xmpp-on conn 'connected
(lambda (conn)
(display "Connected!\n")))
(xmpp-on conn 'disconnected
(lambda (conn)
(display "Disconnected.\n")))Event types: message, presence, iq, stanza, connected, disconnected, error.
xmpp-on-stanza registers a handler for all incoming stanzas regardless of type.
Handling Events (Channels)
For sequential, composable event processing use xmpp-channel:
(import (sigil xmpp)
(sigil async)
(sigil channels))
(with-async
(go (xmpp-run conn))
(let ((msgs (xmpp-channel conn 'message)))
(for-channel msgs
(lambda (stanza)
(let ((body (message-body stanza)))
(when body
(xmpp-send conn
(xmpp-message to: (stanza-from stanza)
body: (string-append "Echo: " body)))))))))Channels work with channel-receive, for-channel, and channel-select for multiplexing:
(with-async
(go (xmpp-run conn))
(let ((msgs (xmpp-channel conn 'message))
(pres (xmpp-channel conn 'presence)))
(let loop ()
(channel-select
(msgs => (lambda (s) (handle-message s)))
(pres => (lambda (s) (handle-presence s))))
(loop))))IQ Requests
Send IQ stanzas with automatic response correlation:
(xmpp-send-iq conn
(xmpp-iq type: "get"
to: "example.com"
children: (list '(query (@ (xmlns "http://jabber.org/protocol/disco#info")))))
(lambda (response)
(display "Got disco response\n")))The callback is invoked once when the server replies with a matching IQ result or error.
Roster
;; Fetch the roster
(xmpp-request-roster conn
(lambda (items)
(for-each (lambda (item)
(display (string-append (roster-item-jid item) "\n")))
items)))
;; Access cached roster (after fetch)
(xmpp-roster conn) ; => list of roster-item, or #f
(xmpp-roster-item conn "alice@example.com") ; => roster-item or #f
;; Add a contact
(xmpp-roster-add conn "friend@example.com" name: "Friend")
;; Remove a contact
(xmpp-roster-remove conn "old@example.com")Roster Item Accessors
(roster-item-jid item) ; => "alice@example.com"
(roster-item-name item) ; => "Alice" or #f
(roster-item-subscription item) ; => "both", "from", "to", "none"
(roster-item-groups item) ; => ("Friends" "Work")Subscriptions
(xmpp-subscribe conn "user@example.com")
(xmpp-accept-subscription conn "user@example.com")
(xmpp-deny-subscription conn "user@example.com")
(xmpp-unsubscribe conn "user@example.com")Presence
;; Send available presence
(xmpp-send-presence conn)
;; Set status
(xmpp-set-status conn show: "away" status: "Be right back")
;; Go offline and disconnect
(xmpp-go-offline conn)Querying Presence
With xmpp-install-features, presence is tracked automatically:
;; Get presence for a full JID
(xmpp-presence-of conn "alice@example.com/mobile")
; => #<presence-info jid: "..." show: "available" status: #f>
;; Get all online resources for a bare JID
(xmpp-resources-of conn "alice@example.com")
; => ("alice@example.com/mobile" "alice@example.com/laptop")Presence Info Accessors
(presence-info-jid info) ; => "alice@example.com/mobile"
(presence-info-show info) ; => "available", "away", "dnd", "xa", "chat"
(presence-info-status info) ; => "Be right back" or #f
(presence-info-priority info) ; => 0Multi-User Chat (MUC)
;; Join a room
(xmpp-muc-join conn "room@conference.example.com" "mynick")
;; Send a message to the room
(xmpp-muc-message conn "room@conference.example.com" "Hello room!")
;; Private message to an occupant
(xmpp-muc-private-message conn "room@conference.example.com" "alice" "Hi!")
;; Set room subject
(xmpp-muc-set-subject conn "room@conference.example.com" "New topic")
;; Leave the room
(xmpp-muc-leave conn "room@conference.example.com")Room Administration
;; Kick an occupant (requires moderator role)
(xmpp-muc-kick conn "room@conference.example.com" "troublemaker"
reason: "Disruptive behavior")
;; Invite a user
(xmpp-muc-invite conn "room@conference.example.com" "friend@example.com"
reason: "Come join us!")
;; Get current occupants
(xmpp-muc-occupants conn "room@conference.example.com")
; => list of muc-occupant recordsMUC Occupant Accessors
(muc-occupant-nick occ) ; => "alice"
(muc-occupant-jid occ) ; => "alice@example.com/res" or #f
(muc-occupant-affiliation occ) ; => "owner", "admin", "member", "none"
(muc-occupant-role occ) ; => "moderator", "participant", "visitor", "none"JIDs
;; Parse a JID string
(define j (parse-jid "user@example.com/mobile"))
(jid-local j) ; => "user"
(jid-domain j) ; => "example.com"
(jid-resource j) ; => "mobile"
;; Convert back to string
(jid->string j) ; => "user@example.com/mobile"
;; Bare JID (without resource)
(jid-bare "user@example.com/mobile") ; => "user@example.com"
(jid-bare j) ; => "user@example.com"Stanza Constructors
All stanzas are represented as SXML and auto-generate unique IDs:
(xmpp-message to: "user@ex.com" body: "Hi" type: "chat")
; => (message (@ (to "user@ex.com") (type "chat") (id "s1-...")) (body "Hi"))
(xmpp-presence show: "away" status: "BRB")
; => (presence (@ (id "s2-...")) (show "away") (status "BRB"))
(xmpp-iq type: "get" to: "example.com"
children: (list '(query (@ (xmlns "jabber:iq:roster")))))
; => (iq (@ (to "example.com") (type "get") (id "s3-...")) (query ...))Stanza Inspection
(stanza-type stanza) ; => message, presence, iq (symbol)
(stanza-to stanza) ; => "user@example.com"
(stanza-from stanza) ; => "other@example.com/res"
(stanza-id stanza) ; => "s1-abc123"
(stanza-attr stanza 'type) ; => "chat"
(message-body stanza) ; => "Hello" or #f
(stanza-child stanza 'body) ; => (body "Hello") or #f
(stanza-children stanza 'item) ; => list of matching child elementsCommon Patterns
Echo Bot
(import (sigil xmpp))
(define conn (make-xmpp-connection
server: "example.com"
jid: "bot@example.com"
password: "secret"))
(xmpp-install-features conn)
(xmpp-on conn 'message
(lambda (stanza)
(let ((body (message-body stanza))
(from (stanza-from stanza)))
(when (and body from)
(xmpp-send conn
(xmpp-message to: from body: (string-append "Echo: " body)))))))
(xmpp-connect conn)
(xmpp-send-presence conn)
(xmpp-run conn)MUC Bot with Channels
(import (sigil xmpp)
(sigil async)
(sigil channels))
(define conn (make-xmpp-connection
server: "example.com"
jid: "bot@example.com"
password: "secret"))
(xmpp-install-features conn)
(xmpp-connect conn)
(xmpp-send-presence conn)
(xmpp-muc-join conn "room@conference.example.com" "bot")
(with-async
(go (xmpp-run conn))
(let ((msgs (xmpp-channel conn 'message)))
(for-channel msgs
(lambda (stanza)
(when (equal? (stanza-attr stanza 'type) "groupchat")
(let ((body (message-body stanza)))
(when (and body (string-starts-with? body "!ping"))
(xmpp-muc-message conn
"room@conference.example.com" "pong!"))))))))