;; jabber-presence.el - roster and presence bookkeeping -*- lexical-binding: t; -*-
;; Copyright (C) 2003, 2004, 2007, 2008 - Magnus Henoch - mange@freemail.hu
;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net
;; This file is a part of jabber.el.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, write to the Free Software
;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
(require 'jabber-core)
(require 'jabber-iq)
(require 'jabber-alert)
(require 'jabber-util)
(require 'jabber-menu)
(require 'jabber-muc)
(defvar jabber-presence-element-functions nil
"List of functions returning extra elements for stanzas.
Each function takes one argument, the connection, and returns a
possibly empty list of extra child element of the
stanza.")
(defvar jabber-presence-history ()
"Keeps track of previously used presence status types.")
(add-to-list 'jabber-iq-set-xmlns-alist
(cons "jabber:iq:roster" (function (lambda (jc x) (jabber-process-roster jc x nil)))))
(defun jabber-process-roster (jc xml-data closure-data)
"Process an incoming roster infoquery result.
CLOSURE-DATA should be 'initial if initial roster push, nil otherwise.
JC is the Jabber connection.
XML-DATA is the parsed tree data from the stream (stanzas)
obtained from `xml-parse-region'."
(let ((roster (plist-get (fsm-get-state-data jc) :roster))
(from (jabber-xml-get-attribute xml-data 'from))
(type (jabber-xml-get-attribute xml-data 'type))
(id (jabber-xml-get-attribute xml-data 'id))
(username (plist-get (fsm-get-state-data jc) :username))
(server (plist-get (fsm-get-state-data jc) :server))
(resource (plist-get (fsm-get-state-data jc) :resource))
new-items changed-items deleted-items)
;; Perform sanity check on "from" attribute: it should be either absent
;; match our own JID, or match the server's JID (the latter is what
;; Facebook does).
(if (not (or (null from)
(string= from server)
(string= from (concat username "@" server))
(string= from (concat username "@" server "/" resource))))
(message "Roster push with invalid \"from\": \"%s\" (expected \"%s\", \"%s@%s\" or \"%s@%s/%s\")"
from
server username server username server resource)
(dolist (item (jabber-xml-get-children (car (jabber-xml-get-children xml-data 'query)) 'item))
(let (roster-item
(jid (jabber-jid-symbol (jabber-xml-get-attribute item 'jid))))
;; If subscripton="remove", contact is to be removed from roster
(if (string= (jabber-xml-get-attribute item 'subscription) "remove")
(progn
(if (jabber-jid-rostername jid)
(message "%s (%s) removed from roster" (jabber-jid-rostername jid) jid)
(message "%s removed from roster" jid))
(push jid deleted-items))
;; Find contact if already in roster
(setq roster-item (car (memq jid roster)))
(if roster-item
(push roster-item changed-items)
;; If not found, create a new roster item.
(unless (eq closure-data 'initial)
(if (jabber-xml-get-attribute item 'name)
(message "%s (%s) added to roster" (jabber-xml-get-attribute item 'name) jid)
(message "%s added to roster" jid)))
(setq roster-item jid)
(push roster-item new-items))
;; If this is an initial push, we want to forget
;; everything we knew about this contact before - e.g. if
;; the contact was online when we disconnected and offline
;; when we reconnect, we don't want to see stale presence
;; information. This assumes that no contacts are shared
;; between accounts.
(when (eq closure-data 'initial)
(setplist roster-item nil))
;; Now, get all data associated with the contact.
(put roster-item 'name (jabber-xml-get-attribute item 'name))
(put roster-item 'subscription (jabber-xml-get-attribute item 'subscription))
(put roster-item 'ask (jabber-xml-get-attribute item 'ask))
;; Since roster items can't be changed incrementally, we
;; save the original XML to be able to modify it, instead of
;; having to reproduce it. This is for forwards
;; compatibility.
(put roster-item 'xml item)
(put roster-item 'groups
(mapcar (lambda (foo) (nth 2 foo))
(jabber-xml-get-children item 'group)))))))
;; This is the function that does the actual updating and
;; redrawing of the roster.
(jabber-roster-update jc new-items changed-items deleted-items)
(if (and id (string= type "set"))
(jabber-send-iq jc nil "result" nil
nil nil nil nil id)))
;; After initial roster push, run jabber-post-connect-hooks. We do
;; it here and not before since we want to have the entire roster
;; before we receive any presence stanzas.
(when (eq closure-data 'initial)
(run-hook-with-args 'jabber-post-connect-hooks jc)))
(defun jabber-initial-roster-failure (jc xml-data _closure-data)
"Report the initial roster failure.
JC is the Jabber connection.
XML-DATA is the parsed tree data from the stream (stanzas)
obtained from `xml-parse-region'."
;; If the initial roster request fails, let's report it, but run
;; `jabber-post-connect-hooks' anyway. According to the spec, there is
;; nothing exceptional about the server not returning a roster.
(jabber-report-success jc xml-data "Initial roster retrieval")
(run-hook-with-args 'jabber-post-connect-hooks jc))
(add-to-list 'jabber-presence-chain 'jabber-process-presence)
(defun jabber-process-presence (jc xml-data)
"Process incoming presence tags.
JC is the Jabber connection.
XML-DATA is the parsed tree data from the stream (stanzas)
obtained from `xml-parse-region'."
;; XXX: use JC argument
(let ((roster (plist-get (fsm-get-state-data jc) :roster))
(from (jabber-xml-get-attribute xml-data 'from))
(to (jabber-xml-get-attribute xml-data 'to))
(type (jabber-xml-get-attribute xml-data 'type))
(presence-show (car (jabber-xml-node-children
(car (jabber-xml-get-children xml-data 'show)))))
(presence-status (car (jabber-xml-node-children
(car (jabber-xml-get-children xml-data 'status)))))
(error (car (jabber-xml-get-children xml-data 'error)))
(priority (string-to-number (or (car (jabber-xml-node-children (car (jabber-xml-get-children xml-data 'priority))))
"0"))))
(cond
((string= type "subscribe")
(run-with-idle-timer 0.01 nil #'jabber-process-subscription-request jc from presence-status))
((jabber-muc-presence-p xml-data)
(jabber-muc-process-presence jc xml-data))
(t
;; XXX: Think about what to do about out-of-roster presences.
(let ((buddy (jabber-jid-symbol from)))
(if (memq buddy roster)
(let* ((oldstatus (get buddy 'show))
(resource (or (jabber-jid-resource from) ""))
(resource-plist (cdr (assoc resource
(get buddy 'resources))))
newstatus)
(cond
((and (string= resource "") (member type '("unavailable" "error")))
;; 'unavailable' or 'error' from bare JID means that all resources
;; are offline.
(setq resource-plist nil)
(setq newstatus (if (string= type "error") "error" nil))
(let ((new-message (if error
(jabber-parse-error error)
presence-status)))
;; erase any previous information
(put buddy 'resources nil)
(put buddy 'connected nil)
(put buddy 'show newstatus)
(put buddy 'status new-message)))
((string= type "unavailable")
(setq resource-plist
(plist-put resource-plist 'connected nil))
(setq resource-plist
(plist-put resource-plist 'show nil))
(setq resource-plist
(plist-put resource-plist 'status
presence-status)))
((string= type "error")
(setq newstatus "error")
(setq resource-plist
(plist-put resource-plist 'connected nil))
(setq resource-plist
(plist-put resource-plist 'show "error"))
(setq resource-plist
(plist-put resource-plist 'status
(if error
(jabber-parse-error error)
presence-status))))
((or
(string= type "unsubscribe")
(string= type "subscribed")
(string= type "unsubscribed"))
;; Do nothing, except letting the user know. The Jabber protocol
;; places all this complexity on the server.
(setq newstatus type))
(t
(setq resource-plist
(plist-put resource-plist 'connected t))
(setq resource-plist
(plist-put resource-plist 'show (or presence-show "")))
(setq resource-plist
(plist-put resource-plist 'status
presence-status))
(setq resource-plist
(plist-put resource-plist 'priority priority))
(setq newstatus (or presence-show ""))))
(when resource-plist
;; this is for `assoc-set!' in guile
(if (assoc resource (get buddy 'resources))
(setcdr (assoc resource (get buddy 'resources)) resource-plist)
(put buddy 'resources (cons (cons resource resource-plist) (get buddy 'resources))))
(jabber-prioritize-resources buddy))
(fsm-send jc (cons :roster-update buddy))
(dolist (hook '(jabber-presence-hooks jabber-alert-presence-hooks))
(run-hook-with-args hook
buddy
oldstatus
newstatus
(plist-get resource-plist 'status)
(funcall jabber-alert-presence-message-function
buddy
oldstatus
newstatus
(plist-get resource-plist 'status)))))))))))
(defun jabber-process-subscription-request (jc from presence-status)
"Process an incoming subscription request.
JC is the Jabber connection."
(with-current-buffer (jabber-chat-create-buffer jc from)
(ewoc-enter-last jabber-chat-ewoc (list :subscription-request presence-status :time (current-time)))
(dolist (hook '(jabber-presence-hooks jabber-alert-presence-hooks))
(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)))))
(defun jabber-subscription-accept-mutual (&rest ignored)
(message "Subscription accepted; reciprocal subscription request sent")
(jabber-subscription-reply "subscribed" "subscribe"))
(defun jabber-subscription-accept-one-way (&rest ignored)
(message "Subscription accepted")
(jabber-subscription-reply "subscribed"))
(defun jabber-subscription-decline (&rest ignored)
(message "Subscription declined")
(jabber-subscription-reply "unsubscribed"))
(defun jabber-subscription-reply (&rest types)
(let ((to (jabber-jid-user jabber-chatting-with)))
(dolist (type types)
(jabber-send-sexp jabber-buffer-connection `(presence ((to . ,to) (type . ,type)))))))
(defun jabber-prioritize-resources (buddy)
"Set connected, show and status properties for BUDDY.
Show status properties from highest-priority resource."
(let ((resource-alist (get buddy 'resources))
(highest-priority nil))
;; Reset to nil at first, for cases (a) resource-alist is nil
;; and (b) all resources are disconnected.
(put buddy 'connected nil)
(put buddy 'show nil)
(put buddy 'status nil)
(mapc #'(lambda (resource)
(let* ((resource-plist (cdr resource))
(priority (plist-get resource-plist 'priority)))
(if (plist-get resource-plist 'connected)
(when (or (null highest-priority)
(and priority
(> priority highest-priority)))
;; if no priority specified, interpret as zero
(setq highest-priority (or priority 0))
(put buddy 'connected (plist-get resource-plist 'connected))
(put buddy 'show (plist-get resource-plist 'show))
(put buddy 'status (plist-get resource-plist 'status))
(put buddy 'resource (car resource)))
;; if we have not found a connected resource yet, but this
;; disconnected resource has a status message, display it.
(when (not (get buddy 'connected))
(if (plist-get resource-plist 'status)
(put buddy 'status (plist-get resource-plist 'status)))
(if (plist-get resource-plist 'show)
(put buddy 'show (plist-get resource-plist 'show)))))))
resource-alist)))
(defun jabber-count-connected-resources (buddy)
"Return the number of connected resources for BUDDY."
(let ((resource-alist (get buddy 'resources))
(count 0))
(dolist (resource resource-alist)
(if (plist-get (cdr resource) 'connected)
(setq count (1+ count))))
count))
;;;###autoload
(defun jabber-send-presence (show status priority)
"Set presence for all accounts."
(interactive
(list
(completing-read "show: " '("" "away" "xa" "dnd" "chat")
nil t nil 'jabber-presence-history)
(jabber-read-with-input-method "status message: " *jabber-current-status*
'*jabber-status-history*)
(read-string "priority: " (int-to-string (if *jabber-current-priority*
*jabber-current-priority*
jabber-default-priority)))))
(setq *jabber-current-show* show *jabber-current-status* status)
(setq *jabber-current-priority*
(if (numberp priority) priority (string-to-number priority)))
(let (subelements-map)
;; For each connection, we use a different set of subelements. We
;; cache them, to only generate them once.
;; Ordinary presence, with no specified recipient
(dolist (jc jabber-connections)
(let ((subelements (jabber-presence-children jc)))
(push (cons jc subelements) subelements-map)
(jabber-send-sexp-if-connected jc `(presence () ,@subelements))))
;; Then send presence to groupchats
(dolist (gc *jabber-active-groupchats*)
(let* ((buffer (get-buffer (jabber-muc-get-buffer (car gc))))
(jc (when buffer
(buffer-local-value 'jabber-buffer-connection buffer)))
(subelements (cdr (assq jc subelements-map))))
(when jc
(jabber-send-sexp-if-connected
jc `(presence ((to . ,(concat (car gc) "/" (cdr gc))))
,@subelements))))))
(jabber-display-roster))
(defun jabber-presence-children (jc)
"Return the children for a stanza.
JC is the Jabber connection."
`(,(when (> (length *jabber-current-status*) 0)
`(status () ,*jabber-current-status*))
,(when (> (length *jabber-current-show*) 0)
`(show () ,*jabber-current-show*))
,(when *jabber-current-priority*
`(priority () ,(number-to-string *jabber-current-priority*)))
,@(apply 'append (mapcar (lambda (f)
(funcall f jc))
jabber-presence-element-functions))))
(defun jabber-send-directed-presence (jc jid type)
"Send a directed presence stanza to JID.
TYPE is one of:
\"online\", \"away\", \"xa\", \"dnd\", \"chatty\":
Appear as present with the given status.
\"unavailable\":
Appear as offline.
\"probe\":
Ask the contact's server for updated presence.
\"subscribe\":
Ask for subscription to contact's presence.
(see also `jabber-send-subscription-request')
\"unsubscribe\":
Cancel your subscription to contact's presence.
\"subscribed\":
Accept contact's request for presence subscription.
(this is usually done within a chat buffer)
\"unsubscribed\":
Cancel contact's subscription to your presence.
JC is the Jabber connection."
(interactive
(list (jabber-read-account)
(jabber-read-jid-completing "Send directed presence to: ")
(completing-read "Type (default is online): "
'(("online")
("away")
("xa")
("dnd")
("chatty")
("probe")
("unavailable")
("subscribe")
("unsubscribe")
("subscribed")
("unsubscribed"))
nil t nil 'jabber-presence-history "online")))
(cond
((member type '("probe" "unavailable"
"subscribe" "unsubscribe"
"subscribed" "unsubscribed"))
(jabber-send-sexp jc `(presence ((to . ,jid)
(type . ,type)))))
(t
(let ((*jabber-current-show*
(if (string= type "online")
""
type))
(*jabber-current-status* nil))
(jabber-send-sexp jc `(presence ((to . ,jid))
,@(jabber-presence-children jc)))))))
(defun jabber-send-away-presence (&optional status)
"Set status to away.
With prefix argument, ask for status message."
(interactive
(list
(when current-prefix-arg
(jabber-read-with-input-method
"status message: " *jabber-current-status* '*jabber-status-history*))))
(jabber-send-presence "away" (if status status *jabber-current-status*)
*jabber-current-priority*))
;; XXX code duplication!
(defun jabber-send-xa-presence (&optional status)
"Send extended away presence.
With prefix argument, ask for status message."
(interactive
(list
(when current-prefix-arg
(jabber-read-with-input-method
"status message: " *jabber-current-status* '*jabber-status-history*))))
(jabber-send-presence "xa" (if status status *jabber-current-status*)
*jabber-current-priority*))
;;;###autoload
(defun jabber-send-default-presence (&optional _ignore)
"Send default presence.
Default presence is specified by `jabber-default-show',
`jabber-default-status', and `jabber-default-priority'."
(interactive)
(jabber-send-presence
jabber-default-show jabber-default-status jabber-default-priority))
(defun jabber-send-current-presence (&optional _ignore)
"(Re-)send current presence.
That is, if presence has already been sent, use current settings,
otherwise send defaults (see `jabber-send-default-presence')."
(interactive)
(if *jabber-current-show*
(jabber-send-presence *jabber-current-show* *jabber-current-status*
*jabber-current-priority*)
(jabber-send-default-presence)))
(add-to-list 'jabber-jid-roster-menu (cons "Send subscription request"
'jabber-send-subscription-request))
(defun jabber-send-subscription-request (jc to &optional request)
"Send a subscription request to jid.
Show him your request text, if specified.
JC is the Jabber connection."
(interactive (list (jabber-read-account)
(jabber-read-jid-completing "to: ")
(jabber-read-with-input-method "request: ")))
(jabber-send-sexp jc
`(presence
((to . ,to)
(type . "subscribe"))
,@(when (and request (> (length request) 0))
(list `(status () ,request))))))
(defvar jabber-roster-group-history nil
"History of entered roster groups.")
(add-to-list 'jabber-jid-roster-menu
(cons "Add/modify roster entry" 'jabber-roster-change))
(defun jabber-roster-change (jc jid name groups)
"Add or change a roster item.
JC is the Jabber connection."
(interactive (let* ((jid (jabber-jid-symbol
(jabber-read-jid-completing "Add/change JID: ")))
(account (jabber-read-account))
(name (get jid 'name))
(groups (get jid 'groups))
(all-groups
(apply #'append
(mapcar
(lambda (j) (get j 'groups))
(plist-get (fsm-get-state-data account) :roster)))))
(when (string< emacs-version "22")
;; Older emacsen want the completion table to be an alist...
(setq all-groups (mapcar #'list all-groups)))
(list account
jid (jabber-read-with-input-method (format "Name: (default `%s') " name) nil nil name)
(delete ""
(completing-read-multiple
(format
"Groups, comma-separated: (default %s) "
(if groups
(mapconcat #'identity groups ",")
"none"))
all-groups
nil nil nil
'jabber-roster-group-history
(mapconcat #'identity groups ",")
t)))))
;; If new fields are added to the roster XML structure in a future standard,
;; they will be clobbered by this function.
;; XXX: specify account
(jabber-send-iq jc nil "set"
(list 'query (list (cons 'xmlns "jabber:iq:roster"))
(append
(list 'item (append
(list (cons 'jid (symbol-name jid)))
(if (and name (> (length name) 0))
(list (cons 'name name)))))
(mapcar #'(lambda (x) `(group () ,x))
groups)))
#'jabber-report-success "Roster item change"
#'jabber-report-success "Roster item change"))
(add-to-list 'jabber-jid-roster-menu
(cons "Delete roster entry" 'jabber-roster-delete))
(defun jabber-roster-delete (jc jid)
(interactive (list (jabber-read-account)
(jabber-read-jid-completing "Delete from roster: ")))
(jabber-send-iq jc nil "set"
`(query ((xmlns . "jabber:iq:roster"))
(item ((jid . ,jid)
(subscription . "remove"))))
#'jabber-report-success "Roster item removal"
#'jabber-report-success "Roster item removal"))
(defun jabber-roster-delete-jid-at-point ()
"Delete JID at point from roster.
Signal an error if there is no JID at point."
(interactive)
(let ((jid-at-point (get-text-property (point)
'jabber-jid))
(account (get-text-property (point) 'jabber-account)))
(if (and jid-at-point account
(or jabber-silent-mode (yes-or-no-p (format "Really delete %s from roster? " jid-at-point))))
(jabber-roster-delete account jid-at-point)
(error "No contact at point"))))
(defun jabber-roster-delete-group-from-jids (jc jids group)
"Delete group `group' from all JIDs.
JC is the Jabber connection."
(interactive)
(dolist (jid jids)
(jabber-roster-change
jc jid (get jid 'name)
(cl-remove-if-not (lambda (g) (not (string= g group)))
(get jid 'groups)))))
(defun jabber-roster-edit-group-from-jids (jc jids group)
"Edit group `group' from all JIDs.
JC is the Jabber connection."
(interactive)
(let ((new-group
(jabber-read-with-input-method
(format "New group: (default `%s') " group) nil nil group)))
(dolist (jid jids)
(jabber-roster-change
jc jid (get jid 'name)
(cl-remove-duplicates
(mapcar
(lambda (g) (if (string= g group)
new-group
g))
(get jid 'groups))
:test 'string=)))))
(provide 'jabber-presence)
;;; arch-tag: b8616d4c-dde8-423e-86c7-da7b4928afc3