(in-package :whatsxmpp) (defun make-message-uuid (comp) (with-accessors ((promises component-promises)) comp (let ((uuid (string-downcase (write-to-string (uuid:make-v4-uuid)))) (promise (make-promise))) (setf (gethash uuid promises) promise) (values uuid promise)))) (defmacro with-stanza ((comp stanza-name &key type from to id) &body body) (alexandria:with-gensyms (uuid ret from-sym id-sym) `(with-component-xml-output (,comp) (let ((,from-sym (or ,from (component-name ,comp))) (,id-sym ,id)) (multiple-value-bind (,uuid ,ret) (if ,id-sym (values ,id-sym ,id-sym) (make-message-uuid ,comp)) (cxml:with-element ,stanza-name (cxml:attribute "from" ,from-sym) (cxml:attribute "id" ,uuid) ,(when to `(cxml:attribute "to" ,to)) ,(when type `(cxml:attribute "type" ,type)) ,@body) ,ret))))) (defmacro with-iq ((comp to &key (type "get") from id) &body body) "Send an IQ stanza (of type TYPE) on the COMP component, from the JID FROM (default: component name) to the JID TO, with BODY specifying further CXML commands to make up the body of the stanza. Returns a promise." `(with-stanza (,comp "iq" :type ,type :to ,to :from ,from :id ,id) ,@body)) (defmacro with-message ((comp to &key (type "chat") from id) &body body) "Send a message stanza (of type TYPE) on the COMP component. Semantics the same as WITH-IQ, except for the fact that message stanzas don't normally prompt a response." `(with-stanza (,comp "message" :type ,type :to ,to :from ,from :id ,id) ,@body)) (defmacro with-presence ((comp to &key type from id) &body body) "Send a presence stanza (of type TYPE) on the COMP component. Semantics the same as WITH-IQ, except for the fact that presence stanzas don't normally prompt a response." `(with-stanza (,comp "presence" :type ,type :to ,to :from ,from :id ,id) ,@body)) (defun get-node-named (nodes name) "Finds the node with tag name NAME in NODES, returning NIL if none was found." (flet ((is-the-node (node) (and (dom:element-p node) (equal (dom:tag-name node) name)))) (find-if #'is-the-node nodes))) (defun get-node-with-xmlns (nodes xmlns) "Finds the node with XML namespace XMLNS in NODES, returning NIL if none was found." (flet ((is-the-node (node) (and (dom:element-p node) (equal (dom:get-attribute node "xmlns") xmlns)))) (find-if #'is-the-node nodes))) (defun get-node-text (node) "Gets the node's text." (let ((child-nodes (dom:child-nodes node))) (if (> (length child-nodes) 0) (dom:node-value (elt child-nodes 0)) ""))) (defun handle-stream-error (comp stanza) (flet ((is-error-node (node) (equal (dom:namespace-uri node) +streams-ns+)) (is-text-node (node) (equal (dom:tag-name node) "text"))) (let* ((children (child-elements stanza)) (error-node (find-if #'is-error-node children)) (error-text-node (find-if #'is-text-node children)) (error-name (dom:tag-name error-node)) (error-text (when error-text-node (dom:node-value (elt (dom:child-nodes error-text-node) 0))))) (warn "Stream error of type ~A encountered: ~A" error-name error-text) (emit :stream-error comp error-name error-text stanza)))) (define-condition stanza-error (error) ((defined-condition :initarg :defined-condition :accessor stanza-error-condition) (type :initarg :type :accessor stanza-error-type) (text :initarg :text :initform nil :accessor stanza-error-text) (raw :initarg :raw :initform nil :accessor stanza-error-raw)) (:report (lambda (err stream) (with-slots (defined-condition type text) err (format stream "~A (type ~A): ~A" defined-condition type text))))) (defun extract-stanza-error (stanza) "Extracts a STANZA-ERROR from the given STANZA, which must contain an element conforming to RFC 6120 ยง 8.3." (flet ((is-error-condition-node (node) (equal (dom:namespace-uri node) +stanzas-ns+)) (is-error-node (node) (equal (dom:tag-name node) "error")) (is-text-node (node) (and (equal (dom:namespace-uri node) +stanzas-ns+) (equal (dom:tag-name node) "text")))) (let* ((error-node (find-if #'is-error-node (child-elements stanza))) (error-children (child-elements error-node)) (type (dom:get-attribute error-node "type")) (condition-node (find-if #'is-error-condition-node error-children)) (condition-name (dom:tag-name condition-node)) (text-node (find-if #'is-text-node error-children)) (text (when text-node (dom:node-value (elt (dom:child-nodes text-node) 0))))) (make-condition 'stanza-error :raw error-node :defined-condition condition-name :type type :text text)))) (defun send-stanza-error (comp &key id to from e stanza-type) "Send E (a STANZA-ERROR) as an error response to a stanza of type STANZA." (with-component-xml-output (comp) (cxml:with-element stanza-type (cxml:attribute "type" "error") (cxml:attribute "id" id) (cxml:attribute "from" from) (cxml:attribute "to" to) (cxml:with-element "error" (cxml:attribute "type" (stanza-error-type e)) (cxml:with-element (stanza-error-condition e) (cxml:attribute "xmlns" +stanzas-ns+)) (when (stanza-error-text e) (cxml:with-element "text" (cxml:text (stanza-error-text e)))))))) (defun parse-jid (jid) "Parse JID, returning the multiple values HOSTNAME, LOCALPART and RESOURCE." (declare (type string jid)) (let ((at-pos (position #\@ jid)) (slash-pos (position #\/ jid))) (cond ((and (not slash-pos) (not at-pos)) (values jid nil nil)) ((and slash-pos (not at-pos)) (multiple-value-bind (hostname resource) (whatscl::split-at jid slash-pos) (values hostname nil resource))) ((and (not slash-pos) at-pos) (multiple-value-bind (localpart hostname) (whatscl::split-at jid at-pos) (values hostname localpart nil))) (t (multiple-value-bind (rest resource) (whatscl::split-at jid slash-pos) (multiple-value-bind (localpart hostname) (whatscl::split-at rest at-pos) (values hostname localpart resource))))))) (defun strip-resource (jid) "Strips a resource from JID, if there is one, returning the bare JID." (let ((slash-pos (position #\/ jid))) (if slash-pos (whatscl::split-at jid slash-pos) jid)))