summaryrefslogtreecommitdiff
path: root/jabber-presence.el
diff options
context:
space:
mode:
Diffstat (limited to 'jabber-presence.el')
-rw-r--r--jabber-presence.el565
1 files changed, 565 insertions, 0 deletions
diff --git a/jabber-presence.el b/jabber-presence.el
new file mode 100644
index 0000000..5f4573d
--- /dev/null
+++ b/jabber-presence.el
@@ -0,0 +1,565 @@
+;; jabber-presence.el - roster and presence bookkeeping
+
+;; 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 <presence/> stanzas.
+Each function takes one argument, the connection, and returns a
+possibly empty list of extra child element of the <presence/>
+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."
+ (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)
+ ;; 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"
+ ;; 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"
+ (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 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 <presence/> stanza."
+ `(,(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."
+ (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, showing him your request
+text, if specified"
+ (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."
+ (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"
+ (interactive)
+ (dolist (jid jids)
+ (jabber-roster-change
+ jc jid (get jid 'name)
+ (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"
+ (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)
+ (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