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
)
24 (require 'jabber-alert
)
25 (require 'jabber-util
)
26 (require 'jabber-menu
)
28 (require 'jabber-autoloads
)
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/>
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\")"
61 username server username server resource
)
63 (dolist (item (jabber-xml-get-children (car (jabber-xml-get-children xml-data
'query
)) '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")
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
)))
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
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
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
))))
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
))))
152 ((and (string= resource
"") (member type
'("unavailable" "error")))
153 ;; 'unavailable' or 'error' from bare JID means that all resources
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
)
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")
168 (plist-put resource-plist
'connected nil
))
170 (plist-put resource-plist
'show nil
))
172 (plist-put resource-plist
'status
175 ((string= type
"error")
176 (setq newstatus
"error")
178 (plist-put resource-plist
'connected nil
))
180 (plist-put resource-plist
'show
"error"))
182 (plist-put resource-plist
'status
184 (jabber-parse-error error
)
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
))
195 (plist-put resource-plist
'connected t
))
197 (plist-put resource-plist
'show
(or presence-show
"")))
199 (plist-put resource-plist
'status
202 (plist-put resource-plist
'priority priority
))
203 (setq newstatus
(or presence-show
""))))
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
219 (plist-get resource-plist
'status
)
220 (funcall jabber-alert-presence-message-function
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
)))
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
)
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
)))))))
283 (defun jabber-count-connected-resources (buddy)
284 "Return the number of connected resources for BUDDY."
285 (let ((resource-alist (get buddy
'resources
))
287 (dolist (resource resource-alist
)
288 (if (plist-get (cdr resource
) 'connected
)
289 (setq count
(1+ count
))))
293 (defun jabber-send-presence (show status priority
)
294 "Set presence for all accounts."
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
))))
323 (buffer-local-value 'jabber-buffer-connection buffer
)))
324 (subelements (cdr (assq jc subelements-map
))))
326 (jabber-send-sexp-if-connected
327 jc
`(presence ((to .
,(concat (car gc
) "/" (cdr gc
))))
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)
342 jabber-presence-element-functions
))))
344 (defun jabber-send-directed-presence (jc jid type
)
345 "Send a directed presence stanza to JID.
347 \"online\", \"away\", \"xa\", \"dnd\", \"chatty\":
348 Appear as present with the given status.
352 Ask the contact's server for updated presence.
354 Ask for subscription to contact's presence.
355 (see also `jabber-send-subscription-request')
357 Cancel your subscription to contact's presence.
359 Accept contact's request for presence subscription.
360 (this is usually done within a chat buffer)
362 Cancel contact's subscription to your presence."
364 (list (jabber-read-account)
365 (jabber-read-jid-completing "Send directed presence to: ")
366 (completing-read "Type (default is online): "
378 nil t nil
'jabber-presence-history
"online")))
380 ((member type
'("probe" "unavailable"
381 "subscribe" "unsubscribe"
382 "subscribed" "unsubscribed"))
383 (jabber-send-sexp jc
`(presence ((to .
,jid
)
387 (let ((*jabber-current-show
*
388 (if (string= type
"online")
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
)
397 With prefix argument, ask for status message."
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."
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
*))
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'."
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')."
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
442 (interactive (list (jabber-read-account)
443 (jabber-read-jid-completing "to: ")
444 (jabber-read-with-input-method "request: ")))
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
))
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
)))
473 jid
(jabber-read-with-input-method (format "Name: (default `%s') " name
) nil nil name
)
475 (completing-read-multiple
477 "Groups, comma-separated: (default %s) "
479 (mapconcat #'identity groups
",")
483 'jabber-roster-group-history
484 (mapconcat #'identity groups
",")
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"))
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
))
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"))
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."
517 (let ((jid-at-point (get-text-property (point)
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"
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"
538 (jabber-read-with-input-method
539 (format "New group: (default `%s') " group
) nil nil group
)))
541 (jabber-roster-change
542 jc jid
(get jid
'name
)
545 (lambda (g) (if (string= g group
)
552 (provide 'jabber-presence
)
554 ;;; arch-tag: b8616d4c-dde8-423e-86c7-da7b4928afc3