summaryrefslogtreecommitdiff
path: root/jabber-core.el
diff options
context:
space:
mode:
Diffstat (limited to 'jabber-core.el')
-rw-r--r--jabber-core.el1006
1 files changed, 1006 insertions, 0 deletions
diff --git a/jabber-core.el b/jabber-core.el
new file mode 100644
index 0000000..9258647
--- /dev/null
+++ b/jabber-core.el
@@ -0,0 +1,1006 @@
+;; jabber-core.el - core functions
+
+;; Copyright (C) 2003, 2004, 2007, 2008 - Magnus Henoch - mange@freemail.hu
+;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net
+
+;; SSL-Connection Parts:
+;; Copyright (C) 2005 - Georg Lehner - jorge@magma.com.ni
+
+;; This file is a part of jabber.el.
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 2 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software
+;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+(require 'cl)
+
+(require 'jabber-util)
+(require 'jabber-logon)
+(require 'jabber-conn)
+(eval-and-compile
+ (or (ignore-errors (require 'fsm))
+ (ignore-errors
+ (let ((load-path (cons (expand-file-name
+ "jabber-fallback-lib"
+ (file-name-directory (locate-library "jabber")))
+ load-path)))
+ (require 'fsm)))
+ (error
+ "fsm not found in `load-path' or jabber-fallback-lib/ directory.")))
+
+(require 'jabber-sasl)
+(require 'jabber-console)
+
+(defvar jabber-connections nil
+ "List of jabber-connection FSMs.")
+
+(defvar *jabber-roster* nil
+ "the roster list")
+
+(defvar jabber-jid-obarray (make-vector 127 0)
+ "obarray for keeping JIDs")
+
+(defvar *jabber-disconnecting* nil
+ "boolean - are we in the process of disconnecting by free will")
+
+(defvar jabber-message-chain nil
+ "Incoming messages are sent to these functions, in order.")
+
+(defvar jabber-iq-chain nil
+ "Incoming infoqueries are sent to these functions, in order.")
+
+(defvar jabber-presence-chain nil
+ "Incoming presence notifications are sent to these functions, in order.")
+
+(defvar jabber-namespace-prefixes nil
+ "XML namespace prefixes used for the current connection.")
+(make-variable-buffer-local 'jabber-namespace-prefixes)
+
+(defgroup jabber-core nil "customize core functionality"
+ :group 'jabber)
+
+(defcustom jabber-post-connect-hooks '(jabber-send-current-presence
+ jabber-muc-autojoin
+ jabber-whitespace-ping-start
+ jabber-vcard-avatars-find-current)
+ "*Hooks run after successful connection and authentication.
+The functions should accept one argument, the connection object."
+ :type 'hook
+ :options '(jabber-send-current-presence
+ jabber-muc-autojoin
+ jabber-whitespace-ping-start
+ jabber-keepalive-start
+ jabber-vcard-avatars-find-current
+ jabber-autoaway-start)
+ :group 'jabber-core)
+
+(defcustom jabber-pre-disconnect-hook nil
+ "*Hooks run just before voluntary disconnection
+This might be due to failed authentication."
+ :type 'hook
+ :group 'jabber-core)
+
+(defcustom jabber-lost-connection-hooks nil
+ "*Hooks run after involuntary disconnection.
+The functions are called with one argument: the connection object."
+ :type 'hook
+ :group 'jabber-core)
+
+(defcustom jabber-post-disconnect-hook nil
+ "*Hooks run after disconnection"
+ :type 'hook
+ :group 'jabber-core)
+
+(defcustom jabber-auto-reconnect nil
+ "Reconnect automatically after losing connection?
+This will be of limited use unless you have the password library
+installed, and have configured it to cache your password
+indefinitely. See `password-cache' and `password-cache-expiry'."
+ :type 'boolean
+ :group 'jabber-core)
+
+(defcustom jabber-reconnect-delay 5
+ "Seconds to wait before reconnecting"
+ :type 'integer
+ :group 'jabber-core)
+
+(defcustom jabber-roster-buffer "*-jabber-roster-*"
+ "The name of the roster buffer"
+ :type 'string
+ :group 'jabber-core)
+
+(defvar jabber-process-buffer " *-jabber-process-*"
+ "The name of the process buffer")
+
+(defcustom jabber-use-sasl t
+ "If non-nil, use SASL if possible.
+SASL will still not be used if the library for it is missing or
+if the server doesn't support it.
+
+Disabling this shouldn't be necessary, but it may solve certain
+problems."
+ :type 'boolean
+ :group 'jabber-core)
+
+(defsubst jabber-have-sasl-p ()
+ "Return non-nil if SASL functions are available."
+ (featurep 'sasl))
+
+(defvar jabber-account-history ()
+ "Keeps track of previously used jabber accounts")
+
+(defvar jabber-connection-type-history ()
+ "Keeps track of previously used connection types")
+
+;; jabber-connect and jabber-connect-all should load jabber.el, not
+;; just jabber-core.el, when autoloaded.
+
+;;;###autoload (autoload 'jabber-connect-all "jabber" "Connect to all configured Jabber accounts.\nSee `jabber-account-list'.\nIf no accounts are configured (or ARG supplied), call `jabber-connect' interactively." t)
+(defun jabber-connect-all (&optional arg)
+ "Connect to all configured Jabber accounts.
+See `jabber-account-list'.
+If no accounts are configured (or with prefix argument), call `jabber-connect' interactively.
+With many prefix arguments, one less is passed to `jabber-connect'."
+ (interactive "P")
+ (let ((accounts
+ (remove-if (lambda (account)
+ (cdr (assq :disabled (cdr account))))
+ jabber-account-list)))
+ (if (or (null accounts) arg)
+ (let ((current-prefix-arg
+ (cond
+ ;; A number of C-u's; remove one, so to speak.
+ ((consp arg)
+ (if (> (car arg) 4)
+ (list (/ (car arg) 4))
+ nil))
+ ;; Otherwise, we just don't care.
+ (t
+ arg))))
+ (call-interactively 'jabber-connect))
+ ;; Only connect those accounts that are not yet connected.
+ (let ((already-connected (mapcar #'jabber-connection-original-jid jabber-connections))
+ (connected-one nil))
+ (dolist (account accounts)
+ (unless (member (jabber-jid-user (car account)) already-connected)
+ (let* ((jid (car account))
+ (alist (cdr account))
+ (password (cdr (assq :password alist)))
+ (network-server (cdr (assq :network-server alist)))
+ (port (cdr (assq :port alist)))
+ (connection-type (cdr (assq :connection-type alist))))
+ (jabber-connect
+ (jabber-jid-username jid)
+ (jabber-jid-server jid)
+ (jabber-jid-resource jid)
+ nil password network-server
+ port connection-type)
+ (setq connected-one t))))
+ (unless connected-one
+ (message "All configured Jabber accounts are already connected"))))))
+
+;;;###autoload (autoload 'jabber-connect "jabber" "Connect to the Jabber server and start a Jabber XML stream.\nWith prefix argument, register a new account.\nWith double prefix argument, specify more connection details." t)
+(defun jabber-connect (username server resource &optional
+ registerp password network-server
+ port connection-type)
+ "Connect to the Jabber server and start a Jabber XML stream.
+With prefix argument, register a new account.
+With double prefix argument, specify more connection details."
+ (interactive
+ (let* ((jid (completing-read "Enter your JID: " jabber-account-list nil nil nil 'jabber-account-history))
+ (entry (assoc jid jabber-account-list))
+ (alist (cdr entry))
+ password network-server port connection-type registerp)
+ (when (zerop (length jid))
+ (error "No JID specified"))
+ (unless (jabber-jid-username jid)
+ (error "Missing username part in JID"))
+ (when entry
+ ;; If the user entered the JID of one of the preconfigured
+ ;; accounts, use that data.
+ (setq password (cdr (assq :password alist)))
+ (setq network-server (cdr (assq :network-server alist)))
+ (setq port (cdr (assq :port alist)))
+ (setq connection-type (cdr (assq :connection-type alist))))
+ (when (equal current-prefix-arg '(16))
+ ;; Double prefix arg: ask about everything.
+ ;; (except password, which is asked about later anyway)
+ (setq password nil)
+ (setq network-server
+ (read-string (format "Network server: (default `%s') " network-server)
+ nil nil network-server))
+ (when (zerop (length network-server))
+ (setq network-server nil))
+ (setq port
+ (car
+ (read-from-string
+ (read-string (format "Port: (default `%s') " port)
+ nil nil (if port (number-to-string port) "nil")))))
+ (setq connection-type
+ (car
+ (read-from-string
+ (let ((default (symbol-name (or connection-type jabber-default-connection-type))))
+ (completing-read
+ (format "Connection type: (default `%s') " default)
+ (mapcar (lambda (type)
+ (cons (symbol-name (car type)) nil))
+ jabber-connect-methods)
+ nil t nil 'jabber-connection-type-history default)))))
+ (setq registerp (or jabber-silent-mode (yes-or-no-p "Register new account? "))))
+ (when (equal current-prefix-arg '(4))
+ (setq registerp t))
+
+ (list (jabber-jid-username jid)
+ (jabber-jid-server jid)
+ (jabber-jid-resource jid)
+ registerp password network-server port connection-type)))
+
+ (require 'jabber)
+
+ (if (member (list username
+ server)
+ (mapcar
+ (lambda (c)
+ (let ((data (fsm-get-state-data c)))
+ (list (plist-get data :username)
+ (plist-get data :server))))
+ jabber-connections))
+ (message "Already connected to %s@%s"
+ username server)
+ ;;(jabber-clear-roster)
+
+ (push (start-jabber-connection username server resource
+ registerp password
+ network-server port connection-type)
+ jabber-connections)))
+
+(define-state-machine jabber-connection
+ :start ((username server resource registerp password network-server port connection-type)
+ "Start a Jabber connection."
+ (let* ((connection-type
+ (or connection-type jabber-default-connection-type))
+ (send-function
+ (jabber-get-send-function connection-type)))
+
+ (list :connecting
+ (list :send-function send-function
+ ;; Save the JID we originally connected with.
+ :original-jid (concat username "@" server)
+ :username username
+ :server server
+ :resource resource
+ :password password
+ :registerp registerp
+ :connection-type connection-type
+ :encrypted (eq connection-type 'ssl)
+ :network-server network-server
+ :port port)))))
+
+(define-enter-state jabber-connection nil
+ (fsm state-data)
+ ;; `nil' is the error state.
+
+ ;; Close the network connection.
+ (let ((connection (plist-get state-data :connection)))
+ (when (processp connection)
+ (let ((process-buffer (process-buffer connection)))
+ (delete-process connection)
+ (when (and (bufferp process-buffer)
+ (not jabber-debug-keep-process-buffers))
+ (kill-buffer process-buffer)))))
+ (setq state-data (plist-put state-data :connection nil))
+ ;; Clear MUC data
+ (jabber-muc-connection-closed (jabber-connection-bare-jid fsm))
+ ;; Remove lost connections from the roster buffer.
+ (jabber-display-roster)
+ (let ((expected (plist-get state-data :disconnection-expected))
+ (reason (plist-get state-data :disconnection-reason))
+ (ever-session-established (plist-get state-data :ever-session-established)))
+ (unless expected
+ (run-hook-with-args 'jabber-lost-connection-hooks fsm)
+ (message "%s@%s%s: connection lost: `%s'"
+ (plist-get state-data :username)
+ (plist-get state-data :server)
+ (if (plist-get state-data :resource)
+ (concat "/" (plist-get state-data :resource))
+ "")
+ reason))
+
+ (if (and jabber-auto-reconnect (not expected) ever-session-established)
+ ;; Reconnect after a short delay?
+ (list state-data jabber-reconnect-delay)
+ ;; Else the connection is really dead. Remove it from the list
+ ;; of connections.
+ (setq jabber-connections
+ (delq fsm jabber-connections))
+ (when jabber-mode-line-mode
+ (jabber-mode-line-presence-update))
+ (jabber-display-roster)
+ ;; And let the FSM sleep...
+ (list state-data nil))))
+
+(define-state jabber-connection nil
+ (fsm state-data event callback)
+ ;; In the `nil' state, the connection is dead. We wait for a
+ ;; :timeout message, meaning to reconnect, or :do-disconnect,
+ ;; meaning to cancel reconnection.
+ (case event
+ (:timeout
+ (list :connecting state-data))
+ (:do-disconnect
+ (setq jabber-connections
+ (delq fsm jabber-connections))
+ (list nil state-data nil))))
+
+(define-enter-state jabber-connection :connecting
+ (fsm state-data)
+ (let* ((connection-type (plist-get state-data :connection-type))
+ (connect-function (jabber-get-connect-function connection-type))
+ (server (plist-get state-data :server))
+ (network-server (plist-get state-data :network-server))
+ (port (plist-get state-data :port)))
+ (funcall connect-function fsm server network-server port))
+ (list state-data nil))
+
+(define-state jabber-connection :connecting
+ (fsm state-data event callback)
+ (case (or (car-safe event) event)
+ (:connected
+ (let ((connection (cadr event))
+ (registerp (plist-get state-data :registerp)))
+
+ (setq state-data (plist-put state-data :connection connection))
+
+ (when (processp connection)
+ ;; TLS connections leave data in the process buffer, which
+ ;; the XML parser will choke on.
+ (with-current-buffer (process-buffer connection)
+ (erase-buffer))
+
+ (set-process-filter connection (fsm-make-filter fsm))
+ (set-process-sentinel connection (fsm-make-sentinel fsm)))
+
+ (list :connected state-data)))
+
+ (:connection-failed
+ (message "Jabber connection failed")
+ (plist-put state-data :disconnection-reason
+ (mapconcat #'identity (cadr event) "; "))
+ (list nil state-data))
+
+ (:do-disconnect
+ ;; We don't have the connection object, so defer the disconnection.
+ :defer)))
+
+(defsubst jabber-fsm-handle-sentinel (state-data event)
+ "Handle sentinel event for jabber fsm."
+ ;; We do the same thing for every state, so avoid code duplication.
+ (let* ((string (car (cddr event)))
+ ;; The event string sometimes (always?) has a trailing
+ ;; newline, that we don't care for.
+ (trimmed-string
+ (if (eq ?\n (aref string (1- (length string))))
+ (substring string 0 -1)
+ string))
+ (new-state-data
+ ;; If we already know the reason (e.g. a stream error), don't
+ ;; overwrite it.
+ (if (plist-get state-data :disconnection-reason)
+ state-data
+ (plist-put state-data :disconnection-reason trimmed-string))))
+ (list nil new-state-data)))
+
+(define-enter-state jabber-connection :connected
+ (fsm state-data)
+
+ (jabber-send-stream-header fsm)
+
+ ;; Next thing happening is the server sending its own <stream:stream> start tag.
+
+ (list state-data nil))
+
+(define-state jabber-connection :connected
+ (fsm state-data event callback)
+ (case (or (car-safe event) event)
+ (:filter
+ (let ((process (cadr event))
+ (string (car (cddr event))))
+ (jabber-pre-filter process string fsm)
+ (list :connected state-data)))
+
+ (:sentinel
+ (jabber-fsm-handle-sentinel state-data event))
+
+ (:stream-start
+ (let ((session-id (cadr event))
+ (stream-version (car (cddr event))))
+ (setq state-data
+ (plist-put state-data :session-id session-id))
+ ;; the stream feature is only sent if the initiating entity has
+ ;; sent 1.0 in the stream header. if sasl is not supported then
+ ;; we don't send 1.0 in the header and therefore we shouldn't wait
+ ;; even if 1.0 is present in the receiving stream.
+ (cond
+ ;; Wait for stream features?
+ ((and stream-version
+ (>= (string-to-number stream-version) 1.0)
+ jabber-use-sasl
+ (jabber-have-sasl-p))
+ ;; Stay in same state...
+ (list :connected state-data))
+ ;; Register account?
+ ((plist-get state-data :registerp)
+ ;; XXX: require encryption for registration?
+ (list :register-account state-data))
+ ;; Legacy authentication?
+ (t
+ (list :legacy-auth state-data)))))
+
+ (:stanza
+ (let ((stanza (cadr event)))
+ (cond
+ ;; At this stage, we only expect a stream:features stanza.
+ ((not (eq (jabber-xml-node-name stanza) 'features))
+ (list nil (plist-put state-data
+ :disconnection-reason
+ (format "Unexpected stanza %s" stanza))))
+ ((and (jabber-xml-get-children stanza 'starttls)
+ (eq (plist-get state-data :connection-type) 'starttls))
+ (list :starttls state-data))
+ ;; XXX: require encryption for registration?
+ ((plist-get state-data :registerp)
+ ;; We could check for the <register/> element in stream
+ ;; features, but as a client we would only lose by doing
+ ;; that.
+ (list :register-account state-data))
+ (t
+ (list :sasl-auth (plist-put state-data :stream-features stanza))))))
+
+ (:do-disconnect
+ (jabber-send-string fsm "</stream:stream>")
+ (list nil (plist-put state-data
+ :disconnection-expected t)))))
+
+(define-enter-state jabber-connection :starttls
+ (fsm state-data)
+ (jabber-starttls-initiate fsm)
+ (list state-data nil))
+
+(define-state jabber-connection :starttls
+ (fsm state-data event callback)
+ (case (or (car-safe event) event)
+ (:filter
+ (let ((process (cadr event))
+ (string (car (cddr event))))
+ (jabber-pre-filter process string fsm)
+ (list :starttls state-data)))
+
+ (:sentinel
+ (jabber-fsm-handle-sentinel state-data event))
+
+ (:stanza
+ (condition-case e
+ (progn
+ (jabber-starttls-process-input fsm (cadr event))
+ ;; Connection is encrypted. Send a stream tag again.
+ (list :connected (plist-put state-data :encrypted t)))
+ (error
+ (let* ((msg (concat "STARTTLS negotiation failed: "
+ (error-message-string e)))
+ (new-state-data (plist-put state-data :disconnection-reason msg)))
+ (list nil new-state-data)))))
+
+ (:do-disconnect
+ (jabber-send-string fsm "</stream:stream>")
+ (list nil (plist-put state-data
+ :disconnection-expected t)))))
+
+(define-enter-state jabber-connection :register-account
+ (fsm state-data)
+ (jabber-get-register fsm nil)
+ (list state-data nil))
+
+(define-state jabber-connection :register-account
+ (fsm state-data event callback)
+ ;; The connection will be closed in jabber-register
+ (case (or (car-safe event) event)
+ (:filter
+ (let ((process (cadr event))
+ (string (car (cddr event))))
+ (jabber-pre-filter process string fsm)
+ (list :register-account state-data)))
+
+ (:sentinel
+ (jabber-fsm-handle-sentinel state-data event))
+
+ (:stanza
+ (or
+ (jabber-process-stream-error (cadr event) state-data)
+ (progn
+ (jabber-process-input fsm (cadr event))
+ (list :register-account state-data))))
+
+ (:do-disconnect
+ (jabber-send-string fsm "</stream:stream>")
+ (list nil (plist-put state-data
+ :disconnection-expected t)))))
+
+(define-enter-state jabber-connection :legacy-auth
+ (fsm state-data)
+ (jabber-get-auth fsm (plist-get state-data :server)
+ (plist-get state-data :session-id))
+ (list state-data nil))
+
+(define-state jabber-connection :legacy-auth
+ (fsm state-data event callback)
+ (case (or (car-safe event) event)
+ (:filter
+ (let ((process (cadr event))
+ (string (car (cddr event))))
+ (jabber-pre-filter process string fsm)
+ (list :legacy-auth state-data)))
+
+ (:sentinel
+ (jabber-fsm-handle-sentinel state-data event))
+
+ (:stanza
+ (or
+ (jabber-process-stream-error (cadr event) state-data)
+ (progn
+ (jabber-process-input fsm (cadr event))
+ (list :legacy-auth state-data))))
+
+ (:authentication-success
+ (jabber-cache-password (jabber-connection-bare-jid fsm) (cdr event))
+ (list :session-established state-data))
+
+ (:authentication-failure
+ (jabber-uncache-password (jabber-connection-bare-jid fsm))
+ ;; jabber-logon has already displayed a message
+ (list nil (plist-put state-data
+ :disconnection-expected t)))
+
+ (:do-disconnect
+ (jabber-send-string fsm "</stream:stream>")
+ (list nil (plist-put state-data
+ :disconnection-expected t)))))
+
+(define-enter-state jabber-connection :sasl-auth
+ (fsm state-data)
+ (let ((new-state-data
+ (plist-put state-data
+ :sasl-data
+ (jabber-sasl-start-auth
+ fsm
+ (plist-get state-data
+ :stream-features)))))
+ (list new-state-data nil)))
+
+(define-state jabber-connection :sasl-auth
+ (fsm state-data event callback)
+ (case (or (car-safe event) event)
+ (:filter
+ (let ((process (cadr event))
+ (string (car (cddr event))))
+ (jabber-pre-filter process string fsm)
+ (list :sasl-auth state-data)))
+
+ (:sentinel
+ (jabber-fsm-handle-sentinel state-data event))
+
+ (:stanza
+ (let ((new-sasl-data
+ (jabber-sasl-process-input
+ fsm (cadr event)
+ (plist-get state-data :sasl-data))))
+ (list :sasl-auth (plist-put state-data :sasl-data new-sasl-data))))
+
+ (:use-legacy-auth-instead
+ (list :legacy-auth (plist-put state-data :sasl-data nil)))
+
+ (:authentication-success
+ (jabber-cache-password (jabber-connection-bare-jid fsm) (cdr event))
+ (list :bind (plist-put state-data :sasl-data nil)))
+
+ (:authentication-failure
+ (jabber-uncache-password (jabber-connection-bare-jid fsm))
+ ;; jabber-sasl has already displayed a message
+ (list nil (plist-put state-data
+ :disconnection-expected t)))
+
+ (:do-disconnect
+ (jabber-send-string fsm "</stream:stream>")
+ (list nil (plist-put state-data
+ :disconnection-expected t)))))
+
+(define-enter-state jabber-connection :bind
+ (fsm state-data)
+ (jabber-send-stream-header fsm)
+ (list state-data nil))
+
+(define-state jabber-connection :bind
+ (fsm state-data event callback)
+ (case (or (car-safe event) event)
+ (:filter
+ (let ((process (cadr event))
+ (string (car (cddr event))))
+ (jabber-pre-filter process string fsm)
+ (list :bind state-data)))
+
+ (:sentinel
+ (jabber-fsm-handle-sentinel state-data event))
+
+ (:stream-start
+ ;; we wait for stream features...
+ (list :bind state-data))
+
+ (:stanza
+ (let ((stanza (cadr event)))
+ (cond
+ ((eq (jabber-xml-node-name stanza) 'features)
+ ;; Record stream features, discarding earlier data:
+ (setq state-data (plist-put state-data :stream-features stanza))
+ (if (jabber-xml-get-children stanza 'bind)
+ (let ((handle-bind
+ (lambda (jc xml-data success)
+ (fsm-send jc (list
+ (if success :bind-success :bind-failure)
+ xml-data))))
+ ;; So let's bind a resource. We can either pick a resource ourselves,
+ ;; or have the server pick one for us.
+ (resource (plist-get state-data :resource)))
+ (jabber-send-iq fsm nil "set"
+ `(bind ((xmlns . "urn:ietf:params:xml:ns:xmpp-bind"))
+ ,@(when resource
+ `((resource () ,resource))))
+ handle-bind t
+ handle-bind nil)
+ (list :bind state-data))
+ (message "Server doesn't permit resource binding")
+ (list nil state-data)))
+ (t
+ (or
+ (jabber-process-stream-error (cadr event) state-data)
+ (progn
+ (jabber-process-input fsm (cadr event))
+ (list :bind state-data)))))))
+
+ (:bind-success
+ (let ((jid (jabber-xml-path (cadr event) '(bind jid ""))))
+ ;; Maybe this isn't the JID we asked for.
+ (plist-put state-data :username (jabber-jid-username jid))
+ (plist-put state-data :server (jabber-jid-server jid))
+ (plist-put state-data :resource (jabber-jid-resource jid)))
+
+ ;; If the server follows the older RFCs 3920 and 3921, it may
+ ;; offer session initiation here. If it follows RFCs 6120 and
+ ;; 6121, it might not offer it, and we should just skip it.
+ (if (jabber-xml-get-children (plist-get state-data :stream-features) 'session)
+ (let ((handle-session
+ (lambda (jc xml-data success)
+ (fsm-send jc (list
+ (if success :session-success :session-failure)
+ xml-data)))))
+ (jabber-send-iq fsm nil "set"
+ '(session ((xmlns . "urn:ietf:params:xml:ns:xmpp-session")))
+ handle-session t
+ handle-session nil)
+ (list :bind state-data))
+ ;; Session establishment not offered - assume not necessary.
+ (list :session-established state-data)))
+
+ (:session-success
+ ;; We have a session
+ (list :session-established state-data))
+
+ (:bind-failure
+ (message "Resource binding failed: %s"
+ (jabber-parse-error
+ (jabber-iq-error (cadr event))))
+ (list nil state-data))
+
+ (:session-failure
+ (message "Session establishing failed: %s"
+ (jabber-parse-error
+ (jabber-iq-error (cadr event))))
+ (list nil state-data))
+
+ (:do-disconnect
+ (jabber-send-string fsm "</stream:stream>")
+ (list nil (plist-put state-data
+ :disconnection-expected t)))))
+
+(define-enter-state jabber-connection :session-established
+ (fsm state-data)
+ (jabber-send-iq fsm nil
+ "get"
+ '(query ((xmlns . "jabber:iq:roster")))
+ #'jabber-process-roster 'initial
+ #'jabber-initial-roster-failure nil)
+ (list (plist-put state-data :ever-session-established t) nil))
+
+(defvar jabber-pending-presence-timeout 0.5
+ "Wait this long before doing presence packet batch processing.")
+
+(define-state jabber-connection :session-established
+ (fsm state-data event callback)
+ (case (or (car-safe event) event)
+ (:filter
+ (let ((process (cadr event))
+ (string (car (cddr event))))
+ (jabber-pre-filter process string fsm)
+ (list :session-established state-data :keep)))
+
+ (:sentinel
+ (jabber-fsm-handle-sentinel state-data event))
+
+ (:stanza
+ (or
+ (jabber-process-stream-error (cadr event) state-data)
+ (progn
+ (jabber-process-input fsm (cadr event))
+ (list :session-established state-data :keep))))
+
+ (:roster-update
+ ;; Batch up roster updates
+ (let* ((jid-symbol-to-update (cdr event))
+ (pending-updates (plist-get state-data :roster-pending-updates)))
+ ;; If there are pending updates, there is a timer running
+ ;; already; just add the new symbol and wait.
+ (if pending-updates
+ (progn
+ (unless (memq jid-symbol-to-update pending-updates)
+ (nconc pending-updates (list jid-symbol-to-update)))
+ (list :session-established state-data :keep))
+ ;; Otherwise, we need to create the list and start the timer.
+ (setq state-data
+ (plist-put state-data
+ :roster-pending-updates
+ (list jid-symbol-to-update)))
+ (list :session-established state-data jabber-pending-presence-timeout))))
+
+ (:timeout
+ ;; Update roster
+ (let ((pending-updates (plist-get state-data :roster-pending-updates)))
+ (setq state-data (plist-put state-data :roster-pending-updates nil))
+ (jabber-roster-update fsm nil pending-updates nil)
+ (list :session-established state-data)))
+
+ (:send-if-connected
+ ;; This is the only state in which we respond to such messages.
+ ;; This is to make sure we don't send anything inappropriate
+ ;; during authentication etc.
+ (jabber-send-sexp fsm (cdr event))
+ (list :session-established state-data :keep))
+
+ (:do-disconnect
+ (jabber-send-string fsm "</stream:stream>")
+ (list nil (plist-put state-data
+ :disconnection-expected t)))))
+
+(defun jabber-disconnect (&optional arg)
+ "Disconnect from all Jabber servers. If ARG supplied, disconnect one account."
+ (interactive "P")
+ (if arg
+ (jabber-disconnect-one (jabber-read-account))
+ (unless *jabber-disconnecting* ; avoid reentry
+ (let ((*jabber-disconnecting* t))
+ (if (null jabber-connections)
+ (message "Already disconnected")
+ (run-hooks 'jabber-pre-disconnect-hook)
+ (dolist (c jabber-connections)
+ (jabber-disconnect-one c t))
+ (setq jabber-connections nil)
+
+ (jabber-disconnected)
+ (when (interactive-p)
+ (message "Disconnected from Jabber server(s)")))))))
+
+(defun jabber-disconnect-one (jc &optional dont-redisplay)
+ "Disconnect from one Jabber server.
+If DONT-REDISPLAY is non-nil, don't update roster buffer."
+ (interactive (list (jabber-read-account)))
+ (fsm-send-sync jc :do-disconnect)
+ (when (interactive-p)
+ (message "Disconnected from %s"
+ (jabber-connection-jid jc)))
+ (unless dont-redisplay
+ (jabber-display-roster)))
+
+(defun jabber-disconnected ()
+ "Re-initialise jabber package variables.
+Call this function after disconnection."
+ (when (get-buffer jabber-roster-buffer)
+ (with-current-buffer (get-buffer jabber-roster-buffer)
+ (let ((inhibit-read-only t))
+ (erase-buffer))))
+
+ (jabber-clear-roster)
+ (run-hooks 'jabber-post-disconnect-hook))
+
+(defun jabber-log-xml (fsm direction data)
+ "Print DATA to XML console (and, optionally, in file).
+If `jabber-debug-log-xml' is nil, do nothing.
+FSM is the connection that is sending/receiving.
+DIRECTION is a string, either \"sending\" or \"receive\".
+DATA is any sexp."
+ (when jabber-debug-log-xml
+ (jabber-process-console fsm direction data)))
+
+(defun jabber-pre-filter (process string fsm)
+ (with-current-buffer (process-buffer process)
+ ;; Append new data
+ (goto-char (point-max))
+ (insert string)
+
+ (unless (boundp 'jabber-filtering)
+ (let (jabber-filtering)
+ (jabber-filter process fsm)))))
+
+(defun jabber-filter (process fsm)
+ "the filter function for the jabber process"
+ (with-current-buffer (process-buffer process)
+ ;; Start from the beginning
+ (goto-char (point-min))
+ (let (xml-data)
+ (loop
+ do
+ ;; Skip whitespace
+ (unless (zerop (skip-chars-forward " \t\r\n"))
+ (delete-region (point-min) (point)))
+ ;; Skip processing directive
+ (when (looking-at "<\\?xml[^?]*\\?>")
+ (delete-region (match-beginning 0) (match-end 0)))
+
+ ;; Stream end?
+ (when (looking-at "</stream:stream>")
+ (return (fsm-send fsm :stream-end)))
+
+ ;; Stream header?
+ (when (looking-at "<stream:stream[^>]*\\(>\\)")
+ ;; Let's pretend that the stream header is a closed tag,
+ ;; and parse it as such.
+ (replace-match "/>" t t nil 1)
+ (let* ((ending-at (point))
+ (stream-header (car (xml-parse-region (point-min) ending-at)))
+ (session-id (jabber-xml-get-attribute stream-header 'id))
+ (stream-version (jabber-xml-get-attribute stream-header 'version)))
+
+ ;; Need to keep any namespace attributes on the stream
+ ;; header, as they can affect any stanza in the
+ ;; stream...
+ (setq jabber-namespace-prefixes
+ (jabber-xml-merge-namespace-declarations
+ (jabber-xml-node-attributes stream-header)
+ nil))
+ (jabber-log-xml fsm "receive" stream-header)
+ (fsm-send fsm (list :stream-start session-id stream-version))
+ (delete-region (point-min) ending-at)))
+
+ ;; Normal tag
+
+ ;; XXX: do these checks make sense? If so, reinstate them.
+ ;;(if (active-minibuffer-window)
+ ;; (run-with-idle-timer 0.01 nil #'jabber-filter process string)
+
+ ;; This check is needed for xml.el of Emacs 21, as it chokes on
+ ;; empty attribute values.
+ (save-excursion
+ (while (search-forward-regexp " \\w+=''" nil t)
+ (replace-match "")))
+
+ (setq xml-data (jabber-xml-parse-next-stanza))
+
+ while xml-data
+ do
+ ;; If there's a problem with writing the XML log,
+ ;; make sure the stanza is delivered, at least.
+ (condition-case e
+ (jabber-log-xml fsm "receive" (car xml-data))
+ (error
+ (ding)
+ (message "Couldn't write XML log: %s" (error-message-string e))
+ (sit-for 2)))
+ (delete-region (point-min) (point))
+
+ (fsm-send fsm (list :stanza
+ (jabber-xml-resolve-namespace-prefixes
+ (car xml-data) nil jabber-namespace-prefixes)))
+ ;; XXX: move this logic elsewhere
+ ;; We explicitly don't catch errors in jabber-process-input,
+ ;; to facilitate debugging.
+ ;; (jabber-process-input (car xml-data))
+ ))))
+
+(defun jabber-process-input (jc xml-data)
+ "process an incoming parsed tag"
+ (let* ((tag (jabber-xml-node-name xml-data))
+ (functions (eval (cdr (assq tag '((iq . jabber-iq-chain)
+ (presence . jabber-presence-chain)
+ (message . jabber-message-chain)))))))
+ (dolist (f functions)
+ (condition-case e
+ (funcall f jc xml-data)
+ ((debug error)
+ (fsm-debug-output "Error %S while processing %S with function %s" e xml-data f))))))
+
+(defun jabber-process-stream-error (xml-data state-data)
+ "Process an incoming stream error.
+Return nil if XML-DATA is not a stream:error stanza.
+Return an fsm result list if it is."
+ (when (and (eq (jabber-xml-node-name xml-data) 'error)
+ (equal (jabber-xml-get-xmlns xml-data) "http://etherx.jabber.org/streams"))
+ (let ((condition (jabber-stream-error-condition xml-data))
+ (text (jabber-parse-stream-error xml-data)))
+ (setq state-data (plist-put state-data :disconnection-reason
+ (format "Stream error: %s" text)))
+ ;; Special case: when the error is `conflict', we have been
+ ;; forcibly disconnected by the same user. Don't reconnect
+ ;; automatically.
+ (when (eq condition 'conflict)
+ (setq state-data (plist-put state-data :disconnection-expected t)))
+ (list nil state-data))))
+
+;; XXX: This function should probably die. The roster is stored
+;; inside the connection plists, and the obarray shouldn't be so big
+;; that we need to clean it.
+(defun jabber-clear-roster ()
+ "Clean up the roster."
+ ;; This is made complicated by the fact that the JIDs are symbols with properties.
+ (mapatoms #'(lambda (x)
+ (unintern x jabber-jid-obarray))
+ jabber-jid-obarray)
+ (setq *jabber-roster* nil))
+
+(defun jabber-send-sexp (jc sexp)
+ "Send the xml corresponding to SEXP to connection JC."
+ (condition-case e
+ (jabber-log-xml jc "sending" sexp)
+ (error
+ (ding)
+ (message "Couldn't write XML log: %s" (error-message-string e))
+ (sit-for 2)))
+ (jabber-send-string jc (jabber-sexp2xml sexp)))
+
+(defun jabber-send-sexp-if-connected (jc sexp)
+ "Send the stanza SEXP only if JC has established a session."
+ (fsm-send-sync jc (cons :send-if-connected sexp)))
+
+(defun jabber-send-stream-header (jc)
+ "Send stream header to connection JC."
+ (let ((stream-header
+ (concat "<?xml version='1.0'?><stream:stream to='"
+ (plist-get (fsm-get-state-data jc) :server)
+ "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'"
+ ;; Not supporting SASL is not XMPP compliant,
+ ;; so don't pretend we are.
+ (if (and (jabber-have-sasl-p) jabber-use-sasl)
+ " version='1.0'"
+ "")
+ ">
+")))
+ (jabber-log-xml jc "sending" stream-header)
+ (jabber-send-string jc stream-header)))
+
+(defun jabber-send-string (jc string)
+ "Send STRING to the connection JC."
+ (let* ((state-data (fsm-get-state-data jc))
+ (connection (plist-get state-data :connection))
+ (send-function (plist-get state-data :send-function)))
+ (unless connection
+ (error "%s has no connection" (jabber-connection-jid jc)))
+ (funcall send-function connection string)))
+
+(provide 'jabber-core)
+
+;;; arch-tag: 9d273ce6-c45a-447b-abf3-21d3ce73a51a