1 ;; jabber-util.el - various utility functions -*- coding: utf-8; -*-
3 ;; Copyright (C) 2003, 2004, 2007, 2008 - Magnus Henoch - mange@freemail.hu
4 ;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net
5 ;; Copyright (C) 2008, 2010 - Terechkov Evgenii - evg@altlinux.org
6 ;; Copyright (C) 2010 - Kirill A. Korinskiy - catap@catap.ru
8 ;; This file is a part of jabber.el.
10 ;; This program is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2 of the License, or
13 ;; (at your option) any later version.
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program; if not, write to the Free Software
22 ;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 (require 'password-cache)
27 (require 'auth-source)
30 (defvar jabber-jid-history nil
31 "History of entered JIDs.")
33 (defalias 'jabber-propertize 'propertize)
35 (defsubst jabber-read-with-input-method (prompt &optional initial-contents history default-value)
36 "Like `read-string', but always inheriting the current input method."
37 ;; Preserve input method when entering a minibuffer.
38 (read-string prompt initial-contents history default-value t))
40 (unless (fboundp 'delete-and-extract-region)
41 (defsubst delete-and-extract-region (start end)
43 (buffer-substring start end)
44 (delete-region start end))))
46 (unless (fboundp 'access-file)
47 (defsubst access-file (filename error-message)
48 (unless (file-readable-p filename)
49 (error error-message))))
51 (defalias 'jabber-float-time 'float-time)
53 (defalias 'jabber-cancel-timer 'cancel-timer)
55 (defvar jabber-connections)
56 (defun jabber-concat-rosters ()
57 "Concatenate the rosters of all connected accounts."
61 (plist-get (fsm-get-state-data jc) :roster))
64 (defun jabber-concat-rosters-full ()
65 "Concatenate the rosters of all connected accounts.
66 Show full JIDs, with resources."
67 (let ((jids (apply #'append
70 (plist-get (fsm-get-state-data jc) :roster))
71 jabber-connections))))
74 (mapcar (lambda (res) (intern (format "%s/%s" jid (car res))))
75 (get (jabber-jid-symbol jid) 'resources)))
78 (defun jabber-connection-jid (jc)
79 "Return the full JID of connection JC."
80 (let ((sd (fsm-get-state-data jc)))
81 (concat (plist-get sd :username) "@"
82 (plist-get sd :server) "/"
83 (plist-get sd :resource))))
85 (defun jabber-connection-bare-jid (jc)
86 "Return the bare JID of connection JC."
87 (let ((sd (fsm-get-state-data jc)))
88 (concat (plist-get sd :username) "@"
89 (plist-get sd :server))))
91 (defun jabber-connection-original-jid (jc)
92 "Return the original JID of connection JC.
93 The \"original JID\" is the JID we authenticated with. The
94 server might subsequently assign us a different JID at resource
96 (plist-get (fsm-get-state-data jc) :original-jid))
98 (defun jabber-find-connection (bare-jid)
99 "Find the connection to the account named by BARE-JID.
100 Return nil if none found."
101 (cl-dolist (jc jabber-connections)
102 (when (string= bare-jid (jabber-connection-bare-jid jc))
105 (defun jabber-find-active-connection (dead-jc)
106 "Find an active connection for dead connection DEAD-JC.
107 Return nil if none found."
108 (let ((jid (jabber-connection-bare-jid dead-jc)))
109 (jabber-find-connection jid)))
111 (defun jabber-jid-username (jid)
112 "Return the username portion of JID, or nil if none found.
113 JID must be a string."
114 (when (string-match "\\(.*\\)@.*\\(/.*\\)?" jid)
115 (match-string 1 jid)))
117 (defun jabber-jid-user (jid)
118 "Return the user portion (username@server) of JID.
119 JID must be a string."
120 ;;transports don't have @, so don't require it
121 ;;(string-match ".*@[^/]*" jid)
122 (string-match "[^/]*" jid)
123 (match-string 0 jid))
125 (defun jabber-jid-server (jid)
126 "Return the server portion of JID."
127 (string-match "^\\(.*@\\)?\\([^@/]+\\)\\(/.*\\)?$" jid)
128 (match-string 2 jid))
130 (defun jabber-jid-rostername (user)
131 "Return the name of USER if present in roster, or nil."
132 (let ((user (jabber-jid-symbol user)))
133 (if (> (length (get user 'name)) 0)
136 (defun jabber-jid-displayname (string)
137 "Return the name of the user from STRING as in roster, else username@server."
138 (or (jabber-jid-rostername string)
139 (jabber-jid-user (if (symbolp string)
143 (defvar jabber-bookmarks)
144 (defun jabber-jid-bookmarkname (string)
145 "Return from STRING the conference name from boomarks or displayname.
146 Use the name according to roster or else the JID if none set."
147 (or (cl-loop for conference in (cl-first (cl-loop for value being the hash-values of jabber-bookmarks
149 do (let ((ls (cadr conference)))
150 (if (string= (cdr (assoc 'jid ls)) string)
151 (cl-return (cdr (assoc 'name ls))))))
152 (jabber-jid-displayname string)))
154 (defun jabber-jid-resource (jid)
155 "Return the resource portion of a JID, or nil if there is none.
156 JID must be a string."
157 (when (string-match "^\\(\\([^/]*@\\)?[^/]*\\)/\\(.*\\)" jid)
158 (match-string 3 jid)))
160 (defvar jabber-jid-obarray)
161 (defun jabber-jid-symbol (jid)
162 "Return the symbol for JID, which must be a symbol or a string."
163 ;; If it's already a symbol, just return it.
166 ;; XXX: "downcase" is a poor man's nodeprep. See XMPP CORE.
167 (intern (downcase (jabber-jid-user jid)) jabber-jid-obarray)))
169 (defvar jabber-account-list)
170 (defun jabber-my-jid-p (jc jid)
171 "Return non-nil if the specified JID is in the `jabber-account-list'.
172 Comment: (modulo resource).
173 Also return non-nil if JID matches JC, modulo resource."
175 (equal (jabber-jid-user jid)
176 (jabber-connection-bare-jid jc))
177 (member (jabber-jid-user jid) (mapcar (lambda (x) (jabber-jid-user (car x))) jabber-account-list))))
179 (defvar *jabber-active-groupchats*)
180 (defun jabber-read-jid-completing (prompt &optional subset require-match default resource fulljids)
181 "Read a jid out of the current roster from the minibuffer.
182 If SUBSET is non-nil, it should be a list of symbols from which
183 the JID is to be selected, instead of using the entire roster.
184 If REQUIRE-MATCH is non-nil, the JID must be in the list used.
185 If DEFAULT is non-nil, it's used as the default value, otherwise
186 the default is inferred from context.
187 RESOURCE is one of the following:
189 nil Accept full or bare JID, as entered
190 full Turn bare JIDs to full ones with highest-priority resource
191 bare-or-muc Turn full JIDs to bare ones, except for in MUC
193 If FULLJIDS is non-nil, complete jids with resources."
194 (let ((jid-at-point (or
196 ;; default can be either a symbol or a string
197 (if (symbolp default)
198 (symbol-name default)
200 (let* ((jid (get-text-property (point) 'jabber-jid))
201 (res (get (jabber-jid-symbol jid) 'resource)))
203 (if (and fulljids res (not (jabber-jid-resource jid)))
204 (format "%s/%s" jid res)
206 (bound-and-true-p jabber-chatting-with)
207 (bound-and-true-p jabber-group)))
208 (completion-ignore-case t)
209 (jid-completion-table (mapcar #'(lambda (item)
210 (cons (symbol-name item) item))
211 (or subset (funcall (if fulljids
212 'jabber-concat-rosters-full
213 'jabber-concat-rosters)))))
215 (dolist (item (or subset (jabber-concat-rosters)))
217 (push (cons (get item 'name) item) jid-completion-table)))
218 ;; if the default is not in the allowed subset, it's not a good default
219 (if (and subset (not (assoc jid-at-point jid-completion-table)))
220 (setq jid-at-point nil))
222 (completing-read (concat prompt
224 (format "(default %s) " jid-at-point)))
226 nil require-match nil 'jabber-jid-history jid-at-point)))
228 (if (and input (assoc-string input jid-completion-table t))
229 (symbol-name (cdr (assoc-string input jid-completion-table t)))
230 (and (not (zerop (length input)))
236 ;; If JID is bare, add the highest-priority resource.
237 (if (jabber-jid-resource chosen)
239 (let ((highest-resource (get (jabber-jid-symbol chosen) 'resource)))
241 (concat chosen "/" highest-resource)
244 ;; If JID is full and non-MUC, remove resource.
245 (if (null (jabber-jid-resource chosen))
247 (let ((bare (jabber-jid-user chosen)))
248 (if (assoc bare *jabber-active-groupchats*)
254 (defun jabber-read-node (prompt)
255 "Read node name, taking default from disco item at point."
256 (let ((node-at-point (get-text-property (point) 'jabber-node)))
257 (read-string (concat prompt
259 (format "(default %s) " node-at-point)))
262 (defun jabber-password-key (bare-jid)
263 "Construct key for `password' library from BARE-JID."
264 (concat "xmpp:" bare-jid))
266 (defun jabber-read-password (bare-jid)
267 "Read Jabber password from minibuffer."
269 (and (fboundp 'auth-source-search)
270 (nth 0 (auth-source-search
271 :user (jabber-jid-username bare-jid)
272 :host (jabber-jid-server bare-jid)
275 :require '(:secret))))))
277 (let ((secret (plist-get found :secret)))
279 (if (functionp secret)
282 (let ((prompt (format "Jabber password for %s: " bare-jid)))
283 ;; Need to copy the password, as sasl.el wants to erase it.
285 (password-read prompt (jabber-password-key bare-jid)))))))
287 (defun jabber-cache-password (bare-jid password)
288 "Cache PASSWORD for BARE-JID."
289 (password-cache-add (jabber-password-key bare-jid) password))
291 (defun jabber-uncache-password (bare-jid)
292 "Uncache cached password for BARE-JID.
293 Useful if the password proved to be wrong."
294 (interactive (list (jabber-jid-user
295 (completing-read "Forget password of account: " jabber-account-list nil nil nil 'jabber-account-history))))
296 (password-cache-remove (jabber-password-key bare-jid)))
298 (defvar jabber-buffer-connection)
299 (defun jabber-read-account (&optional always-ask contact-hint)
300 "Ask for which connected account to use.
301 If ALWAYS-ASK is nil and there is only one account, return that
303 If CONTACT-HINT is a string or a JID symbol, default to an account
304 that has that contact in its roster."
308 (jabber-connection-bare-jid c)
310 jabber-connections)))
312 ((null jabber-connections)
313 (error "Not connected to Jabber"))
314 ((and (null (cdr jabber-connections)) (not always-ask))
316 (car jabber-connections))
319 ;; if there is a jabber-account property at point,
320 ;; present it as default value
321 (cdr (assoc (let ((at-point (get-text-property (point) 'jabber-account)))
323 (memq at-point jabber-connections))
324 (jabber-connection-bare-jid at-point))) completions))
328 (setq contact-hint (jabber-jid-symbol contact-hint))
332 (memq contact-hint (plist-get (fsm-get-state-data jc) :roster)))
333 jabber-connections)))
335 (jabber-connection-bare-jid matching))))
336 ;; if the buffer is associated with a connection, use it
337 (when (and jabber-buffer-connection
338 (jabber-find-active-connection jabber-buffer-connection))
339 (jabber-connection-bare-jid jabber-buffer-connection))
340 ;; else, use the first connection in the list
342 (input (completing-read
343 (concat "Select Jabber account (default "
346 completions nil t nil 'jabber-account-history
348 (cdr (assoc input completions))))))))
350 (defun jabber-iq-query (xml-data)
351 "Return the query part of an IQ stanza.
352 An IQ stanza may have zero or one query child, and zero or one <error/> child.
353 The query child is often but not always <query/>.
355 XML-DATA is the parsed tree data from the stream (stanzas)
356 obtained from `xml-parse-region'."
358 (dolist (x (jabber-xml-node-children xml-data))
361 (not (eq (jabber-xml-node-name x) 'error)))
365 (defun jabber-iq-error (xml-data)
366 "Return the <error/> part of an IQ stanza, if any.
368 XML-DATA is the parsed tree data from the stream (stanzas)
369 obtained from `xml-parse-region'."
370 (car (jabber-xml-get-children xml-data 'error)))
372 (defun jabber-iq-xmlns (xml-data)
373 "Return the namespace of an IQ stanza, i.e. the namespace of its query part.
375 XML-DATA is the parsed tree data from the stream (stanzas)
376 obtained from `xml-parse-region'."
377 (jabber-xml-get-attribute (jabber-iq-query xml-data) 'xmlns))
379 (defun jabber-message-timestamp (xml-data)
380 "Given a <message/> element, return its timestamp, or nil if none.
382 XML-DATA is the parsed tree data from the stream (stanzas)
383 obtained from `xml-parse-region'."
386 (jabber-xml-path xml-data '(("urn:xmpp:delay" . "delay")))
387 (jabber-xml-path xml-data '(("jabber:x:delay" . "x"))))))
389 (defun jabber-x-delay (xml-data)
390 "Return timestamp given a delayed delivery element.
391 This can be either a <delay/> tag in namespace urn:xmpp:delay (XEP-0203), or
392 a <x/> tag in namespace jabber:x:delay (XEP-0091).
393 Return nil if no such data available.
395 XML-DATA is the parsed tree data from the stream (stanzas)
396 obtained from `xml-parse-region'."
398 ((and (eq (jabber-xml-node-name xml-data) 'x)
399 (string= (jabber-xml-get-attribute xml-data 'xmlns) "jabber:x:delay"))
400 (let ((stamp (jabber-xml-get-attribute xml-data 'stamp)))
401 (if (and (stringp stamp)
402 (= (length stamp) 17))
403 (jabber-parse-legacy-time stamp))))
404 ((and (eq (jabber-xml-node-name xml-data) 'delay)
405 (string= (jabber-xml-get-attribute xml-data 'xmlns) "urn:xmpp:delay"))
406 (let ((stamp (jabber-xml-get-attribute xml-data 'stamp)))
407 (when (stringp stamp)
408 (jabber-parse-time stamp))))))
410 (defun jabber-parse-legacy-time (timestamp)
411 "Parse timestamp in ccyymmddThh:mm:ss format (UTC) and return as internal
413 (let ((year (string-to-number (substring timestamp 0 4)))
414 (month (string-to-number (substring timestamp 4 6)))
415 (day (string-to-number (substring timestamp 6 8)))
416 (hour (string-to-number (substring timestamp 9 11)))
417 (minute (string-to-number (substring timestamp 12 14)))
418 (second (string-to-number (substring timestamp 15 17))))
419 (encode-time second minute hour day month year 0)))
421 (defun jabber-encode-legacy-time (timestamp)
422 "Parse TIMESTAMP as internal time value and encode as ccyymmddThh:mm:ss (UTC)."
423 (if (featurep 'xemacs)
424 ;; XEmacs doesn't have `universal' argument to format-time-string,
425 ;; so we have to do it ourselves.
426 (format-time-string "%Y%m%dT%H:%M:%S"
427 (time-subtract timestamp
428 (list 0 (car (current-time-zone)))))
429 (format-time-string "%Y%m%dT%H:%M:%S" timestamp t)))
431 (defun jabber-encode-time (time)
432 "Convert TIME to a string by XEP-0082.
433 TIME is in a format accepted by `format-time-string'."
434 (format-time-string "%Y-%m-%dT%H:%M:%SZ" time t))
436 (defun jabber-encode-timezone ()
437 (let ((time-zone-offset (nth 0 (current-time-zone))))
438 (if (null time-zone-offset)
440 (let* ((positivep (>= time-zone-offset 0))
441 (hours (/ (abs time-zone-offset) 3600))
442 (minutes (/ (% (abs time-zone-offset) 3600) 60)))
443 (format "%s%02d:%02d"(if positivep "+" "-") hours minutes)))))
445 (defun jabber-parse-time (raw-time)
446 "Parse the DateTime encoded in TIME according to XEP-0082."
447 (let* ((time (if (string= (substring raw-time 4 5) "-")
450 (substring raw-time 0 4) "-"
451 (substring raw-time 4 6) "-"
452 (substring raw-time 6 (length raw-time)))))
453 (year (string-to-number (substring time 0 4)))
454 (month (string-to-number (substring time 5 7)))
455 (day (string-to-number (substring time 8 10)))
456 (hour (string-to-number (substring time 11 13)))
457 (minute (string-to-number (substring time 14 16)))
458 (second (string-to-number (substring time 17 19)))
459 (timezone (if (eq (aref time 19) ?.)
460 ;; fractions are optional
461 (let ((timezone (cadr
462 (split-string (substring time 20)
464 (if (string= "" timezone)
467 (substring time 19))))
468 ;; timezone is either Z (UTC) or [+-]HH:MM
469 (let ((timezone-seconds
470 (if (string= timezone "Z")
472 (* (if (eq (aref timezone 0) ?+) 1 -1)
473 (* 60 (+ (* 60 (string-to-number (substring timezone 1 3)))
474 (string-to-number (substring timezone 4 6))))))))
475 (encode-time second minute hour day month year timezone-seconds))))
477 (defun jabber-report-success (_jc xml-data context)
478 "IQ callback reporting success or failure of the operation.
479 CONTEXT is a string describing the action.
480 \"CONTEXT succeeded\" or \"CONTEXT failed: REASON\" is displayed in
482 JC is the Jabber connection.
483 XML-DATA is the parsed tree data from the stream (stanzas)
484 obtained from `xml-parse-region'."
485 (let ((type (jabber-xml-get-attribute xml-data 'type)))
486 (message (concat context
487 (if (string= type "result")
491 (let ((the-error (jabber-iq-error xml-data)))
493 (jabber-parse-error the-error)
494 "No error message given"))))))))
496 (defconst jabber-error-messages
498 (cons 'bad-request "Bad request")
499 (cons 'conflict "Conflict")
500 (cons 'feature-not-implemented "Feature not implemented")
501 (cons 'forbidden "Forbidden")
503 (cons 'internal-server-error "Internal server error")
504 (cons 'item-not-found "Item not found")
505 (cons 'jid-malformed "JID malformed")
506 (cons 'not-acceptable "Not acceptable")
507 (cons 'not-allowed "Not allowed")
508 (cons 'not-authorized "Not authorized")
509 (cons 'payment-required "Payment required")
510 (cons 'recipient-unavailable "Recipient unavailable")
511 (cons 'redirect "Redirect")
512 (cons 'registration-required "Registration required")
513 (cons 'remote-server-not-found "Remote server not found")
514 (cons 'remote-server-timeout "Remote server timeout")
515 (cons 'resource-constraint "Resource constraint")
516 (cons 'service-unavailable "Service unavailable")
517 (cons 'subscription-required "Subscription required")
518 (cons 'undefined-condition "Undefined condition")
519 (cons 'unexpected-request "Unexpected request"))
520 "String descriptions of XMPP stanza errors.")
522 (defconst jabber-legacy-error-messages
524 (cons 302 "Redirect")
525 (cons 400 "Bad request")
526 (cons 401 "Unauthorized")
527 (cons 402 "Payment required")
528 (cons 403 "Forbidden")
529 (cons 404 "Not found")
530 (cons 405 "Not allowed")
531 (cons 406 "Not acceptable")
532 (cons 407 "Registration required")
533 (cons 408 "Request timeout")
534 (cons 409 "Conflict")
535 (cons 500 "Internal server error")
536 (cons 501 "Not implemented")
537 (cons 502 "Remote server error")
538 (cons 503 "Service unavailable")
539 (cons 504 "Remote server timeout")
540 (cons 510 "Disconnected"))
541 "String descriptions of legacy errors (XEP-0086).")
543 (defun jabber-parse-error (error-xml)
544 "Parse the given <error/> tag and return a string fit for human consumption.
545 See secton 9.3, Stanza Errors, of XMPP Core, and XEP-0086, Legacy Errors."
546 (let ((error-type (jabber-xml-get-attribute error-xml 'type))
547 (error-code (jabber-xml-get-attribute error-xml 'code))
550 ;; If the <error/> tag has a type element, it is new-school.
551 (dolist (child (jabber-xml-node-children error-xml))
553 (jabber-xml-get-attribute child 'xmlns)
554 "urn:ietf:params:xml:ns:xmpp-stanzas")
555 (if (eq (jabber-xml-node-name child) 'text)
556 (setq text (car (jabber-xml-node-children child)))
558 (or (cdr (assq (jabber-xml-node-name child) jabber-error-messages))
559 (symbol-name (jabber-xml-node-name child)))))))
560 (setq condition (or (cdr (assq (string-to-number error-code) jabber-legacy-error-messages))
562 (setq text (car (jabber-xml-node-children error-xml))))
564 (if text (format ": %s" text)))))
566 (defun jabber-error-condition (error-xml)
567 "Parse the given <error/> tag and return the condition symbol."
569 (dolist (child (jabber-xml-node-children error-xml))
571 (jabber-xml-get-attribute child 'xmlns)
572 "urn:ietf:params:xml:ns:xmpp-stanzas")
573 (throw 'condition (jabber-xml-node-name child))))))
575 (defvar jabber-stream-error-messages
577 (cons 'bad-format "Bad XML format")
578 (cons 'bad-namespace-prefix "Bad namespace prefix")
579 (cons 'conflict "Conflict")
580 (cons 'connection-timeout "Connection timeout")
581 (cons 'host-gone "Host gone")
582 (cons 'host-unknown "Host unknown")
583 (cons 'improper-addressing "Improper addressing") ; actually only s2s
584 (cons 'internal-server-error "Internal server error")
585 (cons 'invalid-from "Invalid from")
586 (cons 'invalid-id "Invalid id")
587 (cons 'invalid-namespace "Invalid namespace")
588 (cons 'invalid-xml "Invalid XML")
589 (cons 'not-authorized "Not authorized")
590 (cons 'policy-violation "Policy violation")
591 (cons 'remote-connection-failed "Remote connection failed")
592 (cons 'resource-constraint "Resource constraint")
593 (cons 'restricted-xml "Restricted XML")
594 (cons 'see-other-host "See other host")
595 (cons 'system-shutdown "System shutdown")
596 (cons 'undefined-condition "Undefined condition")
597 (cons 'unsupported-encoding "Unsupported encoding")
598 (cons 'unsupported-stanza-type "Unsupported stanza type")
599 (cons 'unsupported-version "Unsupported version")
600 (cons 'xml-not-well-formed "XML not well formed"))
601 "String descriptions of XMPP stream errors.")
603 (defun jabber-stream-error-condition (error-xml)
604 "Return the condition of a <stream:error/> tag."
605 ;; as we don't know the node name of the condition, we have to
607 (cl-dolist (node (jabber-xml-node-children error-xml))
608 (when (and (string= (jabber-xml-get-attribute node 'xmlns)
609 "urn:ietf:params:xml:ns:xmpp-streams")
610 (assq (jabber-xml-node-name node)
611 jabber-stream-error-messages))
612 (cl-return (jabber-xml-node-name node)))))
614 (defun jabber-parse-stream-error (error-xml)
615 "Parse the given error tag and return a string fit for human consumption.
616 ERROR-XML is a <stream:error/> tag parsed with `xml-parse-region'."
617 (let ((text-node (car (jabber-xml-get-children error-xml 'text)))
618 (condition (jabber-stream-error-condition error-xml)))
619 (concat (if condition (cdr (assq condition jabber-stream-error-messages))
620 "Unknown stream error")
621 (if (and text-node (stringp (car (jabber-xml-node-children text-node))))
622 (concat ": " (car (jabber-xml-node-children text-node)))))))
626 '(error jabber-error))
632 ;; https://www.rfc-editor.org/rfc/rfc6120.html#section-8.3 explains
633 ;; that there are stanza errors, which are recoverable and do not
634 ;; terminate the stream.
636 ;; Each stanza has a type which are the one explained at the
637 ;; ERROR-TYPE parameter. checkdoc throws warnings stating that errors
638 ;; messages should start with capital letters, thus the `downcase'
639 ;; function is used as a workaround.
640 (defun jabber-signal-error (error-type condition &optional text app-specific)
641 "Signal an error to be sent by Jabber.
642 ERROR-TYPE is one of \"Cancel\", \"Continue\", \"Mmodify\", \"Auth\"
643 and \"Wait\" (lowercase versions make `checkdoc' to throw errors).
644 CONDITION is a symbol denoting a defined XMPP condition.
645 TEXT is a string to be sent in the error message, or nil for no text.
646 APP-SPECIFIC is a list of extra XML tags.
648 See section 9.3 of XMPP Core (RFC 3920).
649 See section 8.3 of XMPP Core (RFC 6120)."
650 (signal 'jabber-error
651 (list (downcase error-type) condition text app-specific)))
653 (defun jabber-unhex (string)
654 "Convert a hex-encoded UTF-8 string to Emacs representation.
655 For example, \"ji%C5%99i@%C4%8Dechy.example/v%20Praze\" becomes
656 \"jiři@čechy.example/v Praze\"."
657 (decode-coding-string (url-unhex-string string) 'utf-8))
659 (defun jabber-handle-uri (uri &rest _ignored-args)
660 "Handle XMPP links according to draft-saintandre-xmpp-iri-04.
661 See Info node `(jabber)XMPP URIs'.
662 URI is a string with the \"xmpp://\" link to handle.
663 IGNORED-ARGS are ignored arguments the handler may pass. "
664 (interactive "sEnter XMPP URI: ")
666 (when (string-match "//" uri)
667 (error "URIs with authority part are not supported"))
669 ;; This regexp handles three cases:
670 ;; xmpp:romeo@montague.net
671 ;; xmpp:romeo@montague.net?roster
672 ;; xmpp:romeo@montague.net?roster;name=Romeo%20Montague;group=Lovers
673 (unless (string-match "^xmpp:\\([^?]+\\)\\(\\?\\([a-z]+\\)\\(;\\(.*\\)\\)?\\)?" uri)
674 (error "Invalid XMPP URI '%s'" uri))
676 ;; We start by raising the Emacs frame.
679 (let ((jid (jabber-unhex (match-string 1 uri)))
680 (method (match-string 3 uri))
681 (args (let ((text (match-string 5 uri)))
682 ;; If there are arguments...
684 ;; ...split the pairs by ';'...
685 (let ((pairs (split-string text ";")))
686 (mapcar (lambda (pair)
687 ;; ...and split keys from values by '='.
688 (cl-destructuring-bind (key value)
689 (split-string pair "=")
690 ;; Values can be hex-coded.
691 (cons key (jabber-unhex value))))
693 ;; The full list of methods is at
694 ;; <URL:http://www.jabber.org/registrar/querytypes.html>.
697 ((string= method "join")
698 (let ((account (jabber-read-account)))
700 account jid (jabber-muc-read-my-nickname account jid) t)))
701 ;; Register with a service.
702 ((string= method "register")
703 (jabber-get-register (jabber-read-account) jid))
704 ;; Run an ad-hoc command
705 ((string= method "command")
706 ;; XXX: does the 'action' attribute make sense?
707 (jabber-ahc-execute-command
708 (jabber-read-account) jid (cdr (assoc "node" args))))
709 ;; Everything else: open a chat buffer.
711 (jabber-chat-with (jabber-read-account) jid)))))
713 (defun url-xmpp (url)
714 "Handle XMPP URLs from internal Emacs functions."
715 ;; XXX: This parsing roundtrip is redundant, and the parser of the
716 ;; url package might lose information.
717 (jabber-handle-uri (url-recreate-url url)))
719 (defun string>-numerical (s1 s2)
720 "Return t if first arg string is more than second in numerical order."
721 (cond ((string= s1 s2) nil)
722 ((> (length s1) (length s2)) t)
723 ((< (length s1) (length s2)) nil)
724 ((< (string-to-number (substring s1 0 1)) (string-to-number (substring s2 0 1))) nil)
725 ((> (string-to-number (substring s1 0 1)) (string-to-number (substring s2 0 1))) t)
726 (t (string>-numerical (substring s1 1) (substring s2 1)))))
728 (defun jabber-append-string-to-file (string file &optional func &rest args)
729 "Append STRING (may be nil) to FILE. Create FILE if needed.
730 If FUNC is non-nil, then call FUNC with ARGS at beginning of
731 temporaly buffer _before_ inserting STRING."
732 (when (or (stringp string) (functionp func))
734 (when (functionp func) (apply func args))
735 (when (stringp string) (insert string))
736 (write-region (point-min) (point-max) file t (list t)))))
738 (defun jabber-tree-map (fn tree)
739 "Apply FN to all nodes in the TREE starting with root.
740 FN is applied to the node and not to the data itself."
741 (let ((result (cons nil nil)))
742 (cl-do ((tail tree (cdr tail))
744 (end result (let* ((x (car tail))
747 (jabber-tree-map fn x))))
748 (setf (car end) val (cdr end) (cons nil
752 (setf (cdr prev) (if tail (funcall fn tail) nil))
755 (provide 'jabber-util)
757 ;;; arch-tag: cfbb73ac-e2d7-4652-a08d-dc789bcded8a