Commit973a84daRecorded20 Feb 2026Repositorysigil-xmpp
feat: Add roster, presence, and MUC modules to sigil-xmpp
Message
Roster module handles contact list management with subscription operations and roster push handling. Presence module tracks contact availability. MUC module supports multi-user chat rooms with occupant tracking, messaging, and room administration.
Changed
src/sigil/xmpp.sgl | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
src/sigil/xmpp/muc.sgl | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/sigil/xmpp/presence.sgl | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/sigil/xmpp/roster.sgl | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
test/test-muc.sgl | 47 +++++++++++++++++++++++++++++++++++++
test/test-roster.sgl | 57 +++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 712 insertions(+), 5 deletions(-)Diff
src/sigil/xmpp.sglmodified
@@ -1,12 +1,17 @@
1
;;; (sigil xmpp) - XMPP Client Library 2
;;; 3
;;; Provides a complete XMPP client with STARTTLS, SASL authentication,−4
;;; stanza handling, and cooperative I/O.+4
;;; stanza handling, roster, presence, MUC, and cooperative I/O. 5
6
(define-library (sigil xmpp)−7
(import (sigil xmpp stanza)+7
(import (sigil core)+8
(sigil sxml)+9
(sigil xmpp stanza) 10
(sigil xmpp sasl)−9
(sigil xmpp connection))+11
(sigil xmpp connection)+12
(sigil xmpp roster)+13
(sigil xmpp presence)+14
(sigil xmpp muc)) 15
16
(export 17
;; From (sigil xmpp stanza)@@ -38,5 +43,71 @@
43
xmpp-send xmpp-send-raw xmpp-process-input 44
;; Event loop 45
xmpp-run xmpp-tick−41
;; Presence−42
xmpp-send-presence))+46
;; Presence (basic)+47
xmpp-send-presence+48
+49
;; From (sigil xmpp roster)+50
roster-item roster-item?+51
roster-item-jid roster-item-name+52
roster-item-subscription roster-item-groups roster-item-ask+53
xmpp-request-roster xmpp-roster xmpp-roster-item+54
xmpp-roster-add xmpp-roster-remove+55
xmpp-subscribe xmpp-accept-subscription+56
xmpp-deny-subscription xmpp-unsubscribe+57
+58
;; From (sigil xmpp presence)+59
presence-info presence-info?+60
presence-info-jid presence-info-show+61
presence-info-status presence-info-priority+62
xmpp-set-status xmpp-go-offline+63
xmpp-presence-of xmpp-resources-of+64
+65
;; From (sigil xmpp muc)+66
muc-room muc-room?+67
muc-room-jid muc-room-nick+68
muc-room-subject muc-room-occupants muc-room-joined?+69
muc-occupant muc-occupant?+70
muc-occupant-nick muc-occupant-jid+71
muc-occupant-affiliation muc-occupant-role+72
xmpp-muc-join xmpp-muc-leave+73
xmpp-muc-message xmpp-muc-private-message+74
xmpp-muc-set-subject xmpp-muc-occupants+75
xmpp-muc-kick xmpp-muc-invite+76
+77
;; Feature integration+78
xmpp-install-features)+79
+80
(begin+81
+82
;;; Install roster, presence, and MUC feature handlers on a connection.+83
;;;+84
;;; Call this after creating a connection to enable automatic roster+85
;;; push handling, presence tracking, and MUC occupant tracking.+86
;;;+87
;;; ```scheme+88
;;; (define conn (make-xmpp-connection ...))+89
;;; (xmpp-install-features conn)+90
;;; (xmpp-connect conn)+91
;;; ```+92
(define (xmpp-install-features conn)+93
;; Handle roster pushes (IQ set with jabber:iq:roster)+94
(xmpp-on conn 'iq+95
(lambda (stanza)+96
(when (equal? (stanza-attr stanza 'type) "set")+97
(let ((query (stanza-child stanza 'query)))+98
(when (and query+99
(equal? (sxml-attr-ref query 'xmlns) "jabber:iq:roster"))+100
(handle-roster-push conn stanza))))))+101
+102
;; Track presence updates+103
(xmpp-on conn 'presence+104
(lambda (stanza)+105
(handle-presence-update conn stanza)+106
;; Check for MUC presence (has x element with MUC xmlns)+107
(let ((x-el (stanza-child stanza 'x)))+108
(when (and x-el+109
(equal? (sxml-attr-ref x-el 'xmlns)+110
"http://jabber.org/protocol/muc#user"))+111
(handle-muc-presence conn stanza))))))+112
+113
))src/sigil/xmpp/muc.sgladded
@@ -0,0 +1,208 @@
+1
;;; (sigil xmpp muc) - Multi-User Chat (XEP-0045)+2
;;;+3
;;; Provides MUC (chatroom) support including joining, leaving,+4
;;; messaging, occupant tracking, and room administration.+5
+6
(define-library (sigil xmpp muc)+7
(import (sigil core)+8
(sigil struct)+9
(sigil sxml)+10
(sigil xmpp stanza)+11
(sigil xmpp connection))+12
+13
(export+14
;; MUC room+15
muc-room+16
muc-room?+17
muc-room-jid+18
muc-room-nick+19
muc-room-subject+20
muc-room-occupants+21
muc-room-joined?+22
+23
;; MUC occupant+24
muc-occupant+25
muc-occupant?+26
muc-occupant-nick+27
muc-occupant-jid+28
muc-occupant-affiliation+29
muc-occupant-role+30
+31
;; MUC operations+32
xmpp-muc-join+33
xmpp-muc-leave+34
xmpp-muc-message+35
xmpp-muc-private-message+36
xmpp-muc-set-subject+37
xmpp-muc-occupants+38
xmpp-muc-kick+39
xmpp-muc-invite+40
+41
;; Internal+42
handle-muc-presence)+43
+44
(begin+45
+46
;; ============================================================+47
;; MUC Data Structures+48
;; ============================================================+49
+50
(define-struct muc-room+51
(jid)+52
(nick)+53
(subject default: #f mutable: #t)+54
(occupants default: #{} mutable: #t)+55
(joined? default: #f mutable: #t))+56
+57
(define-struct muc-occupant+58
(nick)+59
(jid default: #f)+60
(affiliation default: "none")+61
(role default: "none"))+62
+63
+64
;; ============================================================+65
;; Room Registry+66
;; ============================================================+67
+68
;; Get or create room tracking entry+69
(define (get-muc-rooms conn)+70
(or (dict-ref (xmpp-connection-event-handlers conn) '%muc-rooms #f)+71
#{}))+72
+73
(define (set-muc-rooms! conn rooms)+74
(set-xmpp-connection-event-handlers!+75
conn+76
(dict-set (xmpp-connection-event-handlers conn) '%muc-rooms rooms)))+77
+78
(define (get-room conn room-jid)+79
(dict-ref (get-muc-rooms conn) room-jid #f))+80
+81
+82
;; ============================================================+83
;; MUC Operations+84
;; ============================================================+85
+86
;;; Join a MUC room.+87
;;;+88
;;; ```scheme+89
;;; (xmpp-muc-join conn "[email protected]" "mynick")+90
;;; ```+91
(define (xmpp-muc-join conn room-jid nick)+92
;; Register room+93
(let* ((rooms (get-muc-rooms conn))+94
(room (muc-room jid: room-jid nick: nick)))+95
(set-muc-rooms! conn (dict-set rooms room-jid room)))+96
+97
;; Send presence to room+98
(xmpp-send conn+99
(xmpp-presence+100
to: (string-append room-jid "/" nick)+101
children: (list '(x (@ (xmlns "http://jabber.org/protocol/muc")))))))+102
+103
;;; Leave a MUC room.+104
(define (xmpp-muc-leave conn room-jid (keys: (reason #f)))+105
(let ((room (get-room conn room-jid)))+106
(when room+107
(xmpp-send conn+108
(xmpp-presence+109
to: (string-append room-jid "/" (muc-room-nick room))+110
type: "unavailable"+111
children: (if reason+112
(list `(status ,reason))+113
'())))+114
;; Remove room from tracking+115
(set-muc-rooms! conn+116
(dict-remove (get-muc-rooms conn) room-jid)))))+117
+118
;;; Send a message to a MUC room.+119
;;;+120
;;; ```scheme+121
;;; (xmpp-muc-message conn "[email protected]" "Hello room!")+122
;;; ```+123
(define (xmpp-muc-message conn room-jid body)+124
(xmpp-send conn+125
(xmpp-message to: room-jid type: "groupchat" body: body)))+126
+127
;;; Send a private message to a MUC occupant.+128
(define (xmpp-muc-private-message conn room-jid nick body)+129
(xmpp-send conn+130
(xmpp-message to: (string-append room-jid "/" nick)+131
type: "chat" body: body)))+132
+133
;;; Set the subject of a MUC room.+134
(define (xmpp-muc-set-subject conn room-jid subject)+135
(xmpp-send conn+136
(xmpp-message to: room-jid type: "groupchat" subject: subject)))+137
+138
;;; Get the occupants of a MUC room.+139
;;;+140
;;; Returns a list of muc-occupant records, or '() if not in the room.+141
(define (xmpp-muc-occupants conn room-jid)+142
(let ((room (get-room conn room-jid)))+143
(if room+144
(dict-values (muc-room-occupants room))+145
'())))+146
+147
;;; Kick an occupant from a MUC room (requires moderator role).+148
(define (xmpp-muc-kick conn room-jid nick (keys: (reason #f)))+149
(xmpp-send conn+150
(xmpp-iq type: "set" to: room-jid+151
children: (list+152
`(query (@ (xmlns "http://jabber.org/protocol/muc#admin"))+153
(item (@ (nick ,nick) (role "none"))+154
,@(if reason+155
(list `(reason ,reason))+156
'())))))))+157
+158
;;; Send a mediated invitation to a user.+159
(define (xmpp-muc-invite conn room-jid invitee-jid (keys: (reason #f)))+160
(xmpp-send conn+161
(xmpp-message to: room-jid+162
children: (list+163
`(x (@ (xmlns "http://jabber.org/protocol/muc#user"))+164
(invite (@ (to ,invitee-jid))+165
,@(if reason+166
(list `(reason ,reason))+167
'())))))))+168
+169
+170
;; ============================================================+171
;; MUC Presence Handling+172
;; ============================================================+173
+174
;; Handle MUC presence updates+175
(define (handle-muc-presence conn stanza)+176
(let* ((from (stanza-from stanza))+177
(type (stanza-attr stanza 'type)))+178
(when from+179
(let* ((from-jid (parse-jid from))+180
(room-jid (jid-bare from))+181
(nick (jid-resource from-jid))+182
(room (get-room conn room-jid)))+183
(when (and room nick)+184
;; Extract MUC user info+185
(let* ((x-el (stanza-child stanza 'x))+186
(item-el (and x-el (stanza-child x-el 'item)))+187
(real-jid (and item-el (stanza-attr item-el 'jid)))+188
(affiliation (or (and item-el (stanza-attr item-el 'affiliation)) "none"))+189
(role (or (and item-el (stanza-attr item-el 'role)) "none")))+190
+191
(if (equal? type "unavailable")+192
;; Occupant left+193
(set-muc-room-occupants! room+194
(dict-remove (muc-room-occupants room) nick))+195
;; Occupant joined or updated+196
(let ((occupant (muc-occupant+197
nick: nick+198
jid: real-jid+199
affiliation: affiliation+200
role: role)))+201
(set-muc-room-occupants! room+202
(dict-set (muc-room-occupants room) nick occupant))+203
+204
;; If this is our own presence, mark room as joined+205
(when (equal? nick (muc-room-nick room))+206
(set-muc-room-joined?! room #t))))))))))+207
+208
))src/sigil/xmpp/presence.sgladded
@@ -0,0 +1,125 @@
+1
;;; (sigil xmpp presence) - XMPP Presence Tracking+2
;;;+3
;;; Tracks presence information for contacts and provides+4
;;; convenience functions for managing own presence.+5
+6
(define-library (sigil xmpp presence)+7
(import (sigil core)+8
(sigil struct)+9
(sigil sxml)+10
(sigil xmpp stanza)+11
(sigil xmpp connection))+12
+13
(export+14
;; Presence info+15
presence-info+16
presence-info?+17
presence-info-jid+18
presence-info-show+19
presence-info-status+20
presence-info-priority+21
+22
;; Presence operations+23
xmpp-set-status+24
xmpp-go-offline+25
xmpp-presence-of+26
xmpp-resources-of+27
+28
;; Internal+29
handle-presence-update)+30
+31
(begin+32
+33
;; ============================================================+34
;; Presence Info+35
;; ============================================================+36
+37
(define-struct presence-info+38
(jid)+39
(show default: "available")+40
(status default: #f)+41
(priority default: 0))+42
+43
+44
;; ============================================================+45
;; Presence Operations+46
;; ============================================================+47
+48
;;; Set own presence status.+49
;;;+50
;;; ```scheme+51
;;; (xmpp-set-status conn show: "away" status: "Be right back")+52
;;; ```+53
(define (xmpp-set-status conn (keys: (show #f) (status #f) (priority #f)))+54
(xmpp-send conn+55
(xmpp-presence show: show status: status priority: priority)))+56
+57
;;; Go offline (send unavailable presence and disconnect).+58
(define (xmpp-go-offline conn)+59
(xmpp-send conn (xmpp-presence type: "unavailable"))+60
(xmpp-disconnect conn))+61
+62
;;; Get the cached presence info for a JID.+63
;;;+64
;;; Returns a presence-info record or #f.+65
(define (xmpp-presence-of conn jid)+66
(let ((presences (dict-ref (xmpp-connection-event-handlers conn)+67
'%presences #f)))+68
(and presences (dict-ref presences jid #f))))+69
+70
;;; Get all available resources for a bare JID.+71
;;;+72
;;; Returns a list of full JID strings that are currently available.+73
(define (xmpp-resources-of conn bare-jid)+74
(let ((presences (dict-ref (xmpp-connection-event-handlers conn)+75
'%presences #f)))+76
(if (not presences)+77
'()+78
(let ((bare (jid-bare bare-jid)))+79
(dict-fold+80
(lambda (full-jid info result)+81
(if (and (equal? (jid-bare full-jid) bare)+82
(not (equal? (presence-info-show info) "unavailable")))+83
(cons full-jid result)+84
result))+85
'()+86
presences)))))+87
+88
+89
;; ============================================================+90
;; Presence Update Handling+91
;; ============================================================+92
+93
;; Handle incoming presence stanza and update cache+94
(define (handle-presence-update conn stanza)+95
(let* ((from (stanza-from stanza))+96
(type (stanza-attr stanza 'type))+97
(show-el (stanza-child stanza 'show))+98
(status-el (stanza-child stanza 'status))+99
(priority-el (stanza-child stanza 'priority))+100
(presences (or (dict-ref (xmpp-connection-event-handlers conn)+101
'%presences #f)+102
#{})))+103
(when from+104
(if (equal? type "unavailable")+105
;; Remove from presence cache+106
(set-xmpp-connection-event-handlers!+107
conn+108
(dict-set (xmpp-connection-event-handlers conn)+109
'%presences+110
(dict-remove presences from)))+111
;; Update presence cache+112
(let ((info (presence-info+113
jid: from+114
show: (or (and show-el (sxml-text show-el)) "available")+115
status: (and status-el (sxml-text status-el))+116
priority: (if priority-el+117
(or (string->number (sxml-text priority-el)) 0)+118
0))))+119
(set-xmpp-connection-event-handlers!+120
conn+121
(dict-set (xmpp-connection-event-handlers conn)+122
'%presences+123
(dict-set presences from info))))))))+124
+125
))src/sigil/xmpp/roster.sgladded
@@ -0,0 +1,199 @@
+1
;;; (sigil xmpp roster) - XMPP Roster Management+2
;;;+3
;;; Provides roster (contact list) management including fetching,+4
;;; adding/removing contacts, and subscription handling.+5
+6
(define-library (sigil xmpp roster)+7
(import (sigil core)+8
(sigil struct)+9
(sigil sxml)+10
(sigil xmpp stanza)+11
(sigil xmpp connection))+12
+13
(export+14
;; Roster item+15
roster-item+16
roster-item?+17
roster-item-jid+18
roster-item-name+19
roster-item-subscription+20
roster-item-groups+21
roster-item-ask+22
+23
;; Roster operations+24
xmpp-request-roster+25
xmpp-roster+26
xmpp-roster-item+27
xmpp-roster-add+28
xmpp-roster-remove+29
+30
;; Subscription management+31
xmpp-subscribe+32
xmpp-accept-subscription+33
xmpp-deny-subscription+34
xmpp-unsubscribe+35
+36
;; Internal (for connection module)+37
handle-roster-push+38
parse-roster-query)+39
+40
(begin+41
+42
;; ============================================================+43
;; Roster Item+44
;; ============================================================+45
+46
(define-struct roster-item+47
(jid)+48
(name default: #f)+49
(subscription default: "none")+50
(groups default: '())+51
(ask default: #f))+52
+53
+54
;; ============================================================+55
;; Roster Parsing+56
;; ============================================================+57
+58
;; Parse a roster query element into a list of roster-items+59
(define (parse-roster-query query-el)+60
(let ((items (stanza-children query-el 'item)))+61
(map (lambda (item)+62
(let ((groups (map sxml-text (stanza-children item 'group))))+63
(roster-item+64
jid: (stanza-attr item 'jid)+65
name: (stanza-attr item 'name)+66
subscription: (or (stanza-attr item 'subscription) "none")+67
groups: groups+68
ask: (stanza-attr item 'ask))))+69
items)))+70
+71
+72
;; ============================================================+73
;; Roster Operations+74
;; ============================================================+75
+76
;;; Request the roster from the server.+77
;;;+78
;;; Calls `callback` with a list of `roster-item` records.+79
;;;+80
;;; ```scheme+81
;;; (xmpp-request-roster conn+82
;;; (lambda (items)+83
;;; (for-each (lambda (item)+84
;;; (display (roster-item-jid item)))+85
;;; items)))+86
;;; ```+87
(define (xmpp-request-roster conn callback)+88
(xmpp-send-iq conn+89
(xmpp-iq type: "get"+90
children: (list '(query (@ (xmlns "jabber:iq:roster")))))+91
(lambda (response)+92
(let ((query (stanza-child response 'query)))+93
(if query+94
(callback (parse-roster-query query))+95
(callback '()))))))+96
+97
;;; Get the cached roster as a list of roster-items.+98
;;;+99
;;; Returns #f if the roster hasn't been fetched yet.+100
(define (xmpp-roster conn)+101
;; Roster is stored in the connection's event handlers dict+102
;; under the '%roster key+103
(dict-ref (xmpp-connection-event-handlers conn) '%roster #f))+104
+105
;;; Look up a specific contact in the cached roster.+106
(define (xmpp-roster-item conn jid)+107
(let ((roster (xmpp-roster conn)))+108
(and roster+109
(let loop ((items roster))+110
(if (null? items)+111
#f+112
(if (equal? (roster-item-jid (car items)) (jid-bare jid))+113
(car items)+114
(loop (cdr items))))))))+115
+116
;;; Add or update a contact in the roster.+117
;;;+118
;;; ```scheme+119
;;; (xmpp-roster-add conn "[email protected]" name: "Friend")+120
;;; ```+121
(define (xmpp-roster-add conn jid (keys: (name #f) (groups '())))+122
(let ((children+123
(append+124
(if name+125
`((item (@ (jid ,jid) (name ,name))))+126
`((item (@ (jid ,jid)))))+127
(map (lambda (g) `(group ,g)) groups))))+128
(xmpp-send conn+129
(xmpp-iq type: "set"+130
children: (list+131
`(query (@ (xmlns "jabber:iq:roster"))+132
,@children))))))+133
+134
;;; Remove a contact from the roster.+135
(define (xmpp-roster-remove conn jid)+136
(xmpp-send conn+137
(xmpp-iq type: "set"+138
children: (list+139
`(query (@ (xmlns "jabber:iq:roster"))+140
(item (@ (jid ,jid) (subscription "remove"))))))))+141
+142
+143
;; ============================================================+144
;; Subscription Management+145
;; ============================================================+146
+147
;;; Send a subscription request to a JID.+148
(define (xmpp-subscribe conn jid)+149
(xmpp-send conn (xmpp-presence to: jid type: "subscribe")))+150
+151
;;; Accept a subscription request from a JID.+152
(define (xmpp-accept-subscription conn jid)+153
(xmpp-send conn (xmpp-presence to: jid type: "subscribed")))+154
+155
;;; Deny a subscription request from a JID.+156
(define (xmpp-deny-subscription conn jid)+157
(xmpp-send conn (xmpp-presence to: jid type: "unsubscribed")))+158
+159
;;; Unsubscribe from a JID's presence.+160
(define (xmpp-unsubscribe conn jid)+161
(xmpp-send conn (xmpp-presence to: jid type: "unsubscribe")))+162
+163
+164
;; ============================================================+165
;; Roster Push Handling+166
;; ============================================================+167
+168
;; Handle incoming roster push IQ from server+169
(define (handle-roster-push conn stanza)+170
(let ((query (stanza-child stanza 'query)))+171
(when query+172
(let ((items (parse-roster-query query)))+173
;; Update cached roster+174
(let ((roster (or (xmpp-roster conn) '())))+175
(for-each+176
(lambda (new-item)+177
(let ((jid (roster-item-jid new-item)))+178
(set! roster+179
(if (equal? (roster-item-subscription new-item) "remove")+180
;; Remove item+181
(filter (lambda (i) (not (equal? (roster-item-jid i) jid)))+182
roster)+183
;; Add or replace+184
(cons new-item+185
(filter (lambda (i) (not (equal? (roster-item-jid i) jid)))+186
roster))))))+187
items)+188
;; Store updated roster+189
(set-xmpp-connection-event-handlers!+190
conn+191
(dict-set (xmpp-connection-event-handlers conn)+192
'%roster roster)))+193
+194
;; Acknowledge the push+195
(xmpp-send conn+196
(xmpp-iq type: "result"+197
id: (stanza-id stanza)))))))+198
+199
))test/test-muc.sgladded
@@ -0,0 +1,47 @@
+1
(import (sigil test)+2
(sigil string)+3
(sigil xmpp stanza)+4
(sigil xmpp muc))+5
+6
;; ============================================================+7
;; MUC Data Structures+8
;; ============================================================+9
+10
(test-group "muc-structs"+11
(test "create muc-room"+12
(let ((room (muc-room jid: "[email protected]" nick: "bot")))+13
(assert-equal "[email protected]" (muc-room-jid room))+14
(assert-equal "bot" (muc-room-nick room))+15
(assert-false (muc-room-joined? room))))+16
+17
(test "create muc-occupant"+18
(let ((occ (muc-occupant nick: "alice"+19
jid: "[email protected]/res"+20
affiliation: "member"+21
role: "participant")))+22
(assert-equal "alice" (muc-occupant-nick occ))+23
(assert-equal "[email protected]/res" (muc-occupant-jid occ))+24
(assert-equal "member" (muc-occupant-affiliation occ))+25
(assert-equal "participant" (muc-occupant-role occ)))))+26
+27
;; ============================================================+28
;; MUC Message Construction+29
;; ============================================================+30
+31
(test-group "muc-messages"+32
(test "groupchat message"+33
(let ((msg (xmpp-message to: "[email protected]" type: "groupchat"+34
body: "Hello room" id: "m1")))+35
(assert-equal "groupchat" (stanza-attr msg 'type))+36
(assert-equal "[email protected]" (stanza-to msg))+37
(assert-equal "Hello room" (message-body msg))))+38
+39
(test "MUC join presence has x element"+40
(let ((p (xmpp-presence+41
to: "[email protected]/bot"+42
children: (list '(x (@ (xmlns "http://jabber.org/protocol/muc")))))))+43
(assert-equal "[email protected]/bot" (stanza-to p))+44
(let ((x (stanza-child p 'x)))+45
(assert-true (pair? x))))))+46
+47
(run-tests)test/test-roster.sgladded
@@ -0,0 +1,57 @@
+1
(import (sigil test)+2
(sigil xmpp roster)+3
(sigil xmpp stanza))+4
+5
;; ============================================================+6
;; Roster Parsing+7
;; ============================================================+8
+9
(test-group "roster-parsing"+10
(test "parse roster query"+11
(let ((items (parse-roster-query+12
'(query (@ (xmlns "jabber:iq:roster"))+13
(item (@ (jid "[email protected]")+14
(name "Alice")+15
(subscription "both"))+16
(group "Friends"))+17
(item (@ (jid "[email protected]")+18
(subscription "from")))))))+19
(assert-equal 2 (length items))+20
+21
(let ((alice (car items)))+22
(assert-equal "[email protected]" (roster-item-jid alice))+23
(assert-equal "Alice" (roster-item-name alice))+24
(assert-equal "both" (roster-item-subscription alice))+25
(assert-equal '("Friends") (roster-item-groups alice)))+26
+27
(let ((bob (cadr items)))+28
(assert-equal "[email protected]" (roster-item-jid bob))+29
(assert-false (roster-item-name bob))+30
(assert-equal "from" (roster-item-subscription bob))+31
(assert-equal '() (roster-item-groups bob)))))+32
+33
(test "parse empty roster"+34
(let ((items (parse-roster-query+35
'(query (@ (xmlns "jabber:iq:roster"))))))+36
(assert-equal '() items)))+37
+38
(test "parse roster item with multiple groups"+39
(let ((items (parse-roster-query+40
'(query (@ (xmlns "jabber:iq:roster"))+41
(item (@ (jid "a@b") (subscription "both"))+42
(group "Work")+43
(group "Friends"))))))+44
(assert-equal '("Work" "Friends")+45
(roster-item-groups (car items))))))+46
+47
;; ============================================================+48
;; Subscription Stanza Construction+49
;; ============================================================+50
+51
(test-group "subscription"+52
(test "subscribe presence type"+53
(let ((p (xmpp-presence to: "[email protected]" type: "subscribe")))+54
(assert-equal "subscribe" (stanza-attr p 'type))+55
(assert-equal "[email protected]" (stanza-to p)))))+56
+57
(run-tests)