]>
Commit | Line | Data |
---|---|---|
1 | ;; jabber-util.el - various utility functions -*- coding: utf-8; -*- | |
2 | ||
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 | |
7 | ||
8 | ;; This file is a part of jabber.el. | |
9 | ||
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. | |
14 | ||
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. | |
19 | ||
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 | |
23 | ||
24 | (require 'cl-lib) | |
25 | (require 'password-cache) | |
26 | (condition-case nil | |
27 | (require 'auth-source) | |
28 | (error nil)) | |
29 | ||
30 | (defvar jabber-jid-history nil | |
31 | "History of entered JIDs.") | |
32 | ||
33 | (defalias 'jabber-propertize 'propertize) | |
34 | ||
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)) | |
39 | ||
40 | (unless (fboundp 'delete-and-extract-region) | |
41 | (defsubst delete-and-extract-region (start end) | |
42 | (prog1 | |
43 | (buffer-substring start end) | |
44 | (delete-region start end)))) | |
45 | ||
46 | (unless (fboundp 'access-file) | |
47 | (defsubst access-file (filename error-message) | |
48 | (unless (file-readable-p filename) | |
49 | (error error-message)))) | |
50 | ||
51 | (defalias 'jabber-float-time 'float-time) | |
52 | ||
53 | (defalias 'jabber-cancel-timer 'cancel-timer) | |
54 | ||
55 | (defvar jabber-connections) | |
56 | (defun jabber-concat-rosters () | |
57 | "Concatenate the rosters of all connected accounts." | |
58 | (apply #'append | |
59 | (mapcar | |
60 | (lambda (jc) | |
61 | (plist-get (fsm-get-state-data jc) :roster)) | |
62 | jabber-connections))) | |
63 | ||
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 | |
68 | (mapcar | |
69 | (lambda (jc) | |
70 | (plist-get (fsm-get-state-data jc) :roster)) | |
71 | jabber-connections)))) | |
72 | (apply #'append | |
73 | (mapcar (lambda (jid) | |
74 | (mapcar (lambda (res) (intern (format "%s/%s" jid (car res)))) | |
75 | (get (jabber-jid-symbol jid) 'resources))) | |
76 | jids)))) | |
77 | ||
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)))) | |
84 | ||
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)))) | |
90 | ||
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 | |
95 | binding." | |
96 | (plist-get (fsm-get-state-data jc) :original-jid)) | |
97 | ||
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)) | |
103 | (cl-return jc)))) | |
104 | ||
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))) | |
110 | ||
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))) | |
116 | ||
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)) | |
124 | ||
125 | (defun jabber-jid-server (jid) | |
126 | "Return the server portion of JID." | |
127 | (string-match "^\\(.*@\\)?\\([^@/]+\\)\\(/.*\\)?$" jid) | |
128 | (match-string 2 jid)) | |
129 | ||
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) | |
134 | (get user 'name)))) | |
135 | ||
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) | |
140 | (symbol-name string) | |
141 | string)))) | |
142 | ||
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 | |
148 | collect value)) | |
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))) | |
153 | ||
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))) | |
159 | ||
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. | |
164 | (if (symbolp jid) | |
165 | jid | |
166 | ;; XXX: "downcase" is a poor man's nodeprep. See XMPP CORE. | |
167 | (intern (downcase (jabber-jid-user jid)) jabber-jid-obarray))) | |
168 | ||
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." | |
174 | (or | |
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)))) | |
178 | ||
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: | |
188 | ||
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 | |
192 | ||
193 | If FULLJIDS is non-nil, complete jids with resources." | |
194 | (let ((jid-at-point (or | |
195 | (and default | |
196 | ;; default can be either a symbol or a string | |
197 | (if (symbolp default) | |
198 | (symbol-name default) | |
199 | default)) | |
200 | (let* ((jid (get-text-property (point) 'jabber-jid)) | |
201 | (res (get (jabber-jid-symbol jid) 'resource))) | |
202 | (when jid | |
203 | (if (and fulljids res (not (jabber-jid-resource jid))) | |
204 | (format "%s/%s" jid res) | |
205 | jid))) | |
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))))) | |
214 | chosen) | |
215 | (dolist (item (or subset (jabber-concat-rosters))) | |
216 | (if (get item 'name) | |
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)) | |
221 | (let ((input | |
222 | (completing-read (concat prompt | |
223 | (if jid-at-point | |
224 | (format "(default %s) " jid-at-point))) | |
225 | jid-completion-table | |
226 | nil require-match nil 'jabber-jid-history jid-at-point))) | |
227 | (setq chosen | |
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))) | |
231 | input)))) | |
232 | ||
233 | (when chosen | |
234 | (cl-case resource | |
235 | (full | |
236 | ;; If JID is bare, add the highest-priority resource. | |
237 | (if (jabber-jid-resource chosen) | |
238 | chosen | |
239 | (let ((highest-resource (get (jabber-jid-symbol chosen) 'resource))) | |
240 | (if highest-resource | |
241 | (concat chosen "/" highest-resource) | |
242 | chosen)))) | |
243 | (bare-or-muc | |
244 | ;; If JID is full and non-MUC, remove resource. | |
245 | (if (null (jabber-jid-resource chosen)) | |
246 | chosen | |
247 | (let ((bare (jabber-jid-user chosen))) | |
248 | (if (assoc bare *jabber-active-groupchats*) | |
249 | chosen | |
250 | bare)))) | |
251 | (t | |
252 | chosen))))) | |
253 | ||
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 | |
258 | (if node-at-point | |
259 | (format "(default %s) " node-at-point))) | |
260 | node-at-point))) | |
261 | ||
262 | (defun jabber-password-key (bare-jid) | |
263 | "Construct key for `password' library from BARE-JID." | |
264 | (concat "xmpp:" bare-jid)) | |
265 | ||
266 | (defun jabber-read-password (bare-jid) | |
267 | "Read Jabber password from minibuffer." | |
268 | (let ((found | |
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) | |
273 | :port "xmpp" | |
274 | :max 1 | |
275 | :require '(:secret)))))) | |
276 | (if found | |
277 | (let ((secret (plist-get found :secret))) | |
278 | (copy-sequence | |
279 | (if (functionp secret) | |
280 | (funcall secret) | |
281 | secret))) | |
282 | (let ((prompt (format "Jabber password for %s: " bare-jid))) | |
283 | ;; Need to copy the password, as sasl.el wants to erase it. | |
284 | (copy-sequence | |
285 | (password-read prompt (jabber-password-key bare-jid))))))) | |
286 | ||
287 | (defun jabber-cache-password (bare-jid password) | |
288 | "Cache PASSWORD for BARE-JID." | |
289 | (password-cache-add (jabber-password-key bare-jid) password)) | |
290 | ||
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))) | |
297 | ||
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 | |
302 | account. | |
303 | If CONTACT-HINT is a string or a JID symbol, default to an account | |
304 | that has that contact in its roster." | |
305 | (let ((completions | |
306 | (mapcar (lambda (c) | |
307 | (cons | |
308 | (jabber-connection-bare-jid c) | |
309 | c)) | |
310 | jabber-connections))) | |
311 | (cond | |
312 | ((null jabber-connections) | |
313 | (error "Not connected to Jabber")) | |
314 | ((and (null (cdr jabber-connections)) (not always-ask)) | |
315 | ;; only one account | |
316 | (car jabber-connections)) | |
317 | (t | |
318 | (or | |
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))) | |
322 | (when (and at-point | |
323 | (memq at-point jabber-connections)) | |
324 | (jabber-connection-bare-jid at-point))) completions)) | |
325 | (let* ((default | |
326 | (or | |
327 | (and contact-hint | |
328 | (setq contact-hint (jabber-jid-symbol contact-hint)) | |
329 | (let ((matching | |
330 | (cl-find-if | |
331 | (lambda (jc) | |
332 | (memq contact-hint (plist-get (fsm-get-state-data jc) :roster))) | |
333 | jabber-connections))) | |
334 | (when matching | |
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 | |
341 | (caar completions))) | |
342 | (input (completing-read | |
343 | (concat "Select Jabber account (default " | |
344 | default | |
345 | "): ") | |
346 | completions nil t nil 'jabber-account-history | |
347 | default))) | |
348 | (cdr (assoc input completions)))))))) | |
349 | ||
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/>. | |
354 | ||
355 | XML-DATA is the parsed tree data from the stream (stanzas) | |
356 | obtained from `xml-parse-region'." | |
357 | (let (query) | |
358 | (dolist (x (jabber-xml-node-children xml-data)) | |
359 | (if (and | |
360 | (listp x) | |
361 | (not (eq (jabber-xml-node-name x) 'error))) | |
362 | (setq query x))) | |
363 | query)) | |
364 | ||
365 | (defun jabber-iq-error (xml-data) | |
366 | "Return the <error/> part of an IQ stanza, if any. | |
367 | ||
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))) | |
371 | ||
372 | (defun jabber-iq-xmlns (xml-data) | |
373 | "Return the namespace of an IQ stanza, i.e. the namespace of its query part. | |
374 | ||
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)) | |
378 | ||
379 | (defun jabber-message-timestamp (xml-data) | |
380 | "Given a <message/> element, return its timestamp, or nil if none. | |
381 | ||
382 | XML-DATA is the parsed tree data from the stream (stanzas) | |
383 | obtained from `xml-parse-region'." | |
384 | (jabber-x-delay | |
385 | (or | |
386 | (jabber-xml-path xml-data '(("urn:xmpp:delay" . "delay"))) | |
387 | (jabber-xml-path xml-data '(("jabber:x:delay" . "x")))))) | |
388 | ||
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. | |
394 | ||
395 | XML-DATA is the parsed tree data from the stream (stanzas) | |
396 | obtained from `xml-parse-region'." | |
397 | (cond | |
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)))))) | |
409 | ||
410 | (defun jabber-parse-legacy-time (timestamp) | |
411 | "Parse timestamp in ccyymmddThh:mm:ss format (UTC) and return as internal | |
412 | time value." | |
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))) | |
420 | ||
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))) | |
430 | ||
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)) | |
435 | ||
436 | (defun jabber-encode-timezone () | |
437 | (let ((time-zone-offset (nth 0 (current-time-zone)))) | |
438 | (if (null time-zone-offset) | |
439 | "Z" | |
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))))) | |
444 | ||
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) "-") | |
448 | raw-time | |
449 | (concat | |
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) | |
463 | "[-+Z]")))) | |
464 | (if (string= "" timezone) | |
465 | "Z" | |
466 | timezone)) | |
467 | (substring time 19)))) | |
468 | ;; timezone is either Z (UTC) or [+-]HH:MM | |
469 | (let ((timezone-seconds | |
470 | (if (string= timezone "Z") | |
471 | 0 | |
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)))) | |
476 | ||
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 | |
481 | the echo area. | |
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") | |
488 | " succeeded" | |
489 | (concat | |
490 | " failed: " | |
491 | (let ((the-error (jabber-iq-error xml-data))) | |
492 | (if the-error | |
493 | (jabber-parse-error the-error) | |
494 | "No error message given")))))))) | |
495 | ||
496 | (defconst jabber-error-messages | |
497 | (list | |
498 | (cons 'bad-request "Bad request") | |
499 | (cons 'conflict "Conflict") | |
500 | (cons 'feature-not-implemented "Feature not implemented") | |
501 | (cons 'forbidden "Forbidden") | |
502 | (cons 'gone "Gone") | |
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.") | |
521 | ||
522 | (defconst jabber-legacy-error-messages | |
523 | (list | |
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).") | |
542 | ||
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)) | |
548 | condition text) | |
549 | (if error-type | |
550 | ;; If the <error/> tag has a type element, it is new-school. | |
551 | (dolist (child (jabber-xml-node-children error-xml)) | |
552 | (when (string= | |
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))) | |
557 | (setq condition | |
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)) | |
561 | error-code)) | |
562 | (setq text (car (jabber-xml-node-children error-xml)))) | |
563 | (concat condition | |
564 | (if text (format ": %s" text))))) | |
565 | ||
566 | (defun jabber-error-condition (error-xml) | |
567 | "Parse the given <error/> tag and return the condition symbol." | |
568 | (catch 'condition | |
569 | (dolist (child (jabber-xml-node-children error-xml)) | |
570 | (when (string= | |
571 | (jabber-xml-get-attribute child 'xmlns) | |
572 | "urn:ietf:params:xml:ns:xmpp-stanzas") | |
573 | (throw 'condition (jabber-xml-node-name child)))))) | |
574 | ||
575 | (defvar jabber-stream-error-messages | |
576 | (list | |
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.") | |
602 | ||
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 | |
606 | ;; search for it. | |
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))))) | |
613 | ||
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))))))) | |
623 | ||
624 | (put 'jabber-error | |
625 | 'error-conditions | |
626 | '(error jabber-error)) | |
627 | (put 'jabber-error | |
628 | 'error-message | |
629 | "Jabber error") | |
630 | ||
631 | ||
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. | |
635 | ||
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. | |
647 | ||
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))) | |
652 | ||
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)) | |
658 | ||
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: ") | |
665 | ||
666 | (when (string-match "//" uri) | |
667 | (error "URIs with authority part are not supported")) | |
668 | ||
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)) | |
675 | ||
676 | ;; We start by raising the Emacs frame. | |
677 | (raise-frame) | |
678 | ||
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... | |
683 | (when text | |
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)))) | |
692 | pairs)))))) | |
693 | ;; The full list of methods is at | |
694 | ;; <URL:http://www.jabber.org/registrar/querytypes.html>. | |
695 | (cond | |
696 | ;; Join an MUC. | |
697 | ((string= method "join") | |
698 | (let ((account (jabber-read-account))) | |
699 | (jabber-muc-join | |
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. | |
710 | (t | |
711 | (jabber-chat-with (jabber-read-account) jid))))) | |
712 | ||
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))) | |
718 | ||
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))))) | |
727 | ||
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)) | |
733 | (with-temp-buffer | |
734 | (when (functionp func) (apply func args)) | |
735 | (when (stringp string) (insert string)) | |
736 | (write-region (point-min) (point-max) file t (list t))))) | |
737 | ||
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)) | |
743 | (prev result end) | |
744 | (end result (let* ((x (car tail)) | |
745 | (val (if (atom x) | |
746 | (funcall fn x) | |
747 | (jabber-tree-map fn x)))) | |
748 | (setf (car end) val (cdr end) (cons nil | |
749 | nil))))) | |
750 | ((atom tail) | |
751 | (progn | |
752 | (setf (cdr prev) (if tail (funcall fn tail) nil)) | |
753 | result))))) | |
754 | ||
755 | (provide 'jabber-util) | |
756 | ||
757 | ;;; arch-tag: cfbb73ac-e2d7-4652-a08d-dc789bcded8a |