diff options
Diffstat (limited to 'jabber-util.el')
-rw-r--r-- | jabber-util.el | 772 |
1 files changed, 772 insertions, 0 deletions
diff --git a/jabber-util.el b/jabber-util.el new file mode 100644 index 0000000..40d2b20 --- /dev/null +++ b/jabber-util.el @@ -0,0 +1,772 @@ +;; jabber-util.el - various utility functions -*- coding: utf-8; -*- + +;; Copyright (C) 2003, 2004, 2007, 2008 - Magnus Henoch - mange@freemail.hu +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2008, 2010 - Terechkov Evgenii - evg@altlinux.org +;; Copyright (C) 2010 - Kirill A. Korinskiy - catap@catap.ru + +;; 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) +(condition-case nil + (require 'password) + (error nil)) +(condition-case nil + (require 'auth-source) + (error nil)) + +(defvar jabber-jid-history nil + "History of entered JIDs") + +;; Define `jabber-replace-in-string' somehow. +(cond + ;; Emacs 21 has replace-regexp-in-string. + ((fboundp 'replace-regexp-in-string) + (defsubst jabber-replace-in-string (str regexp newtext) + (replace-regexp-in-string regexp newtext str t t))) + ;; XEmacs has replace-in-string. However, color-theme defines it as + ;; well on Emacs 2x, so this check must be last. + ((fboundp 'replace-in-string) + ;; And the version in color-theme takes only three arguments. Check + ;; just to be sure. + (condition-case nil + (replace-in-string "foobar" "foo" "bar" t) + (wrong-number-of-arguments + (error "`replace-in-string' doesn't accept fourth argument"))) + (defsubst jabber-replace-in-string (str regexp newtext) + (replace-in-string str regexp newtext t))) + (t + (error "No implementation of `jabber-replace-in-string' available"))) + +;;; XEmacs compatibility. Stolen from ibuffer.el +(if (fboundp 'propertize) + (defalias 'jabber-propertize 'propertize) + (defun jabber-propertize (string &rest properties) + "Return a copy of STRING with text properties added. + + [Note: this docstring has been copied from the Emacs 21 version] + +First argument is the string to copy. +Remaining arguments form a sequence of PROPERTY VALUE pairs for text +properties to add to the result." + (let ((str (copy-sequence string))) + (add-text-properties 0 (length str) + properties + str) + str))) + +(unless (fboundp 'bound-and-true-p) + (defmacro bound-and-true-p (var) + "Return the value of symbol VAR if it is bound, else nil." + `(and (boundp (quote ,var)) ,var))) + +;;; more XEmacs compatibility +;;; Preserve input method when entering a minibuffer +(if (featurep 'xemacs) + ;; I don't know how to do this + (defsubst jabber-read-with-input-method (prompt &optional initial-contents history default-value) + (read-string prompt initial-contents history default-value)) + (defsubst jabber-read-with-input-method (prompt &optional initial-contents history default-value) + (read-string prompt initial-contents history default-value t))) + +(unless (fboundp 'delete-and-extract-region) + (defsubst delete-and-extract-region (start end) + (prog1 + (buffer-substring start end) + (delete-region start end)))) + +(unless (fboundp 'access-file) + (defsubst access-file (filename error-message) + (unless (file-readable-p filename) + (error error-message)))) + +(if (fboundp 'float-time) + (defalias 'jabber-float-time 'float-time) + (defun jabber-float-time (&optional specified-time) + (unless specified-time + (setq specified-time (current-time))) + ;; second precision is good enough for us + (+ (* 65536.0 (car specified-time)) + (cadr specified-time)))) + +(cond + ((fboundp 'cancel-timer) + (defalias 'jabber-cancel-timer 'cancel-timer)) + ((fboundp 'delete-itimer) + (defalias 'jabber-cancel-timer 'delete-itimer)) + (t + (error "No `cancel-timer' function found"))) + +(defun jabber-concat-rosters () + "Concatenate the rosters of all connected accounts." + (apply #'append + (mapcar + (lambda (jc) + (plist-get (fsm-get-state-data jc) :roster)) + jabber-connections))) + +(defun jabber-concat-rosters-full () + "Concatenate the rosters of all connected accounts. Show full jids (with resources)" + (let ((jids (apply #'append + (mapcar + (lambda (jc) + (plist-get (fsm-get-state-data jc) :roster)) + jabber-connections)))) + (apply #'append + (mapcar (lambda (jid) + (mapcar (lambda (res) (intern (format "%s/%s" jid (car res)))) + (get (jabber-jid-symbol jid) 'resources))) + jids)))) + +(defun jabber-connection-jid (jc) + "Return the full JID of the given connection." + (let ((sd (fsm-get-state-data jc))) + (concat (plist-get sd :username) "@" + (plist-get sd :server) "/" + (plist-get sd :resource)))) + +(defun jabber-connection-bare-jid (jc) + "Return the bare JID of the given connection." + (let ((sd (fsm-get-state-data jc))) + (concat (plist-get sd :username) "@" + (plist-get sd :server)))) + +(defun jabber-connection-original-jid (jc) + "Return the original JID of the given connection. +The \"original JID\" is the JID we authenticated with. The +server might subsequently assign us a different JID at resource +binding." + (plist-get (fsm-get-state-data jc) :original-jid)) + +(defun jabber-find-connection (bare-jid) + "Find the connection to the account named by BARE-JID. +Return nil if none found." + (dolist (jc jabber-connections) + (when (string= bare-jid (jabber-connection-bare-jid jc)) + (return jc)))) + +(defun jabber-find-active-connection (dead-jc) + "Given a dead connection, find an active connection to the same account. +Return nil if none found." + (let ((jid (jabber-connection-bare-jid dead-jc))) + (jabber-find-connection jid))) + +(defun jabber-jid-username (string) + "return the username portion of a JID, or nil if no username" + (when (string-match "\\(.*\\)@.*\\(/.*\\)?" string) + (match-string 1 string))) + +(defun jabber-jid-user (string) + "return the user (username@server) portion of a JID" + ;;transports don't have @, so don't require it + ;;(string-match ".*@[^/]*" string) + (string-match "[^/]*" string) + (match-string 0 string)) + +(defun jabber-jid-server (string) + "Return the server portion of a JID." + (string-match "^\\(.*@\\)?\\([^@/]+\\)\\(/.*\\)?$" string) + (match-string 2 string)) + +(defun jabber-jid-rostername (string) + "return the name of the user, if given in roster, else nil" + (let ((user (jabber-jid-symbol string))) + (if (> (length (get user 'name)) 0) + (get user 'name)))) + +(defun jabber-jid-displayname (string) + "return the name of the user, if given in roster, else username@server" + (or (jabber-jid-rostername string) + (jabber-jid-user (if (symbolp string) + (symbol-name string) + string)))) + +(defun jabber-jid-bookmarkname (string) + "Return the conference name from boomarks or displayname from roster, or JID if none set" + (require 'jabber-bookmarks) + (or (loop for conference in (first (loop for value being the hash-values of jabber-bookmarks + collect value)) + do (let ((ls (cadr conference))) + (if (string= (cdr (assoc 'jid ls)) string) + (return (cdr (assoc 'name ls)))))) + (jabber-jid-displayname string))) + +(defun jabber-jid-resource (string) + "return the resource portion of a JID, or nil if there is none." + (when (string-match "^\\(\\([^/]*@\\)?[^/]*\\)/\\(.*\\)" string) + (match-string 3 string))) + +(defun jabber-jid-symbol (string) + "return the symbol for the given JID" + ;; If it's already a symbol, just return it. + (if (symbolp string) + string + ;; XXX: "downcase" is poor man's nodeprep. See XMPP CORE. + (intern (downcase (jabber-jid-user string)) jabber-jid-obarray))) + +(defun jabber-my-jid-p (jc jid) + "Return non-nil if the specified JID is in jabber-account-list (modulo resource). +Also return non-nil if JID matches JC, modulo resource." + (or + (equal (jabber-jid-user jid) + (jabber-connection-bare-jid jc)) + (member (jabber-jid-user jid) (mapcar (lambda (x) (jabber-jid-user (car x))) jabber-account-list)))) + +(defun jabber-read-jid-completing (prompt &optional subset require-match default resource fulljids) + "read a jid out of the current roster from the minibuffer. +If SUBSET is non-nil, it should be a list of symbols from which +the JID is to be selected, instead of using the entire roster. +If REQUIRE-MATCH is non-nil, the JID must be in the list used. +If DEFAULT is non-nil, it's used as the default value, otherwise +the default is inferred from context. +RESOURCE is one of the following: + +nil Accept full or bare JID, as entered +full Turn bare JIDs to full ones with highest-priority resource +bare-or-muc Turn full JIDs to bare ones, except for in MUC + +If FULLJIDS is non-nil, complete jids with resources." + (let ((jid-at-point (or + (and default + ;; default can be either a symbol or a string + (if (symbolp default) + (symbol-name default) + default)) + (let* ((jid (get-text-property (point) 'jabber-jid)) + (res (get (jabber-jid-symbol jid) 'resource))) + (when jid + (if (and fulljids res (not (jabber-jid-resource jid))) + (format "%s/%s" jid res) + jid))) + (bound-and-true-p jabber-chatting-with) + (bound-and-true-p jabber-group))) + (completion-ignore-case t) + (jid-completion-table (mapcar #'(lambda (item) + (cons (symbol-name item) item)) + (or subset (funcall (if fulljids + 'jabber-concat-rosters-full + 'jabber-concat-rosters))))) + chosen) + (dolist (item (or subset (jabber-concat-rosters))) + (if (get item 'name) + (push (cons (get item 'name) item) jid-completion-table))) + ;; if the default is not in the allowed subset, it's not a good default + (if (and subset (not (assoc jid-at-point jid-completion-table))) + (setq jid-at-point nil)) + (let ((input + (completing-read (concat prompt + (if jid-at-point + (format "(default %s) " jid-at-point))) + jid-completion-table + nil require-match nil 'jabber-jid-history jid-at-point))) + (setq chosen + (if (and input (assoc-ignore-case input jid-completion-table)) + (symbol-name (cdr (assoc-ignore-case input jid-completion-table))) + (and (not (zerop (length input))) + input)))) + + (when chosen + (case resource + (full + ;; If JID is bare, add the highest-priority resource. + (if (jabber-jid-resource chosen) + chosen + (let ((highest-resource (get (jabber-jid-symbol chosen) 'resource))) + (if highest-resource + (concat chosen "/" highest-resource) + chosen)))) + (bare-or-muc + ;; If JID is full and non-MUC, remove resource. + (if (null (jabber-jid-resource chosen)) + chosen + (let ((bare (jabber-jid-user chosen))) + (if (assoc bare *jabber-active-groupchats*) + chosen + bare)))) + (t + chosen))))) + +(defun jabber-read-node (prompt) + "Read node name, taking default from disco item at point." + (let ((node-at-point (get-text-property (point) 'jabber-node))) + (read-string (concat prompt + (if node-at-point + (format "(default %s) " node-at-point))) + node-at-point))) + +(defun jabber-password-key (bare-jid) + "Construct key for `password' library from BARE-JID." + (concat "xmpp:" bare-jid)) + +(defun jabber-read-password (bare-jid) + "Read Jabber password from minibuffer." + (let ((found + (and (fboundp 'auth-source-search) + (nth 0 (auth-source-search + :user (jabber-jid-username bare-jid) + :host (jabber-jid-server bare-jid) + :port "xmpp" + :max 1 + :require '(:secret)))))) + (if found + (let ((secret (plist-get found :secret))) + (copy-sequence + (if (functionp secret) + (funcall secret) + secret))) + (let ((prompt (format "Jabber password for %s: " bare-jid))) + (if (require 'password-cache nil t) + ;; Need to copy the password, as sasl.el wants to erase it. + (copy-sequence + (password-read prompt (jabber-password-key bare-jid))) + (read-passwd prompt)))))) + +(defun jabber-cache-password (bare-jid password) + "Cache PASSWORD for BARE-JID." + (when (fboundp 'password-cache-add) + (password-cache-add (jabber-password-key bare-jid) password))) + +(defun jabber-uncache-password (bare-jid) + "Uncache cached password for BARE-JID. +Useful if the password proved to be wrong." + (interactive (list (jabber-jid-user + (completing-read "Forget password of account: " jabber-account-list nil nil nil 'jabber-account-history)))) + (when (fboundp 'password-cache-remove) + (password-cache-remove (jabber-password-key bare-jid)))) + +(defun jabber-read-account (&optional always-ask contact-hint) + "Ask for which connected account to use. +If ALWAYS-ASK is nil and there is only one account, return that +account. +If CONTACT-HINT is a string or a JID symbol, default to an account +that has that contact in its roster." + (let ((completions + (mapcar (lambda (c) + (cons + (jabber-connection-bare-jid c) + c)) + jabber-connections))) + (cond + ((null jabber-connections) + (error "Not connected to Jabber")) + ((and (null (cdr jabber-connections)) (not always-ask)) + ;; only one account + (car jabber-connections)) + (t + (or + ;; if there is a jabber-account property at point, + ;; present it as default value + (cdr (assoc (let ((at-point (get-text-property (point) 'jabber-account))) + (when (and at-point + (memq at-point jabber-connections)) + (jabber-connection-bare-jid at-point))) completions)) + (let* ((default + (or + (and contact-hint + (setq contact-hint (jabber-jid-symbol contact-hint)) + (let ((matching + (find-if + (lambda (jc) + (memq contact-hint (plist-get (fsm-get-state-data jc) :roster))) + jabber-connections))) + (when matching + (jabber-connection-bare-jid matching)))) + ;; if the buffer is associated with a connection, use it + (when (and jabber-buffer-connection + (jabber-find-active-connection jabber-buffer-connection)) + (jabber-connection-bare-jid jabber-buffer-connection)) + ;; else, use the first connection in the list + (caar completions))) + (input (completing-read + (concat "Select Jabber account (default " + default + "): ") + completions nil t nil 'jabber-account-history + default))) + (cdr (assoc input completions)))))))) + +(defun jabber-iq-query (xml-data) + "Return the query part of an IQ stanza. +An IQ stanza may have zero or one query child, and zero or one <error/> child. +The query child is often but not always <query/>." + (let (query) + (dolist (x (jabber-xml-node-children xml-data)) + (if (and + (listp x) + (not (eq (jabber-xml-node-name x) 'error))) + (setq query x))) + query)) + +(defun jabber-iq-error (xml-data) + "Return the <error/> part of an IQ stanza, if any." + (car (jabber-xml-get-children xml-data 'error))) + +(defun jabber-iq-xmlns (xml-data) + "Return the namespace of an IQ stanza, i.e. the namespace of its query part." + (jabber-xml-get-attribute (jabber-iq-query xml-data) 'xmlns)) + +(defun jabber-message-timestamp (xml-data) + "Given a <message/> element, return its timestamp, or nil if none." + (jabber-x-delay + (or + (jabber-xml-path xml-data '(("urn:xmpp:delay" . "delay"))) + (jabber-xml-path xml-data '(("jabber:x:delay" . "x")))))) + +(defun jabber-x-delay (xml-data) + "Return timestamp given a delayed delivery element. +This can be either a <delay/> tag in namespace urn:xmpp:delay (XEP-0203), or +a <x/> tag in namespace jabber:x:delay (XEP-0091). +Return nil if no such data available." + (cond + ((and (eq (jabber-xml-node-name xml-data) 'x) + (string= (jabber-xml-get-attribute xml-data 'xmlns) "jabber:x:delay")) + (let ((stamp (jabber-xml-get-attribute xml-data 'stamp))) + (if (and (stringp stamp) + (= (length stamp) 17)) + (jabber-parse-legacy-time stamp)))) + ((and (eq (jabber-xml-node-name xml-data) 'delay) + (string= (jabber-xml-get-attribute xml-data 'xmlns) "urn:xmpp:delay")) + (let ((stamp (jabber-xml-get-attribute xml-data 'stamp))) + (when (stringp stamp) + (jabber-parse-time stamp)))))) + +(defun jabber-parse-legacy-time (timestamp) + "Parse timestamp in ccyymmddThh:mm:ss format (UTC) and return as internal time value." + (let ((year (string-to-number (substring timestamp 0 4))) + (month (string-to-number (substring timestamp 4 6))) + (day (string-to-number (substring timestamp 6 8))) + (hour (string-to-number (substring timestamp 9 11))) + (minute (string-to-number (substring timestamp 12 14))) + (second (string-to-number (substring timestamp 15 17)))) + (encode-time second minute hour day month year 0))) + +(defun jabber-encode-legacy-time (timestamp) + "Parse TIMESTAMP as internal time value and encode as ccyymmddThh:mm:ss (UTC)." + (if (featurep 'xemacs) + ;; XEmacs doesn't have `universal' argument to format-time-string, + ;; so we have to do it ourselves. + (format-time-string "%Y%m%dT%H:%M:%S" + (time-subtract timestamp + (list 0 (car (current-time-zone))))) + (format-time-string "%Y%m%dT%H:%M:%S" timestamp t))) + +(defun jabber-encode-time (time) + "Convert TIME to a string by JEP-0082. +TIME is in a format accepted by `format-time-string'." + (format-time-string "%Y-%m-%dT%H:%M:%SZ" time t)) + +(defun jabber-encode-timezone () + (let ((time-zone-offset (nth 0 (current-time-zone)))) + (if (null time-zone-offset) + "Z" + (let* ((positivep (>= time-zone-offset 0)) + (hours (/ (abs time-zone-offset) 3600)) + (minutes (/ (% (abs time-zone-offset) 3600) 60))) + (format "%s%02d:%02d"(if positivep "+" "-") hours minutes))))) + +(defun jabber-parse-time (raw-time) + "Parse the DateTime encoded in TIME according to JEP-0082." + (let* ((time (if (string= (substring raw-time 4 5) "-") + raw-time + (concat + (substring raw-time 0 4) "-" + (substring raw-time 4 6) "-" + (substring raw-time 6 (length raw-time))))) + (year (string-to-number (substring time 0 4))) + (month (string-to-number (substring time 5 7))) + (day (string-to-number (substring time 8 10))) + (hour (string-to-number (substring time 11 13))) + (minute (string-to-number (substring time 14 16))) + (second (string-to-number (substring time 17 19))) + ;; fractions are optional + (fraction (if (eq (aref time 19) ?.) + (string-to-number (substring time 20 23)))) + (timezone (substring time (if fraction 23 19)))) + ;; timezone is either Z (UTC) or [+-]HH:MM + (let ((timezone-seconds + (if (string= timezone "Z") + 0 + (* (if (eq (aref timezone 0) ?+) 1 -1) + (* 60 (+ (* 60 (string-to-number (substring timezone 1 3))) + (string-to-number (substring timezone 4 6)))))))) + (encode-time second minute hour day month year timezone-seconds)))) + +(defun jabber-report-success (jc xml-data context) + "IQ callback reporting success or failure of the operation. +CONTEXT is a string describing the action. +\"CONTEXT succeeded\" or \"CONTEXT failed: REASON\" is displayed in +the echo area." + (let ((type (jabber-xml-get-attribute xml-data 'type))) + (message (concat context + (if (string= type "result") + " succeeded" + (concat + " failed: " + (let ((the-error (jabber-iq-error xml-data))) + (if the-error + (jabber-parse-error the-error) + "No error message given")))))))) + +(defconst jabber-error-messages + (list + (cons 'bad-request "Bad request") + (cons 'conflict "Conflict") + (cons 'feature-not-implemented "Feature not implemented") + (cons 'forbidden "Forbidden") + (cons 'gone "Gone") + (cons 'internal-server-error "Internal server error") + (cons 'item-not-found "Item not found") + (cons 'jid-malformed "JID malformed") + (cons 'not-acceptable "Not acceptable") + (cons 'not-allowed "Not allowed") + (cons 'not-authorized "Not authorized") + (cons 'payment-required "Payment required") + (cons 'recipient-unavailable "Recipient unavailable") + (cons 'redirect "Redirect") + (cons 'registration-required "Registration required") + (cons 'remote-server-not-found "Remote server not found") + (cons 'remote-server-timeout "Remote server timeout") + (cons 'resource-constraint "Resource constraint") + (cons 'service-unavailable "Service unavailable") + (cons 'subscription-required "Subscription required") + (cons 'undefined-condition "Undefined condition") + (cons 'unexpected-request "Unexpected request")) + "String descriptions of XMPP stanza errors") + +(defconst jabber-legacy-error-messages + (list + (cons 302 "Redirect") + (cons 400 "Bad request") + (cons 401 "Unauthorized") + (cons 402 "Payment required") + (cons 403 "Forbidden") + (cons 404 "Not found") + (cons 405 "Not allowed") + (cons 406 "Not acceptable") + (cons 407 "Registration required") + (cons 408 "Request timeout") + (cons 409 "Conflict") + (cons 500 "Internal server error") + (cons 501 "Not implemented") + (cons 502 "Remote server error") + (cons 503 "Service unavailable") + (cons 504 "Remote server timeout") + (cons 510 "Disconnected")) + "String descriptions of legacy errors (JEP-0086)") + +(defun jabber-parse-error (error-xml) + "Parse the given <error/> tag and return a string fit for human consumption. +See secton 9.3, Stanza Errors, of XMPP Core, and JEP-0086, Legacy Errors." + (let ((error-type (jabber-xml-get-attribute error-xml 'type)) + (error-code (jabber-xml-get-attribute error-xml 'code)) + condition text) + (if error-type + ;; If the <error/> tag has a type element, it is new-school. + (dolist (child (jabber-xml-node-children error-xml)) + (when (string= + (jabber-xml-get-attribute child 'xmlns) + "urn:ietf:params:xml:ns:xmpp-stanzas") + (if (eq (jabber-xml-node-name child) 'text) + (setq text (car (jabber-xml-node-children child))) + (setq condition + (or (cdr (assq (jabber-xml-node-name child) jabber-error-messages)) + (symbol-name (jabber-xml-node-name child))))))) + (setq condition (or (cdr (assq (string-to-number error-code) jabber-legacy-error-messages)) + error-code)) + (setq text (car (jabber-xml-node-children error-xml)))) + (concat condition + (if text (format ": %s" text))))) + +(defun jabber-error-condition (error-xml) + "Parse the given <error/> tag and return the condition symbol." + (catch 'condition + (dolist (child (jabber-xml-node-children error-xml)) + (when (string= + (jabber-xml-get-attribute child 'xmlns) + "urn:ietf:params:xml:ns:xmpp-stanzas") + (throw 'condition (jabber-xml-node-name child)))))) + +(defvar jabber-stream-error-messages + (list + (cons 'bad-format "Bad XML format") + (cons 'bad-namespace-prefix "Bad namespace prefix") + (cons 'conflict "Conflict") + (cons 'connection-timeout "Connection timeout") + (cons 'host-gone "Host gone") + (cons 'host-unknown "Host unknown") + (cons 'improper-addressing "Improper addressing") ; actually only s2s + (cons 'internal-server-error "Internal server error") + (cons 'invalid-from "Invalid from") + (cons 'invalid-id "Invalid id") + (cons 'invalid-namespace "Invalid namespace") + (cons 'invalid-xml "Invalid XML") + (cons 'not-authorized "Not authorized") + (cons 'policy-violation "Policy violation") + (cons 'remote-connection-failed "Remote connection failed") + (cons 'resource-constraint "Resource constraint") + (cons 'restricted-xml "Restricted XML") + (cons 'see-other-host "See other host") + (cons 'system-shutdown "System shutdown") + (cons 'undefined-condition "Undefined condition") + (cons 'unsupported-encoding "Unsupported encoding") + (cons 'unsupported-stanza-type "Unsupported stanza type") + (cons 'unsupported-version "Unsupported version") + (cons 'xml-not-well-formed "XML not well formed")) + "String descriptions of XMPP stream errors") + +(defun jabber-stream-error-condition (error-xml) + "Return the condition of a <stream:error/> tag." + ;; as we don't know the node name of the condition, we have to + ;; search for it. + (dolist (node (jabber-xml-node-children error-xml)) + (when (and (string= (jabber-xml-get-attribute node 'xmlns) + "urn:ietf:params:xml:ns:xmpp-streams") + (assq (jabber-xml-node-name node) + jabber-stream-error-messages)) + (return (jabber-xml-node-name node))))) + +(defun jabber-parse-stream-error (error-xml) + "Parse the given <stream:error/> tag and return a sting fit for human consumption." + (let ((text-node (car (jabber-xml-get-children error-xml 'text))) + (condition (jabber-stream-error-condition error-xml))) + (concat (if condition (cdr (assq condition jabber-stream-error-messages)) + "Unknown stream error") + (if (and text-node (stringp (car (jabber-xml-node-children text-node)))) + (concat ": " (car (jabber-xml-node-children text-node))))))) + +(put 'jabber-error + 'error-conditions + '(error jabber-error)) +(put 'jabber-error + 'error-message + "Jabber error") + +(defun jabber-signal-error (error-type condition &optional text app-specific) + "Signal an error to be sent by Jabber. +ERROR-TYPE is one of \"cancel\", \"continue\", \"modify\", \"auth\" +and \"wait\". +CONDITION is a symbol denoting a defined XMPP condition. +TEXT is a string to be sent in the error message, or nil for no text. +APP-SPECIFIC is a list of extra XML tags. + +See section 9.3 of XMPP Core." + (signal 'jabber-error + (list error-type condition text app-specific))) + +(defun jabber-unhex (string) + "Convert a hex-encoded UTF-8 string to Emacs representation. +For example, \"ji%C5%99i@%C4%8Dechy.example/v%20Praze\" becomes +\"jiři@čechy.example/v Praze\"." + (decode-coding-string (url-unhex-string string) 'utf-8)) + +(defun jabber-handle-uri (uri &rest ignored-args) + "Handle XMPP links according to draft-saintandre-xmpp-iri-04. +See Info node `(jabber)XMPP URIs'." + (interactive "sEnter XMPP URI: ") + + (when (string-match "//" uri) + (error "URIs with authority part are not supported")) + + ;; This regexp handles three cases: + ;; xmpp:romeo@montague.net + ;; xmpp:romeo@montague.net?roster + ;; xmpp:romeo@montague.net?roster;name=Romeo%20Montague;group=Lovers + (unless (string-match "^xmpp:\\([^?]+\\)\\(\\?\\([a-z]+\\)\\(;\\(.*\\)\\)?\\)?" uri) + (error "Invalid XMPP URI '%s'" uri)) + + ;; We start by raising the Emacs frame. + (raise-frame) + + (let ((jid (jabber-unhex (match-string 1 uri))) + (method (match-string 3 uri)) + (args (let ((text (match-string 5 uri))) + ;; If there are arguments... + (when text + ;; ...split the pairs by ';'... + (let ((pairs (split-string text ";"))) + (mapcar (lambda (pair) + ;; ...and split keys from values by '='. + (destructuring-bind (key value) + (split-string pair "=") + ;; Values can be hex-coded. + (cons key (jabber-unhex value)))) + pairs)))))) + ;; The full list of methods is at + ;; <URL:http://www.jabber.org/registrar/querytypes.html>. + (cond + ;; Join an MUC. + ((string= method "join") + (let ((account (jabber-read-account))) + (jabber-muc-join + account jid (jabber-muc-read-my-nickname account jid) t))) + ;; Register with a service. + ((string= method "register") + (jabber-get-register (jabber-read-account) jid)) + ;; Run an ad-hoc command + ((string= method "command") + ;; XXX: does the 'action' attribute make sense? + (jabber-ahc-execute-command + (jabber-read-account) jid (cdr (assoc "node" args)))) + ;; Everything else: open a chat buffer. + (t + (jabber-chat-with (jabber-read-account) jid))))) + +(defun url-xmpp (url) + "Handle XMPP URLs from internal Emacs functions." + ;; XXX: This parsing roundtrip is redundant, and the parser of the + ;; url package might lose information. + (jabber-handle-uri (url-recreate-url url))) + +(defun string>-numerical (s1 s2) + "Return t if first arg string is more than second in numerical order." + (cond ((string= s1 s2) nil) + ((> (length s1) (length s2)) t) + ((< (length s1) (length s2)) nil) + ((< (string-to-number (substring s1 0 1)) (string-to-number (substring s2 0 1))) nil) + ((> (string-to-number (substring s1 0 1)) (string-to-number (substring s2 0 1))) t) + (t (string>-numerical (substring s1 1) (substring s2 1))))) + +(defun jabber-append-string-to-file (string file &optional func &rest args) + "Append STRING (may be nil) to FILE. Create FILE if needed. +If FUNC is non-nil, then call FUNC with ARGS at beginning of +temporaly buffer _before_ inserting STRING." + (when (or (stringp string) (functionp func)) + (with-temp-buffer + (when (functionp func) (apply func args)) + (when (stringp string) (insert string)) + (write-region (point-min) (point-max) file t (list t))))) + +(defun jabber-tree-map (fn tree) + "Apply FN to all nodes in the TREE starting with root. FN is +applied to the node and not to the data itself." + (let ((result (cons nil nil))) + (do ((tail tree (cdr tail)) + (prev result end) + (end result (let* ((x (car tail)) + (val (if (atom x) + (funcall fn x) + (jabber-tree-map fn x)))) + (setf (car end) val (cdr end) (cons nil + nil))))) + ((atom tail) + (progn + (setf (cdr prev) (if tail (funcall fn tail) nil)) + result))))) + +(provide 'jabber-util) + +;;; arch-tag: cfbb73ac-e2d7-4652-a08d-dc789bcded8a |