Merge remote-tracking branch 'sourceforge/master'
[emacs-jabber.git] / jabber-presence.el
blobe12be874ba76ce4e6030f0dae08a168fe84da563
1 ;; jabber-presence.el - roster and presence bookkeeping
3 ;; Copyright (C) 2003, 2004, 2007, 2008 - Magnus Henoch - mange@freemail.hu
4 ;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net
6 ;; This file is a part of jabber.el.
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation; either version 2 of the License, or
11 ;; (at your option) any later version.
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program; if not, write to the Free Software
20 ;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 (require 'jabber-core)
23 (require 'jabber-iq)
24 (require 'jabber-alert)
25 (require 'jabber-util)
26 (require 'jabber-menu)
27 (require 'jabber-muc)
28 (require 'jabber-autoloads)
30 (require 'assoc)
32 (defvar jabber-presence-element-functions nil
33 "List of functions returning extra elements for <presence/> stanzas.
34 Each function takes one argument, the connection, and returns a
35 possibly empty list of extra child element of the <presence/>
36 stanza.")
38 (defvar jabber-presence-history ()
39 "Keeps track of previously used presence status types")
41 (add-to-list 'jabber-iq-set-xmlns-alist
42 (cons "jabber:iq:roster" (function (lambda (jc x) (jabber-process-roster jc x nil)))))
43 (defun jabber-process-roster (jc xml-data closure-data)
44 "process an incoming roster infoquery result
45 CLOSURE-DATA should be 'initial if initial roster push, nil otherwise."
46 (let ((roster (plist-get (fsm-get-state-data jc) :roster))
47 (from (jabber-xml-get-attribute xml-data 'from))
48 (type (jabber-xml-get-attribute xml-data 'type))
49 (id (jabber-xml-get-attribute xml-data 'id))
50 (username (plist-get (fsm-get-state-data jc) :username))
51 (server (plist-get (fsm-get-state-data jc) :server))
52 (resource (plist-get (fsm-get-state-data jc) :resource))
53 new-items changed-items deleted-items)
54 ;; Perform sanity check on "from" attribute: it should be either absent
55 ;; or match our own JID.
56 (if (not (or (null from)
57 (string= from (concat username "@" server))
58 (string= from (concat username "@" server "/" resource))))
59 (message "Roster push with invalid \"from\": \"%s\" (expected \"%s@%s\" or \"%s@%s/%s\")"
60 from
61 username server username server resource)
63 (dolist (item (jabber-xml-get-children (car (jabber-xml-get-children xml-data 'query)) 'item))
64 (let (roster-item
65 (jid (jabber-jid-symbol (jabber-xml-get-attribute item 'jid))))
67 ;; If subscripton="remove", contact is to be removed from roster
68 (if (string= (jabber-xml-get-attribute item 'subscription) "remove")
69 (progn
70 (message "%s removed from roster" jid)
71 (push jid deleted-items))
73 ;; Find contact if already in roster
74 (setq roster-item (car (memq jid roster)))
76 (if roster-item
77 (push roster-item changed-items)
78 ;; If not found, create a new roster item.
79 (message "%s added to roster" jid)
80 (setq roster-item jid)
81 (push roster-item new-items))
83 ;; If this is an initial push, we want to forget
84 ;; everything we knew about this contact before - e.g. if
85 ;; the contact was online when we disconnected and offline
86 ;; when we reconnect, we don't want to see stale presence
87 ;; information. This assumes that no contacts are shared
88 ;; between accounts.
89 (when (eq closure-data 'initial)
90 (setplist roster-item nil))
92 ;; Now, get all data associated with the contact.
93 (put roster-item 'name (jabber-xml-get-attribute item 'name))
94 (put roster-item 'subscription (jabber-xml-get-attribute item 'subscription))
95 (put roster-item 'ask (jabber-xml-get-attribute item 'ask))
97 ;; Since roster items can't be changed incrementally, we
98 ;; save the original XML to be able to modify it, instead of
99 ;; having to reproduce it. This is for forwards
100 ;; compatibility.
101 (put roster-item 'xml item)
103 (put roster-item 'groups
104 (mapcar (lambda (foo) (nth 2 foo))
105 (jabber-xml-get-children item 'group)))))))
106 ;; This is the function that does the actual updating and
107 ;; redrawing of the roster.
108 (jabber-roster-update jc new-items changed-items deleted-items)
110 (if (and id (string= type "set"))
111 (jabber-send-iq jc nil "result" nil
112 nil nil nil nil id)))
114 ;; After initial roster push, run jabber-post-connect-hooks. We do
115 ;; it here and not before since we want to have the entire roster
116 ;; before we receive any presence stanzas.
117 (when (eq closure-data 'initial)
118 (run-hook-with-args 'jabber-post-connect-hooks jc)))
120 (add-to-list 'jabber-presence-chain 'jabber-process-presence)
121 (defun jabber-process-presence (jc xml-data)
122 "process incoming presence tags"
123 ;; XXX: use JC argument
124 (let ((roster (plist-get (fsm-get-state-data jc) :roster))
125 (from (jabber-xml-get-attribute xml-data 'from))
126 (to (jabber-xml-get-attribute xml-data 'to))
127 (type (jabber-xml-get-attribute xml-data 'type))
128 (presence-show (car (jabber-xml-node-children
129 (car (jabber-xml-get-children xml-data 'show)))))
130 (presence-status (car (jabber-xml-node-children
131 (car (jabber-xml-get-children xml-data 'status)))))
132 (error (car (jabber-xml-get-children xml-data 'error)))
133 (priority (string-to-number (or (car (jabber-xml-node-children (car (jabber-xml-get-children xml-data 'priority))))
134 "0"))))
135 (cond
136 ((string= type "subscribe")
137 (run-with-idle-timer 0.01 nil #'jabber-process-subscription-request jc from presence-status))
139 ((jabber-muc-presence-p xml-data)
140 (jabber-muc-process-presence jc xml-data))
143 ;; XXX: Think about what to do about out-of-roster presences.
144 (let ((buddy (jabber-jid-symbol from)))
145 (if (memq buddy roster)
146 (let* ((oldstatus (get buddy 'show))
147 (resource (or (jabber-jid-resource from) ""))
148 (resource-plist (cdr (assoc resource
149 (get buddy 'resources))))
150 newstatus)
151 (cond
152 ((and (string= resource "") (member type '("unavailable" "error")))
153 ;; 'unavailable' or 'error' from bare JID means that all resources
154 ;; are offline.
155 (setq resource-plist nil)
156 (setq newstatus (if (string= type "error") "error" nil))
157 (let ((new-message (if error
158 (jabber-parse-error error)
159 presence-status)))
160 ;; erase any previous information
161 (put buddy 'resources nil)
162 (put buddy 'connected nil)
163 (put buddy 'show newstatus)
164 (put buddy 'status new-message)))
166 ((string= type "unavailable")
167 (setq resource-plist
168 (plist-put resource-plist 'connected nil))
169 (setq resource-plist
170 (plist-put resource-plist 'show nil))
171 (setq resource-plist
172 (plist-put resource-plist 'status
173 presence-status)))
175 ((string= type "error")
176 (setq newstatus "error")
177 (setq resource-plist
178 (plist-put resource-plist 'connected nil))
179 (setq resource-plist
180 (plist-put resource-plist 'show "error"))
181 (setq resource-plist
182 (plist-put resource-plist 'status
183 (if error
184 (jabber-parse-error error)
185 presence-status))))
186 ((or
187 (string= type "unsubscribe")
188 (string= type "subscribed")
189 (string= type "unsubscribed"))
190 ;; Do nothing, except letting the user know. The Jabber protocol
191 ;; places all this complexity on the server.
192 (setq newstatus type))
194 (setq resource-plist
195 (plist-put resource-plist 'connected t))
196 (setq resource-plist
197 (plist-put resource-plist 'show (or presence-show "")))
198 (setq resource-plist
199 (plist-put resource-plist 'status
200 presence-status))
201 (setq resource-plist
202 (plist-put resource-plist 'priority priority))
203 (setq newstatus (or presence-show ""))))
205 (when resource-plist
206 ;; this is for `assoc-set!' in guile
207 (if (assoc resource (get buddy 'resources))
208 (setcdr (assoc resource (get buddy 'resources)) resource-plist)
209 (put buddy 'resources (cons (cons resource resource-plist) (get buddy 'resources))))
210 (jabber-prioritize-resources buddy))
212 (fsm-send jc (cons :roster-update buddy))
214 (dolist (hook '(jabber-presence-hooks jabber-alert-presence-hooks))
215 (run-hook-with-args hook
216 buddy
217 oldstatus
218 newstatus
219 (plist-get resource-plist 'status)
220 (funcall jabber-alert-presence-message-function
221 buddy
222 oldstatus
223 newstatus
224 (plist-get resource-plist 'status)))))))))))
226 (defun jabber-process-subscription-request (jc from presence-status)
227 "process an incoming subscription request"
228 (with-current-buffer (jabber-chat-create-buffer jc from)
229 (ewoc-enter-last jabber-chat-ewoc (list :subscription-request presence-status :time (current-time)))
231 (dolist (hook '(jabber-presence-hooks jabber-alert-presence-hooks))
232 (run-hook-with-args hook (jabber-jid-symbol from) nil "subscribe" presence-status (funcall jabber-alert-presence-message-function (jabber-jid-symbol from) nil "subscribe" presence-status)))))
234 (defun jabber-subscription-accept-mutual (&rest ignored)
235 (message "Subscription accepted; reciprocal subscription request sent")
236 (jabber-subscription-reply "subscribed" "subscribe"))
238 (defun jabber-subscription-accept-one-way (&rest ignored)
239 (message "Subscription accepted")
240 (jabber-subscription-reply "subscribed"))
242 (defun jabber-subscription-decline (&rest ignored)
243 (message "Subscription declined")
244 (jabber-subscription-reply "unsubscribed"))
246 (defun jabber-subscription-reply (&rest types)
247 (let ((to (jabber-jid-user jabber-chatting-with)))
248 (dolist (type types)
249 (jabber-send-sexp jabber-buffer-connection `(presence ((to . ,to) (type . ,type)))))))
251 (defun jabber-prioritize-resources (buddy)
252 "Set connected, show and status properties for BUDDY from highest-priority resource."
253 (let ((resource-alist (get buddy 'resources))
254 (highest-priority nil))
255 ;; Reset to nil at first, for cases (a) resource-alist is nil
256 ;; and (b) all resources are disconnected.
257 (put buddy 'connected nil)
258 (put buddy 'show nil)
259 (put buddy 'status nil)
260 (mapc #'(lambda (resource)
261 (let* ((resource-plist (cdr resource))
262 (priority (plist-get resource-plist 'priority)))
263 (if (plist-get resource-plist 'connected)
264 (when (or (null highest-priority)
265 (and priority
266 (> priority highest-priority)))
267 ;; if no priority specified, interpret as zero
268 (setq highest-priority (or priority 0))
269 (put buddy 'connected (plist-get resource-plist 'connected))
270 (put buddy 'show (plist-get resource-plist 'show))
271 (put buddy 'status (plist-get resource-plist 'status))
272 (put buddy 'resource (car resource)))
274 ;; if we have not found a connected resource yet, but this
275 ;; disconnected resource has a status message, display it.
276 (when (not (get buddy 'connected))
277 (if (plist-get resource-plist 'status)
278 (put buddy 'status (plist-get resource-plist 'status)))
279 (if (plist-get resource-plist 'show)
280 (put buddy 'show (plist-get resource-plist 'show)))))))
281 resource-alist)))
283 (defun jabber-count-connected-resources (buddy)
284 "Return the number of connected resources for BUDDY."
285 (let ((resource-alist (get buddy 'resources))
286 (count 0))
287 (dolist (resource resource-alist)
288 (if (plist-get (cdr resource) 'connected)
289 (setq count (1+ count))))
290 count))
292 ;;;###autoload
293 (defun jabber-send-presence (show status priority)
294 "Set presence for all accounts."
295 (interactive
296 (list
297 (completing-read "show: " '("" "away" "xa" "dnd" "chat")
298 nil t nil 'jabber-presence-history)
299 (jabber-read-with-input-method "status message: " *jabber-current-status*
300 '*jabber-status-history*)
301 (read-string "priority: " (int-to-string (if *jabber-current-priority*
302 *jabber-current-priority*
303 jabber-default-priority)))))
305 (setq *jabber-current-show* show *jabber-current-status* status)
306 (setq *jabber-current-priority*
307 (if (numberp priority) priority (string-to-number priority)))
309 (let (subelements-map)
310 ;; For each connection, we use a different set of subelements. We
311 ;; cache them, to only generate them once.
313 ;; Ordinary presence, with no specified recipient
314 (dolist (jc jabber-connections)
315 (let ((subelements (jabber-presence-children jc)))
316 (aput 'subelements-map jc subelements)
317 (jabber-send-sexp-if-connected jc `(presence () ,@subelements))))
319 ;; Then send presence to groupchats
320 (dolist (gc *jabber-active-groupchats*)
321 (let* ((buffer (get-buffer (jabber-muc-get-buffer (car gc))))
322 (jc (when buffer
323 (buffer-local-value 'jabber-buffer-connection buffer)))
324 (subelements (cdr (assq jc subelements-map))))
325 (when jc
326 (jabber-send-sexp-if-connected
327 jc `(presence ((to . ,(concat (car gc) "/" (cdr gc))))
328 ,@subelements))))))
330 (jabber-display-roster))
332 (defun jabber-presence-children (jc)
333 "Return the children for a <presence/> stanza."
334 `(,(when (> (length *jabber-current-status*) 0)
335 `(status () ,*jabber-current-status*))
336 ,(when (> (length *jabber-current-show*) 0)
337 `(show () ,*jabber-current-show*))
338 ,(when *jabber-current-priority*
339 `(priority () ,(number-to-string *jabber-current-priority*)))
340 ,@(apply 'append (mapcar (lambda (f)
341 (funcall f jc))
342 jabber-presence-element-functions))))
344 (defun jabber-send-directed-presence (jc jid type)
345 "Send a directed presence stanza to JID.
346 TYPE is one of:
347 \"online\", \"away\", \"xa\", \"dnd\", \"chatty\":
348 Appear as present with the given status.
349 \"unavailable\":
350 Appear as offline.
351 \"probe\":
352 Ask the contact's server for updated presence.
353 \"subscribe\":
354 Ask for subscription to contact's presence.
355 (see also `jabber-send-subscription-request')
356 \"unsubscribe\":
357 Cancel your subscription to contact's presence.
358 \"subscribed\":
359 Accept contact's request for presence subscription.
360 (this is usually done within a chat buffer)
361 \"unsubscribed\":
362 Cancel contact's subscription to your presence."
363 (interactive
364 (list (jabber-read-account)
365 (jabber-read-jid-completing "Send directed presence to: ")
366 (completing-read "Type (default is online): "
367 '(("online")
368 ("away")
369 ("xa")
370 ("dnd")
371 ("chatty")
372 ("probe")
373 ("unavailable")
374 ("subscribe")
375 ("unsubscribe")
376 ("subscribed")
377 ("unsubscribed"))
378 nil t nil 'jabber-presence-history "online")))
379 (cond
380 ((member type '("probe" "unavailable"
381 "subscribe" "unsubscribe"
382 "subscribed" "unsubscribed"))
383 (jabber-send-sexp jc `(presence ((to . ,jid)
384 (type . ,type)))))
387 (let ((*jabber-current-show*
388 (if (string= type "online")
390 type))
391 (*jabber-current-status* nil))
392 (jabber-send-sexp jc `(presence ((to . ,jid))
393 ,@(jabber-presence-children jc)))))))
395 (defun jabber-send-away-presence (&optional status)
396 "Set status to away.
397 With prefix argument, ask for status message."
398 (interactive
399 (list
400 (when current-prefix-arg
401 (jabber-read-with-input-method
402 "status message: " *jabber-current-status* '*jabber-status-history*))))
403 (jabber-send-presence "away" (if status status *jabber-current-status*)
404 *jabber-current-priority*))
406 ;; XXX code duplication!
407 (defun jabber-send-xa-presence (&optional status)
408 "Send extended away presence.
409 With prefix argument, ask for status message."
410 (interactive
411 (list
412 (when current-prefix-arg
413 (jabber-read-with-input-method
414 "status message: " *jabber-current-status* '*jabber-status-history*))))
415 (jabber-send-presence "xa" (if status status *jabber-current-status*)
416 *jabber-current-priority*))
418 ;;;###autoload
419 (defun jabber-send-default-presence (&optional ignore)
420 "Send default presence.
421 Default presence is specified by `jabber-default-show',
422 `jabber-default-status', and `jabber-default-priority'."
423 (interactive)
424 (jabber-send-presence
425 jabber-default-show jabber-default-status jabber-default-priority))
427 (defun jabber-send-current-presence (&optional ignore)
428 "(Re-)send current presence.
429 That is, if presence has already been sent, use current settings,
430 otherwise send defaults (see `jabber-send-default-presence')."
431 (interactive)
432 (if *jabber-current-show*
433 (jabber-send-presence *jabber-current-show* *jabber-current-status*
434 *jabber-current-priority*)
435 (jabber-send-default-presence)))
437 (add-to-list 'jabber-jid-roster-menu (cons "Send subscription request"
438 'jabber-send-subscription-request))
439 (defun jabber-send-subscription-request (jc to &optional request)
440 "send a subscription request to jid, showing him your request
441 text, if specified"
442 (interactive (list (jabber-read-account)
443 (jabber-read-jid-completing "to: ")
444 (jabber-read-with-input-method "request: ")))
445 (jabber-send-sexp jc
446 `(presence
447 ((to . ,to)
448 (type . "subscribe"))
449 ,@(when (and request (> (length request) 0))
450 (list `(status () ,request))))))
452 (defvar jabber-roster-group-history nil
453 "History of entered roster groups")
455 (add-to-list 'jabber-jid-roster-menu
456 (cons "Add/modify roster entry" 'jabber-roster-change))
457 (defun jabber-roster-change (jc jid name groups)
458 "Add or change a roster item."
459 (interactive (let* ((jid (jabber-jid-symbol
460 (jabber-read-jid-completing "Add/change JID: ")))
461 (account (jabber-read-account))
462 (name (get jid 'name))
463 (groups (get jid 'groups))
464 (all-groups
465 (apply #'append
466 (mapcar
467 (lambda (j) (get j 'groups))
468 (plist-get (fsm-get-state-data account) :roster)))))
469 (when (string< emacs-version "22")
470 ;; Older emacsen want the completion table to be an alist...
471 (setq all-groups (mapcar #'list all-groups)))
472 (list account
473 jid (jabber-read-with-input-method (format "Name: (default `%s') " name) nil nil name)
474 (delete ""
475 (completing-read-multiple
476 (format
477 "Groups, comma-separated: (default %s) "
478 (if groups
479 (mapconcat #'identity groups ",")
480 "none"))
481 all-groups
482 nil nil nil
483 'jabber-roster-group-history
484 (mapconcat #'identity groups ",")
485 t)))))
486 ;; If new fields are added to the roster XML structure in a future standard,
487 ;; they will be clobbered by this function.
488 ;; XXX: specify account
489 (jabber-send-iq jc nil "set"
490 (list 'query (list (cons 'xmlns "jabber:iq:roster"))
491 (append
492 (list 'item (append
493 (list (cons 'jid (symbol-name jid)))
494 (if (and name (> (length name) 0))
495 (list (cons 'name name)))))
496 (mapcar #'(lambda (x) `(group () ,x))
497 groups)))
498 #'jabber-report-success "Roster item change"
499 #'jabber-report-success "Roster item change"))
501 (add-to-list 'jabber-jid-roster-menu
502 (cons "Delete roster entry" 'jabber-roster-delete))
503 (defun jabber-roster-delete (jc jid)
504 (interactive (list (jabber-read-account)
505 (jabber-read-jid-completing "Delete from roster: ")))
506 (jabber-send-iq jc nil "set"
507 `(query ((xmlns . "jabber:iq:roster"))
508 (item ((jid . ,jid)
509 (subscription . "remove"))))
510 #'jabber-report-success "Roster item removal"
511 #'jabber-report-success "Roster item removal"))
513 (defun jabber-roster-delete-jid-at-point ()
514 "Delete JID at point from roster.
515 Signal an error if there is no JID at point."
516 (interactive)
517 (let ((jid-at-point (get-text-property (point)
518 'jabber-jid))
519 (account (get-text-property (point) 'jabber-account)))
520 (if (and jid-at-point account
521 (or jabber-silent-mode (yes-or-no-p (format "Really delete %s from roster? " jid-at-point))))
522 (jabber-roster-delete account jid-at-point)
523 (error "No contact at point"))))
525 (defun jabber-roster-delete-group-from-jids (jc jids group)
526 "Delete group `group' from all JIDs"
527 (interactive)
528 (dolist (jid jids)
529 (jabber-roster-change
530 jc jid (get jid 'name)
531 (remove-if-not (lambda (g) (not (string= g group)))
532 (get jid 'groups)))))
534 (defun jabber-roster-edit-group-from-jids (jc jids group)
535 "Edit group `group' from all JIDs"
536 (interactive)
537 (let ((new-group
538 (jabber-read-with-input-method
539 (format "New group: (default `%s') " group) nil nil group)))
540 (dolist (jid jids)
541 (jabber-roster-change
542 jc jid (get jid 'name)
543 (remove-duplicates
544 (mapcar
545 (lambda (g) (if (string= g group)
546 new-group
548 (get jid 'groups))
549 :test 'string=)))))
552 (provide 'jabber-presence)
554 ;;; arch-tag: b8616d4c-dde8-423e-86c7-da7b4928afc3