diff options
author | Matteo F. Vescovi <mfv@debian.org> | 2016-11-06 14:36:10 +0100 |
---|---|---|
committer | Matteo F. Vescovi <mfv@debian.org> | 2016-11-06 14:36:10 +0100 |
commit | 484a3c53538e94f8d8c6a432b3c3174d98deb92f (patch) | |
tree | 4d1ea21a7698463dcfbdaf0461343c7ee25320fe |
Import Upstream version 0.7
50 files changed, 11343 insertions, 0 deletions
@@ -0,0 +1,15 @@ +Developers: +Tom Berger +Magnus Henoch + +Contributors: +Mathias Dahl +Mario Domenech Goulart +Nolan Eakins +François Fleuret +Justin Kirby +Carl Henrik Lunde +Andrey Slusar + + +arch-tag: 15700144-3BD9-11D9-871C-000A95C2FCD0 @@ -0,0 +1,92 @@ +-*- mode: outline -*- + +* New features in jabber.el 0.7 + +** SSL connections possible +See variable `jabber-connection-type'. + +** Chat buffers rewritten +New modular design gives increased extensibility. +*** Received URLs are displayed +*** Long lines are filled +See jabber-chat-fill-long-lines. +*** Rare timestamps are printed by default +See jabber-print-rare-time and jabber-rare-time-format. + +** MUC features +*** Different default nicknames for different MUC rooms +See jabber-muc-default-nicknames. +*** Autojoin MUC rooms on connection +See jabber-muc-autojoin. +*** Change nickname +Actually simply an alias from jabber-muc-nick to jabber-groupchat-join. +*** Invitations +Both sending and receiving invitiations is supported. +*** Basic affiliation change support +(Not finished) +*** Private MUC messages +*** Support for setting and displaying topic + +** Global key bindings +Global keymap under C-x C-j. + +** Vcard viewer and editor + +** Roster export + +** Message events (JEP-0022) + +** Easy way to define external notifiers +See define-jabber-alert. Alerts for Festival (speech synthesis), +Sawfish, and xmessage added. + +** Activity mode improved +Can now display count in frame title. Update hook added. + +** Roster display optimized + +** Optionally use per-contact history files + +** Jabber menu in menubar not enabled by default +Call jabber-menu to have it there. + +** Flyspell in chat buffers +Flyspell will only spell check what you're currently writing. + +** Different time formats for instant and delayed messages +See `jabber-chat-time-format' and `jabber-chat-delayed-time-format'. +You can see the complete timestamp in a tooltip by holding the mouse +over the prompt. + +** Chat buffers in inactive windows are scrolled + +** Roster is sorted by name also + +* New features in jabber.el 0.6.1 + +** Message history +Set jabber-history-enabled to t to activate it. + +** Backlogs +If you have history enabled, the last few messages are inserted when +you open a new chat buffer. + +** Activity tracking on the mode line +Activate it with M-x jabber-activity-mode. + +** Receive an alert when a specific person goes online +Use it with M-x jabber-watch-add. + +** Support for /me in chats +As in "/me laughs" etc. + +** Message alerts for current buffer can be disabled +Set jabber-message-alert-same-buffer to nil to do that. + +** Basic moderation support in MUC + +** MUC alerts are separated from ordinary message alerts +Customize jabber-alert-muc-hooks to get your desired behaviour. + + +arch-tag: 1CE20E4E-3BD9-11D9-8D64-000A95C2FCD0 @@ -0,0 +1,104 @@ +This is jabber.el 0.7, a Jabber client for Emacs. If you don't know +what Jabber is, see http://www.jabber.org . + +Home page: http://emacs-jabber.sourceforge.net +Project page: http://sourceforge.net/projects/emacs-jabber +Wiki page: http://www.emacswiki.org/cgi-bin/wiki/JabberEl +Mailing list: http://lists.sourceforge.net/lists/listinfo/emacs-jabber-general +and: http://dir.gmane.org/gmane.emacs.jabber.general + +GNU Emacs +========= + +jabber.el depends on GNU Emacs 21, in particular xml.el, and some +files from Gnus 5.10. If you don't have Gnus 5.10 (M-x gnus-version +will tell), you can download sha1.el and hex-util.el from Gnus CVS +at http://quimby.gnus.org/cgi-bin/cvsweb.cgi/gnus/lisp/ . For your +convenience, these files are included in the tarball. + +XEmacs +====== + +You need an XEmacs with Mule support, and recent versions of the gnus, +net-utils and mule-ucs packages. jabber.el basically works on XEmacs, +but some features are missing (in particular mouse support). Testing +and patches are very welcome. + +SASL +==== +jabber.el will use the SASL library of FLIM (Faithful Library about +Internet Message; it is also included in newer versions of Gnus) if +it's present. If not, it will fall back to JEP-0077 authentication. + +TLS/SSL +======= +To get an encrypted connection, you need either tls.el (from Gnus) or +ssl.el. These are interfaces to GnuTLS and OpenSSL, respectively; use +the appropriate one. Recent versions of tls.el support both programs, +though. The version of ssl.el distributed with Gnus is outdated; use +the one from W3 CVS instead: +http://cvs.savannah.gnu.org/viewcvs/w3/lisp/ssl.el?root=w3 + +To actually use encryption, customize the variables +jabber-connection-type and jabber-connection-ssl-program. + +Note that only the connection from you to the server is encrypted; +there is no guarantee of other connections being encrypted. + +StartTLS is not supported in this version of jabber.el. + +Installation +============ +To install, put all .el files somewhere in your load-path (or have +your load-path include the directory they're in) and put +(require 'jabber) in your .emacs file. To install the Info +documentation, copy jabber.info to /usr/local/info and run +"install-info /usr/local/info/jabber.info". + +If you've been using a post-0.6 CVS version of jabber.el, you might +need to remove some redundant hook functions. Make sure that +jabber-alert-message-hooks doesn't contain jabber-message-history, and +that jabber-alert-presence-hooks doesn't contain +jabber-presence-watch. + +Usage +===== +To start using it, type M-x jabber-customize and set your username and +server. Then, type C-x C-j C-c (or equivalently M-x jabber-connect) +to connect (with prefix argument, register new account). + +Your roster is displayed in a buffer called *-jabber-*. To +disconnect, type C-x C-j C-d or M-x jabber-disconnect. + +You may want to use the menu bar to execute Jabber commands. To +enable the Jabber menu, type M-x jabber-menu. + +For a less terse description, read the enclosed manual. + +For bug reports, help requests and other feedback, use the trackers +and forums at the project page mentioned above. + +Configuration +============= +All available configuration options are described in the manual. This +section only serves to point out the most important ones. + +To change how you are notified about incoming events, type M-x +customize-group RET jabber-alerts. + +To activate logging of all chats, set jabber-history-enabled to t. By +default, history will be saved in ~/.jabber_global_message_log; make +sure that this file has appropriate permissions. Type M-x +customize-group RET jabber-history for more options. + +By default, jabber.el will send a confirmation when messages sent to +you are delivered and displayed, and also send "contact is typing" +notifications. To change this, type M-x customize-group RET +jabber-events, and set the three jabber-events-confirm-* variables to +nil. + +File transfer +============= +This release of jabber.el contains experimental support for file +transfer. It is not enabled by default. See the file +filetransfer.txt for details. diff --git a/filetransfer.txt b/filetransfer.txt new file mode 100644 index 0000000..756f7e3 --- /dev/null +++ b/filetransfer.txt @@ -0,0 +1,55 @@ +-*- outline -*- +* File transfer + +This release of jabber.el contains some support for file transfer. +Both sending and receiving files are supported. Since this feature +needs more testing, it is not enabled by default. To enable it, add + +(require 'jabber-ft-server) +(require 'jabber-ft-client) +(require 'jabber-socks5) + +to your .emacs file. Please share your experiences - does it work for +you? Can you suggest any improvements? + +** Sending files + +Sending files over Jabber normally requires the ability to listen on a +network port. As of Emacs 21.3 and XEmacs 21.4, elisp programs can't +do this, so you have to specify a JEP-0065 proxy. The variable +jabber-socks5-proxies is a list of proxies to use. "proxy.jabber.org" +and "proxy65.jabber.ccc.de" are the only proxies I know of. + +After you have specified one or more proxies, jabber.el needs to know +their network addresses. Type M-x jabber-socks5-query-all-proxies, +and watch the progress in the echo area. Note that you have to be +connected when you do this, and that you have to do this every +session. + +To send a file, type M-x jabber-ft-send. You will be asked for which +file to send, and whom to send it to. You have to specify a complete +JID with resource, such as user@domain/resource - only user@domain +will not work. To see the resources of your contacts, set +jabber-show-resources to t and type M-x jabber-display-roster. + +While the file is being sent, your Emacs will be locked up and you +can't do anything else. Hopefully, this will be fixed some time. + +** Receiving files + +When someone tries to send a file to you, you will get a message +either in the echo area or in a dialog box, asking you to confirm. +You will also be asked for where to save the file. + +Receiving a file should not cause any interruption to your work. If +it does, please tell. + +** Protocol details + +See JEPs 95, 96 and 65. + +SOCKS5 (JEP-0065) is the only stream method currently supported by +jabber.el, in conflict with JEP-0096, which requires that In-Band +Bytestreams be supported as well. + +Range requests are not supported, neither in sending nor in receiving. diff --git a/hex-util.el b/hex-util.el new file mode 100644 index 0000000..1cc67c2 --- /dev/null +++ b/hex-util.el @@ -0,0 +1,75 @@ +;;; hex-util.el --- Functions to encode/decode hexadecimal string. + +;; Copyright (C) 1999, 2001, 2002, 2003, 2004, +;; 2005 Free Software Foundation, Inc. + +;; Author: Shuhei KOBAYASHI <shuhei@aqua.ocn.ne.jp> +;; Keywords: data + +;; This file is part of FLIM (Faithful Library about Internet Message). + +;; 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, 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; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: + +;;; Code: + +(eval-when-compile + (defmacro hex-char-to-num (chr) + (` (let ((chr (, chr))) + (cond + ((and (<= ?a chr)(<= chr ?f)) (+ (- chr ?a) 10)) + ((and (<= ?A chr)(<= chr ?F)) (+ (- chr ?A) 10)) + ((and (<= ?0 chr)(<= chr ?9)) (- chr ?0)) + (t (error "Invalid hexadecimal digit `%c'" chr)))))) + (defmacro num-to-hex-char (num) + (` (aref "0123456789abcdef" (, num))))) + +(defun decode-hex-string (string) + "Decode hexadecimal STRING to octet string." + (let* ((len (length string)) + (dst (make-string (/ len 2) 0)) + (idx 0)(pos 0)) + (while (< pos len) +;;; logior and lsh are not byte-coded. +;;; (aset dst idx (logior (lsh (hex-char-to-num (aref string pos)) 4) +;;; (hex-char-to-num (aref string (1+ pos))))) + (aset dst idx (+ (* (hex-char-to-num (aref string pos)) 16) + (hex-char-to-num (aref string (1+ pos))))) + (setq idx (1+ idx) + pos (+ 2 pos))) + dst)) + +(defun encode-hex-string (string) + "Encode octet STRING to hexadecimal string." + (let* ((len (length string)) + (dst (make-string (* len 2) 0)) + (idx 0)(pos 0)) + (while (< pos len) +;;; logand and lsh are not byte-coded. +;;; (aset dst idx (num-to-hex-char (logand (lsh (aref string pos) -4) 15))) + (aset dst idx (num-to-hex-char (/ (aref string pos) 16))) + (setq idx (1+ idx)) +;;; (aset dst idx (num-to-hex-char (logand (aref string pos) 15))) + (aset dst idx (num-to-hex-char (% (aref string pos) 16))) + (setq idx (1+ idx) + pos (1+ pos))) + dst)) + +(provide 'hex-util) + +;;; arch-tag: fe8aaa79-6c86-400e-813f-5a8cc4cb3859 +;;; hex-util.el ends here diff --git a/jabber-activity.el b/jabber-activity.el new file mode 100644 index 0000000..d9cc861 --- /dev/null +++ b/jabber-activity.el @@ -0,0 +1,373 @@ +;;; jabber-activity.el --- show jabber activity in the mode line + +;; Copyright (C) 2004 Carl Henrik Lunde - <chlunde+jabber+@ping.uio.no> + +;; 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, or (at your option) +;; any later version. + +;; GNU Emacs 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 GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; Allows tracking messages from buddies using the global mode line +;; See (info "(jabber)Tracking activity") + +;;; TODO: + +;; - Make it possible to enable this mode using M-x customize +;; - When Emacs is on another desktop, (get-buffer-window buf 'visible) +;; returns nil. We need to know when the user selects the frame again +;; so we can remove the string from the mode line. (Or just run +;; jabber-activity-clean often). +;; - jabber-activity-switch-to needs a keybinding. In which map? +;; - Is there any need for having defcustom jabber-activity-make-string? +;; - When there's activity in a buffer it would be nice with a hook which +;; does the opposite of bury-buffer, so switch-to-buffer will show that +;; buffer first. + +;;; Code: + +(require 'jabber-core) +(require 'jabber-alert) +(require 'jabber-util) +(require 'cl) + +(defgroup jabber-activity nil + "activity tracking options" + :group 'jabber) + +(defcustom jabber-activity-make-string 'jabber-activity-make-string-default + "Function to call, for making the string to put in the mode +line. The default function returns the nick of the user." + :set #'(lambda (var val) + (custom-set-default var val) + (when (fboundp 'jabber-activity-make-name-alist) + (jabber-activity-make-name-alist) + (jabber-activity-mode-line-update))) + :type 'function + :group 'jabber-activity) + +(defcustom jabber-activity-shorten-minimum 1 + "All strings returned by `jabber-activity-make-strings-shorten' will be +at least this long, when possible." + :group 'jabber-activity + :type 'number) + +(defcustom jabber-activity-make-strings 'jabber-activity-make-strings-default + "Function which should return an alist of JID -> string when given a list of +JIDs." + :set #'(lambda (var val) + (custom-set-default var val) + (when (fboundp 'jabber-activity-make-name-alist) + (jabber-activity-make-name-alist) + (jabber-activity-mode-line-update))) + :type '(choice (function-item :tag "Keep strings" + :value jabber-activity-make-strings-default) + (function-item :tag "Shorten strings" + :value jabber-activity-make-strings-shorten) + (function :tag "Other function")) + :group 'jabber-activity) + +(defcustom jabber-activity-count-in-title nil + "If non-nil, display number of active JIDs in frame title." + :type 'boolean + :group 'jabber-activity + :set #'(lambda (var val) + (custom-set-default var val) + (when (bound-and-true-p jabber-activity-mode) + (jabber-activity-mode -1) + (jabber-activity-mode 1)))) + +(defcustom jabber-activity-count-in-title-format + '(jabber-activity-jids ("[" jabber-activity-count-string "] ")) + "Format string used for displaying activity in frame titles. +Same syntax as `mode-line-format'." + :type 'sexp + :group 'jabber-activity + :set #'(lambda (var val) + (if (not (bound-and-true-p jabber-activity-mode)) + (custom-set-default var val) + (jabber-activity-mode -1) + (custom-set-default var val) + (jabber-activity-mode 1)))) + +(defcustom jabber-activity-show-p 'jabber-activity-show-p-default + "Predicate function to call to check if the given JID should be +shown in the mode line or not." + :type 'function + :group 'jabber-activity) + +(defcustom jabber-activity-query-unread t + "Query the user as to whether killing Emacs should be cancelled when +there are unread messages which otherwise would be lost." + :type 'boolean + :group 'jabber-activity) + +(defface jabber-activity-face + '((t (:foreground "red" :weight bold))) + "The face for displaying jabber-activity-string in the mode line" + :group 'jabber-activity) + +(defvar jabber-activity-jids nil + "A list of JIDs which have caused activity") + +(defvar jabber-activity-name-alist nil + "Alist of mode line names for bare JIDs") + +(defvar jabber-activity-mode-string "" + "The mode string for jabber activity") + +(defvar jabber-activity-count-string "0" + "Number of active JIDs as a string.") + +(defvar jabber-activity-update-hook nil + "Hook called when `jabber-activity-jids' changes. +It is called after `jabber-activity-mode-string' and +`jabber-activity-count-string' are updated.") + +;; Protect this variable from being set in Local variables etc. +(put 'jabber-activity-mode-string 'risky-local-variable t) +(put 'jabber-activity-count-string 'risky-local-variable t) + +(defun jabber-activity-make-string-default (jid) + "Return the nick of the JID. If no nick is available, return +the user name part of the JID. In private MUC conversations, +return the user's nickname." + (if (jabber-muc-sender-p jid) + (jabber-jid-resource jid) + (let ((nick (jabber-jid-displayname jid)) + (user (jabber-jid-user jid)) + (username (jabber-jid-username jid))) + (if (and username (string= nick user)) + username + nick)))) + +(defun jabber-activity-make-strings-default (jids) + "Apply `jabber-activity-make-string' on JIDS" + (mapcar #'(lambda (jid) (cons jid (funcall jabber-activity-make-string jid))) + jids)) + +(defun jabber-activity-common-prefix (s1 s2) + "Return length of common prefix string shared by S1 and S2" + (let ((len (min (length s1) (length s2)))) + (or (dotimes (i len) + (when (not (eq (aref s1 i) (aref s2 i))) + (return i))) + ;; Substrings, equal, nil, or empty ("") + len))) + +(defun jabber-activity-make-strings-shorten (jids) + "Return an alist of JID -> names acquired by running +`jabber-activity-make-string' on JIDS, and then shortening the names +as much as possible such that all strings still are unique and at +least `jabber-activity-shorten-minimum' long." + (let ((alist + (sort (mapcar + #'(lambda (x) (cons x (funcall jabber-activity-make-string x))) + jids) + #'(lambda (x y) (string-lessp (cdr x) (cdr y)))))) + (loop for ((prev-jid . prev) (cur-jid . cur) (next-jid . next)) + on (cons nil alist) + until (null cur) + collect + (cons + cur-jid + (substring + cur + 0 (min (length cur) + (max jabber-activity-shorten-minimum + (1+ (jabber-activity-common-prefix cur prev)) + (1+ (jabber-activity-common-prefix cur next))))))))) + +(defun jabber-activity-find-buffer-name (jid) + "Find the name of the buffer that messages from JID would use." + (or (and (jabber-jid-resource jid) + (get-buffer (jabber-muc-private-get-buffer + (jabber-jid-user jid) + (jabber-jid-resource jid)))) + (get-buffer (jabber-chat-get-buffer jid)) + (get-buffer (jabber-muc-get-buffer jid)))) + +(defun jabber-activity-show-p-default (jid) + "Returns t only if there is an invisible buffer for JID" + (let ((buffer (jabber-activity-find-buffer-name jid))) + (and (buffer-live-p buffer) + (not (get-buffer-window buffer 'visible))))) + +(defun jabber-activity-make-name-alist () + "Rebuild `jabber-activity-name-alist' based on currently known JIDs" + (let ((jids (or (mapcar #'car jabber-activity-name-alist) + (mapcar #'symbol-name *jabber-roster*)))) + (setq jabber-activity-name-alist + (funcall jabber-activity-make-strings jids)))) + +(defun jabber-activity-lookup-name (jid) + "Lookup name in `jabber-activity-name-alist', creates an entry +if needed, and returns a (jid . string) pair suitable for the mode line" + (let ((elm (assoc jid jabber-activity-name-alist))) + (if elm + elm + (progn + ;; Remake alist with the new JID + (setq jabber-activity-name-alist + (funcall jabber-activity-make-strings + (cons jid (mapcar #'car jabber-activity-name-alist)))) + (jabber-activity-lookup-name jid))))) + +(defun jabber-activity-mode-line-update () + "Update the string shown in the mode line using `jabber-activity-make-string' +on JIDs where `jabber-activity-show-p'" + (setq jabber-activity-mode-string + (if jabber-activity-jids + (mapconcat + (lambda (x) + (let ((jump-to-jid (car x))) + (jabber-propertize + (cdr x) + 'face 'jabber-activity-face + 'local-map (make-mode-line-mouse-map + 'mouse-1 `(lambda () + (interactive) + (jabber-activity-switch-to + ,(car x)))) + 'help-echo (concat "Jump to " + (jabber-jid-displayname (car x)) + "'s buffer")))) + (mapcar #'jabber-activity-lookup-name + jabber-activity-jids) + ",") + "")) + (setq jabber-activity-count-string + (number-to-string (length jabber-activity-jids))) + (force-mode-line-update 'all) + (run-hooks 'jabber-activity-update-hook)) + +;;; Hooks + +(defun jabber-activity-clean () + "Remove JIDs where `jabber-activity-show-p' no longer is true" + (setq jabber-activity-jids (delete-if-not jabber-activity-show-p + jabber-activity-jids)) + (jabber-activity-mode-line-update)) + +(defun jabber-activity-add (from buffer text proposed-alert) + "Add a JID to mode line when `jabber-activity-show-p'" + ;; In case of private MUC message, we want to keep the full JID. + (let ((jid (if (jabber-muc-sender-p from) + from + (jabber-jid-user from)))) + (when (funcall jabber-activity-show-p jid) + (add-to-list 'jabber-activity-jids jid) + (jabber-activity-mode-line-update)))) + +(defun jabber-activity-add-muc (nick group buffer text proposed-alert) + "Add a JID to mode line when `jabber-activity-show-p'" + (when (funcall jabber-activity-show-p group) + (add-to-list 'jabber-activity-jids group) + (jabber-activity-mode-line-update))) + +(defun jabber-activity-kill-hook () + "Query the user as to whether killing Emacs should be cancelled +when there are unread messages which otherwise would be lost, if +`jabber-activity-query-unread' is t" + (if (and jabber-activity-jids + jabber-activity-query-unread) + (yes-or-no-p + "You have unread Jabber messages, are you sure you want to quit?") + t)) + +;;; Interactive functions + +(defun jabber-activity-switch-to (&optional jid-param) + "If JID-PARAM is provided, switch to that buffer. If JID-PARAM is nil and +there has been activity in another buffer, switch to that buffer. If no such +buffer exists, switch back to most recently used buffer." + (interactive) + (if (or jid-param jabber-activity-jids) + (let ((jid (or jid-param (car jabber-activity-jids)))) + (switch-to-buffer (jabber-activity-find-buffer-name jid)) + (jabber-activity-clean)) + ;; Switch back to the buffer used last + (switch-to-buffer nil))) + +;;;###autoload +(define-minor-mode jabber-activity-mode + "Toggle display of activity in hidden jabber buffers in the mode line. + +With a numeric arg, enable this display if arg is positive." + :global t + :group 'jabber-activity + :init-value t + (if jabber-activity-mode + (progn + ;; XEmacs compatibilty hack from erc-track + (if (featurep 'xemacs) + (defadvice switch-to-buffer (after jabber-activity-update (&rest args) activate) + (jabber-activity-clean)) + (add-hook 'window-configuration-change-hook + 'jabber-activity-clean)) + (add-hook 'jabber-message-hooks + 'jabber-activity-add) + (add-hook 'jabber-muc-hooks + 'jabber-activity-add-muc) + (add-hook 'jabber-post-connect-hook + 'jabber-activity-make-name-alist) + (add-to-list 'kill-emacs-query-functions + 'jabber-activity-kill-hook) + (add-to-list 'global-mode-string + '(t jabber-activity-mode-string)) + (when jabber-activity-count-in-title + ;; Be careful not to override specific meanings of the + ;; existing title format. In particular, if the car is + ;; a symbol, we can't just add our stuff at the beginning. + ;; If the car is "", we should be safe. + (if (equal (car frame-title-format) "") + (add-to-list 'frame-title-format + jabber-activity-count-in-title-format) + (setq frame-title-format (list "" + jabber-activity-count-in-title-format + frame-title-format))) + (if (equal (car icon-title-format) "") + (add-to-list 'icon-title-format + jabber-activity-count-in-title-format) + (setq icon-title-format (list "" + jabber-activity-count-in-title-format + icon-title-format))))) + (progn + (if (featurep 'xemacs) + (ad-disable-advice 'switch-to-buffer 'after 'jabber-activity-update) + (remove-hook 'window-configuration-change-hook + 'jabber-activity-remove-visible)) + (remove-hook 'jabber-message-hooks + 'jabber-activity-add) + (remove-hook 'jabber-muc-hooks + 'jabber-activity-add-muc) + (remove-hook 'jabber-post-connect-hook + 'jabber-activity-make-name-alist) + (setq global-mode-string (delete '(t jabber-activity-mode-string) + global-mode-string)) + (setq frame-title-format + (delete jabber-activity-count-in-title-format + frame-title-format)) + (setq icon-title-format + (delete jabber-activity-count-in-title-format + icon-title-format))))) + +;; XXX: define-minor-mode should probably do this for us, but it doesn't. +(if jabber-activity-mode (jabber-activity-mode 1)) + +(provide 'jabber-activity) + +;; arch-tag: 127D7E42-356B-11D9-BE1E-000A95C2FCD0 diff --git a/jabber-ahc-presence.el b/jabber-ahc-presence.el new file mode 100644 index 0000000..8239e45 --- /dev/null +++ b/jabber-ahc-presence.el @@ -0,0 +1,105 @@ +;; jabber-ahc-presence.el - provide remote control of presence + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-ahc) + +(defconst jabber-ahc-presence-node "presence" + "Node used by jabber-ahc-presence") + +(jabber-ahc-add jabber-ahc-presence-node "Set presence" 'jabber-ahc-presence + 'jabber-my-jid-p) + +(defun jabber-ahc-presence (xml-data) + "Process presence change command." + + (let* ((query (jabber-iq-query xml-data)) + (sessionid (jabber-xml-get-attribute query 'sessionid)) + (action (jabber-xml-get-attribute query 'action))) + ;; No session state is kept; instead, lack of session-id is used + ;; as indication of first command. + (cond + ;; command cancelled + ((string= action "cancel") + `(command ((xmlns . "http://jabber.org/protocol/commands") + (sessionid . ,sessionid) + (node . ,jabber-ahc-presence-node) + (status . "canceled")))) + ;; return form + ((null sessionid) + `(command ((xmlns . "http://jabber.org/protocol/commands") + (sessionid . "jabber-ahc-presence") + (node . ,jabber-ahc-presence-node) + (status . "executing")) + (x ((xmlns . "jabber:x:data") + (type . "form")) + (title nil ,(format "Set presence of %s@%s/%s" jabber-username jabber-server jabber-resource)) + (instructions nil "Select new presence status.") + (field ((var . "show") + (label . "Show") + (type . "list-single")) + (value nil ,(if (string= *jabber-current-show* "") + "online" + *jabber-current-show*)) + (option ((label . "Online")) (value nil "online")) + (option ((label . "Chatty")) (value nil "chat")) + (option ((label . "Away")) (value nil "away")) + (option ((label . "Extended away")) (value nil "xa")) + (option ((label . "Do not disturb")) (value nil "dnd"))) + (field ((var . "status") + (label . "Status text") + (type . "text-single")) + (value nil ,*jabber-current-status*)) + (field ((var . "priority") + (label . "Priority") + (type . "text-single")) + (value nil ,(int-to-string *jabber-current-priority*)))))) + ;; process form + (t + (let* ((x (car (jabber-xml-get-children query 'x))) + ;; we assume that the first <x/> is the jabber:x:data one + (fields (jabber-xml-get-children x 'field)) + (new-show *jabber-current-show*) + (new-status *jabber-current-status*) + (new-priority *jabber-current-priority*)) + (dolist (field fields) + (let ((var (jabber-xml-get-attribute field 'var)) + ;; notice that multi-value fields won't be handled properly + ;; by this + (value (car (jabber-xml-node-children (car (jabber-xml-get-children field 'value)))))) + (cond + ((string= var "show") + (setq new-show (if (string= value "online") + "" + value))) + ((string= var "status") + (setq new-status value)) + ((string= var "priority") + (setq new-priority (string-to-int value)))))) + (jabber-send-presence new-show new-status new-priority)) + `(command ((xmlns . "http://jabber.org/protocol/commands") + (sessionid . ,sessionid) + (node . ,jabber-ahc-presence-node) + (status . "completed")) + (note ((type . "info")) "Presence has been changed.")))))) + +(provide 'jabber-ahc-presence) + +;;; arch-tag: 4b8cbbe7-00a9-4d42-a4ac-b824ab914fba diff --git a/jabber-ahc.el b/jabber-ahc.el new file mode 100644 index 0000000..b2501d2 --- /dev/null +++ b/jabber-ahc.el @@ -0,0 +1,226 @@ +;; jabber-ahc.el - Ad-Hoc Commands by JEP-0050 + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-disco) +(require 'jabber-widget) + +(defvar jabber-ahc-sessionid nil + "session id of Ad-Hoc Command session") + +(defvar jabber-ahc-node nil + "node to send commands to") + +(defvar jabber-ahc-commands nil + "Commands provided + +This is an alist, where the keys are node names as strings (which +means that they must not conflict). The values are plists having +following properties: + +acl - function taking JID as single argument, return non-nil for + access allowed. No function means open for everyone. +name - name of command +func - function receiving entire IQ stanza as single argument + and returning a <command/> node + +Use the function `jabber-ahc-add' to add a command to this list.") + + +;;; SERVER +(add-to-list 'jabber-disco-info-nodes + (list "http://jabber.org/protocol/commands" + '((identity ((category . "automation") + (type . "command-list") + (name . "Ad-Hoc Command list"))) + (feature ((var . "http://jabber.org/protocol/commands"))) + (feature ((var . "http://jabber.org/protocol/disco#items"))) + (feature + ((var . "http://jabber.org/protocol/disco#info")))))) + +(defun jabber-ahc-add (node name func acl) + "Add a command to internal lists. +NODE is the node name to be used. It must be unique. +NAME is the natural-language name of the command. +FUNC is a function taking the entire IQ stanza as single argument when +this command is invoked, and returns a <command/> node. +ACL is a function taking JID as single argument, returning non-nil for +access allowed. nil means open for everyone." + (add-to-list 'jabber-ahc-commands (cons node (list 'name name + 'func func + 'acl acl))) + (add-to-list 'jabber-disco-info-nodes + (list node `((identity ((category . "automation") + (type . "command-node") + (name . ,name))) + (feature ((var . "http://jabber.org/protocol/commands"))) + (feature ((var . "http://jabber.org/protocol/disco#info"))) + (feature ((var . "jabber:x:data"))))))) + +(add-to-list 'jabber-advertised-features "http://jabber.org/protocol/commands") +(add-to-list 'jabber-disco-items-nodes + (list "http://jabber.org/protocol/commands" #'jabber-ahc-disco-items nil)) +(defun jabber-ahc-disco-items (xml-data) + "Return commands in response to disco#items request" + (let ((jid (jabber-xml-get-attribute xml-data 'from))) + (mapcar (function + (lambda (command) + (let ((node (car command)) + (plist (cdr command))) + (let ((acl (plist-get plist 'acl)) + (name (plist-get plist 'name)) + (func (plist-get plist 'func))) + (when (or (not (functionp acl)) + (funcall acl jid)) + `(item ((name . ,name) + (jid . ,(format "%s@%s/%s" jabber-username jabber-server jabber-resource)) + (node . ,node)))))))) + jabber-ahc-commands))) + +(add-to-list 'jabber-iq-set-xmlns-alist + (cons "http://jabber.org/protocol/commands" 'jabber-ahc-process)) +(defun jabber-ahc-process (xml-data) + + (let ((to (jabber-xml-get-attribute xml-data 'from)) + (id (jabber-xml-get-attribute xml-data 'id)) + (node (jabber-xml-get-attribute (jabber-iq-query xml-data) 'node))) + ;; find command + (let* ((plist (cdr (assoc node jabber-ahc-commands))) + (acl (plist-get plist 'acl)) + (func (plist-get plist 'func))) + (if plist + ;; found + (if (or (not (functionp acl)) + (funcall acl to)) + ;; access control passed + (jabber-send-iq to "result" + (funcall func xml-data) + nil nil nil nil id) + ;; ...or failed + (jabber-signal-error "cancel" 'not-allowed)) + ;; No such node + (jabber-signal-error "cancel" 'item-not-found))))) + +;;; CLIENT +(add-to-list 'jabber-jid-service-menu + (cons "Request command list" 'jabber-ahc-get-list)) +(defun jabber-ahc-get-list (to) + "Request list of ad-hoc commands. (JEP-0050)" + (interactive (list (jabber-read-jid-completing "Request command list from: "))) + (jabber-get-disco-items to "http://jabber.org/protocol/commands")) + +(add-to-list 'jabber-jid-service-menu + (cons "Execute command" 'jabber-ahc-execute-command)) +(defun jabber-ahc-execute-command (to node) + "Execute ad-hoc command. (JEP-0050)" + (interactive (list (jabber-read-jid-completing "Execute command of: ") + (jabber-read-node "Node of command: "))) + (jabber-send-iq to + "set" + `(command ((xmlns . "http://jabber.org/protocol/commands") + (node . ,node) + (action . "execute"))) + #'jabber-process-data #'jabber-ahc-display + #'jabber-process-data "Command execution failed")) + +(defun jabber-ahc-display (xml-data) + (let* ((from (jabber-xml-get-attribute xml-data 'from)) + (query (jabber-iq-query xml-data)) + (node (jabber-xml-get-attribute query 'node)) + (notes (jabber-xml-get-children query 'note)) + (sessionid (jabber-xml-get-attribute query 'sessionid)) + (status (jabber-xml-get-attribute query 'status)) + (actions (car (jabber-xml-get-children query 'actions))) + xdata + (inhibit-read-only t)) + + (make-local-variable 'jabber-ahc-sessionid) + (setq jabber-ahc-sessionid sessionid) + (make-local-variable 'jabber-ahc-node) + (setq jabber-ahc-node node) + + (dolist (x (jabber-xml-get-children query 'x)) + (when (string= (jabber-xml-get-attribute x 'xmlns) "jabber:x:data") + (setq xdata x))) + + (cond + ((string= status "executing") + (insert "Executing command\n\n")) + ((string= status "completed") + (insert "Command completed\n\n")) + ((string= status "canceled") + (insert "Command canceled\n\n"))) + + (dolist (note notes) + (let ((note-type (jabber-xml-get-attribute note 'type))) + (cond + ((string= note-type "warn") + (insert "Warning: ")) + ((string= note-type "error") + (insert "Error: "))) + (insert (car (jabber-xml-node-children note)) "\n"))) + (insert "\n") + + (when xdata + (jabber-init-widget-buffer from) + + (let ((formtype (jabber-xml-get-attribute xdata 'type))) + (if (string= formtype "result") + (jabber-render-xdata-search-results xdata) + (jabber-render-xdata-form xdata) + + (when (string= status "executing") + (let ((button-titles + (cond + ((null actions) + '(complete cancel)) + (t + (let ((children (mapcar #'jabber-xml-node-name (jabber-xml-node-children actions))) + (default-action (jabber-xml-get-attribute actions 'execute))) + (if (or (null default-action) (memq (intern default-action) children)) + children + (cons (intern default-action) children))))))) + (dolist (button-title button-titles) + (widget-create 'push-button :notify `(lambda (&rest ignore) (jabber-ahc-submit (quote ,button-title))) (symbol-name button-title)) + (widget-insert "\t"))) + (widget-insert "\n")))) + + (widget-setup) + (widget-minor-mode 1)))) + +(defun jabber-ahc-submit (action) + "Submit Ad-Hoc Command." + + (jabber-send-iq jabber-submit-to + "set" + `(command ((xmlns . "http://jabber.org/protocol/commands") + (sessionid . ,jabber-ahc-sessionid) + (node . ,jabber-ahc-node) + (action . ,(symbol-name action))) + ,(if (and (not (eq action 'cancel)) + (eq jabber-form-type 'xdata)) + (jabber-parse-xdata-form))) + + #'jabber-process-data #'jabber-ahc-display + #'jabber-process-data "Command execution failed")) + +(provide 'jabber-ahc) + +;;; arch-tag: c0d5ed8c-50cb-44e1-8e0f-4058b79ee353 diff --git a/jabber-alert.el b/jabber-alert.el new file mode 100644 index 0000000..5709e14 --- /dev/null +++ b/jabber-alert.el @@ -0,0 +1,424 @@ +;; jabber-alert.el - alert hooks + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004, 2005 - Magnus Henoch - mange@freemail.hu + +;; 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-util) + +(require 'cl) + +(defgroup jabber-alerts nil "auditory and visual alerts for jabber events" + :group 'jabber) + +(defcustom jabber-alert-message-hooks '(jabber-message-echo + jabber-message-scroll) + "Hooks run when a new message arrives. + +Arguments are FROM, BUFFER, TEXT and PROPOSED-ALERT. FROM is the JID +of the sender, BUFFER is the the buffer where the message can be read, +and TEXT is the text of the message. PROPOSED-ALERT is the string +returned by `jabber-alert-message-function' for these arguments, so that +hooks do not have to call it themselves. + +This hook is meant for user customization of message alerts. For +other uses, see `jabber-message-hooks'." + :type 'hook + :options '(jabber-message-beep + jabber-message-wave + jabber-message-echo + jabber-message-switch + jabber-message-display + jabber-message-scroll) + :group 'jabber-alerts) + +(defvar jabber-message-hooks nil + "Internal hooks run when a new message arrives. + +This hook works just like `jabber-alert-message-hooks', except that +it's not meant to be customized by the user.") + +(defcustom jabber-alert-message-function + 'jabber-message-default-message + "Function for constructing message alert messages. + +Arguments are FROM, BUFFER, and TEXT. This function should return a +string containing an appropriate text message, or nil if no message +should be displayed. + +The provided hooks displaying a text message get it from this function, +and show no message if it returns nil. Other hooks do what they do +every time." + :type 'function + :group 'jabber-alerts) + +(defcustom jabber-alert-muc-hooks '(jabber-muc-echo jabber-muc-scroll) + "Hooks run when a new MUC message arrives. + +Arguments are NICK, GROUP, BUFFER, TEXT and PROPOSED-ALERT. NICK +is the nickname of the sender. GROUP is the JID of the group. +BUFFER is the the buffer where the message can be read, and TEXT +is the text of the message. PROPOSED-ALERT is the string +returned by `jabber-alert-muc-function' for these arguments, +so that hooks do not have to call it themselves." + :type 'hook + :options '(jabber-muc-beep + jabber-muc-wave + jabber-muc-echo + jabber-muc-switch + jabber-muc-display + jabber-muc-scroll) + :group 'jabber-alerts) + +(defvar jabber-muc-hooks '() + "Internal hooks run when a new MUC message arrives. + +This hook works just like `jabber-alert-muc-hooks', except that +it's not meant to be customized by the user.") + +(defcustom jabber-alert-muc-function + 'jabber-muc-default-message + "Function for constructing message alert messages. + +Arguments are NICK, GROUP, BUFFER, and TEXT. This function +should return a string containing an appropriate text message, or +nil if no message should be displayed. + +The provided hooks displaying a text message get it from this function, +and show no message if it returns nil. Other hooks do what they do +every time." + :type 'function + :group 'jabber-alerts) + +(defcustom jabber-alert-presence-hooks + '(jabber-presence-update-roster + jabber-presence-echo) + "Hooks run when a user's presence changes. + +Arguments are WHO, OLDSTATUS, NEWSTATUS, STATUSTEXT and +PROPOSED-ALERT. WHO is a symbol whose text is the JID of the contact, +and which has various interesting properties. OLDSTATUS is the old +presence or nil if disconnected. NEWSTATUS is the new presence, or +one of \"subscribe\", \"unsubscribe\", \"subscribed\" and +\"unsubscribed\". PROPOSED-ALERT is the string returned by +`jabber-alert-presence-message-function' for these arguments." + :type 'hook + :options '(jabber-presence-beep + jabber-presence-wave + jabber-presence-update-roster + jabber-presence-switch + jabber-presence-display + jabber-presence-echo) + :group 'jabber-alerts) + +(defvar jabber-presence-hooks '(jabber-presence-watch) + "Internal hooks run when a user's presence changes. + +This hook works just like `jabber-alert-presence-hooks', except that +it's not meant to be customized by the user.") + +(defcustom jabber-alert-presence-message-function + 'jabber-presence-default-message + "Function for constructing presence alert messages. + +Arguments are WHO, OLDSTATUS, NEWSTATUS and STATUSTEXT. See +`jabber-alert-presence-hooks' for documentation. This function +should return a string containing an appropriate text message, or nil +if no message should be displayed. + +The provided hooks displaying a text message get it from this function. +All hooks refrain from action if this function returns nil." + :type 'function + :group 'jabber-alerts) + +(defcustom jabber-alert-info-message-hooks '(jabber-info-display jabber-info-echo) + "Hooks run when an info request is completed. + +First argument is WHAT, a symbol telling the kind of info request completed. +That might be 'roster, for requested roster updates, and 'browse, for +browse requests. Second argument in BUFFER, a buffer containing the result. +Third argument is PROPOSED-ALERT, containing the string returned by +`jabber-alert-info-message-function' for these arguments." + :type 'hook + :options '(jabber-info-beep + jabber-info-wave + jabber-info-echo + jabber-info-switch + jabber-info-display) + :group 'jabber-alerts) + +(defvar jabber-info-message-hooks '() + "Internal hooks run when an info request is completed. + +This hook works just like `jabber-alert-info-message-hooks', +except that it's not meant to be customized by the user.") + +(defcustom jabber-alert-info-message-function + 'jabber-info-default-message + "Function for constructing info alert messages. + +Arguments are WHAT, a symbol telling the kind of info request completed, +and BUFFER, a buffer containing the result." + :type 'function + :group 'jabber-alerts) + +(defcustom jabber-info-message-alist + '((roster . "Roster display updated") + (browse . "Browse request completed")) + "Alist for info alert messages, used by `jabber-info-default-message'." + :type '(alist :key-type symbol :value-type string + :options (roster browse)) + :group 'jabber-alerts) + +(defcustom jabber-alert-message-wave "" + "a sound file to play when a message arrived" + :type 'file + :group 'jabber-alerts) + +(defcustom jabber-alert-muc-wave "" + "a sound file to play when a MUC message arrived" + :type 'file + :group 'jabber-alerts) + +(defcustom jabber-alert-presence-wave "" + "a sound file to play when a presence arrived" + :type 'file + :group 'jabber-alerts) + +(defcustom jabber-alert-info-wave "" + "a sound file to play when an info query result arrived" + :type 'file + :group 'jabber-alerts) + +(defmacro define-jabber-alert (name docstring function) + "Define a new family of external alert hooks. +Use this macro when your hooks do nothing except displaying a string +in some new innovative way. You write a string display function, and +this macro does all the boring and repetitive work. + +NAME is the name of the alert family. The resulting hooks will be +called jabber-{message,muc,presence,info}-NAME. +DOCSTRING is the docstring to use for those hooks. +FUNCTION is a function that takes one argument, a string, +and displays it in some meaningful way. It can be either a +lambda form or a quoted function name. +The created functions are inserted as options in Customize. + +Examples: +\(define-jabber-alert foo \"Send foo alert\" 'foo-message) +\(define-jabber-alert bar \"Send bar alert\" + (lambda (msg) (bar msg 42)))" + (let ((sn (symbol-name name))) + (let ((msg (intern (format "jabber-message-%s" sn))) + (muc (intern (format "jabber-muc-%s" sn))) + (pres (intern (format "jabber-presence-%s" sn))) + (info (intern (format "jabber-info-%s" sn)))) + `(progn + (defun ,msg (from buffer text proposed-alert) + ,docstring + (when proposed-alert + (funcall ,function proposed-alert))) + (pushnew (quote ,msg) (get 'jabber-alert-message-hooks 'custom-options)) + (defun ,muc (nick group buffer text proposed-alert) + ,docstring + (when proposed-alert + (funcall ,function proposed-alert))) + (pushnew (quote ,muc) (get 'jabber-alert-muc-hooks 'custom-options)) + (defun ,pres (who oldstatus newstatus statustext proposed-alert) + ,docstring + (when proposed-alert + (funcall ,function proposed-alert))) + (pushnew (quote ,pres) (get 'jabber-alert-presence-hooks 'custom-options)) + (defun ,info (infotype buffer proposed-alert) + ,docstring + (when proposed-alert + (funcall ,function proposed-alert))) + (pushnew (quote ,info) (get 'jabber-alert-info-message-hooks 'custom-options)))))) + +;; Alert hooks +(define-jabber-alert echo "Show a message in the echo area" + (lambda (msg) (message "%s" msg))) +(define-jabber-alert beep "Beep on event" + (lambda (&rest ignore) (beep))) + +;; External notifiers +(require 'jabber-screen) +(require 'jabber-ratpoison) +(require 'jabber-sawfish) +(require 'jabber-festival) +(require 'jabber-xmessage) + +;; Message alert hooks +(defun jabber-message-default-message (from buffer text) + (when (or jabber-message-alert-same-buffer + (not (memq (selected-window) (get-buffer-window-list buffer)))) + (if (jabber-muc-sender-p from) + (format "Private message from %s in %s" + (jabber-jid-resource from) + (jabber-jid-displayname (jabber-jid-user from))) + (format "Message from %s" (jabber-jid-displayname from))))) + +(defcustom jabber-message-alert-same-buffer t + "If nil, don't display message alerts for the current buffer." + :type 'boolean + :group 'jabber-alerts) + +(defun jabber-message-wave (from buffer text proposed-alert) + "Play the wave file specified in `jabber-alert-message-wave'" + (when proposed-alert + (jabber-play-sound-file jabber-alert-message-wave))) + +(defun jabber-message-display (from buffer text proposed-alert) + "Display the buffer where a new message has arrived." + (when proposed-alert + (display-buffer buffer))) + +(defun jabber-message-switch (from buffer text proposed-alert) + "Switch to the buffer where a new message has arrived." + (when proposed-alert + (switch-to-buffer buffer))) + +(defun jabber-message-scroll (from buffer text proposed-alert) + "Scroll all nonselected windows where the chat buffer is displayed." + ;; jabber-chat-buffer-display will DTRT with point in the buffer. + ;; But this change will not take effect in nonselected windows. + ;; Therefore we do that manually here. + ;; + ;; There are three cases: + ;; 1. The user started typing a message in this window. Point is + ;; greater than jabber-point-insert. In that case, we don't + ;; want to move point. + ;; 2. Point was at the end of the buffer, but no message was being + ;; typed. After displaying the message, point is now close to + ;; the end of the buffer. We advance it to the end. + ;; 3. The user was perusing history in this window. There is no + ;; simple way to distinguish this from 2, so the user loses. + (let ((windows (get-buffer-window-list buffer nil t)) + (new-point-max (with-current-buffer buffer (point-max)))) + (dolist (w windows) + (unless (eq w (selected-window)) + (set-window-point w new-point-max))))) + +;; MUC alert hooks +(defun jabber-muc-default-message (nick group buffer text) + (when (or jabber-message-alert-same-buffer + (not (memq (selected-window) (get-buffer-window-list buffer)))) + (if nick + (format "Message from %s in %s" nick (jabber-jid-displayname + group)) + (format "Message in %s" (jabber-jid-displayname group))))) + +(defun jabber-muc-wave (nick group buffer text proposed-alert) + "Play the wave file specified in `jabber-alert-muc-wave'" + (when proposed-alert + (jabber-play-sound-file jabber-alert-muc-wave))) + +(defun jabber-muc-display (nick group buffer text proposed-alert) + "Display the buffer where a new message has arrived." + (when proposed-alert + (display-buffer buffer))) + +(defun jabber-muc-switch (nick group buffer text proposed-alert) + "Switch to the buffer where a new message has arrived." + (when proposed-alert + (switch-to-buffer buffer))) + +(defun jabber-muc-scroll (nick group buffer text proposed-alert) + "Scroll buffer even if it is in an unselected window." + (jabber-message-scroll nil buffer nil nil)) + +;; Presence alert hooks +(defun jabber-presence-default-message (who oldstatus newstatus statustext) + "This function returns nil if OLDSTATUS and NEWSTATUS are equal, and in other +cases a string of the form \"'name' (jid) is now NEWSTATUS (STATUSTEXT)\". + +This function is not called directly, but is the default for +`jabber-alert-presence-message-function'." + (cond + ((equal oldstatus newstatus) + nil) + (t + (let ((formattedname + (if (> (length (get who 'name)) 0) + (get who 'name) + (symbol-name who))) + (formattedstatus + (or + (cdr (assoc newstatus + '(("subscribe" . " requests subscription to your presence") + ("subscribed" . " has granted presence subscription to you") + ("unsubscribe" . " no longer subscribes to your presence") + ("unsubscribed" . " cancels your presence subscription")))) + (concat " is now " + (or + (cdr (assoc newstatus jabber-presence-strings)) + newstatus)))) + (formattedtext + (if (> (length statustext) 0) + (concat " (" (jabber-unescape-xml statustext) ")") + ""))) + (concat formattedname formattedstatus formattedtext))))) + +(defun jabber-presence-wave (who oldstatus newstatus statustext proposed-alert) + "Play the wave file specified in `jabber-alert-presence-wave'" + (if proposed-alert + (jabber-play-sound-file jabber-alert-presence-wave))) + +;; This is now defined in jabber-roster.el. +;; (defun jabber-presence-update-roster (who oldstatus newstatus statustext proposed-alert) +;; "Update the roster display by calling `jabber-display-roster'" +;; (jabber-display-roster)) + +(defun jabber-presence-display (who oldstatus newstatus statustext proposed-alert) + "Display the roster buffer" + (when proposed-alert + (display-buffer jabber-roster-buffer))) + +(defun jabber-presence-switch (who oldstatus newstatus statustext proposed-alert) + "Switch to the roster buffer" + (when proposed-alert + (switch-to-buffer jabber-roster-buffer))) + +;;; Info alert hooks + +(defun jabber-info-default-message (infotype buffer) + "Function for constructing info alert messages. + +The argument is INFOTYPE, a symbol telling the kind of info request completed. +This function uses `jabber-info-message-alist' to find a message." + (concat (cdr (assq infotype jabber-info-message-alist)) + " (buffer "(buffer-name buffer) ")")) + +(defun jabber-info-wave (infotype buffer proposed-alert) + "Play the wave file specified in `jabber-alert-info-wave'" + (if proposed-alert + (jabber-play-sound-file jabber-alert-info-wave))) + +(defun jabber-info-display (infotype buffer proposed-alert) + "Display buffer of completed request" + (when proposed-alert + (display-buffer buffer))) + +(defun jabber-info-switch (infotype buffer proposed-alert) + "Switch to buffer of completed request" + (when proposed-alert + (switch-to-buffer buffer))) + +(provide 'jabber-alert) + +;;; arch-tag: 725bd73e-c613-4fdc-a11d-3392a7598d4f diff --git a/jabber-browse.el b/jabber-browse.el new file mode 100644 index 0000000..b391a62 --- /dev/null +++ b/jabber-browse.el @@ -0,0 +1,98 @@ +;; jabber-browse.el - jabber browsing by JEP-0011 + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-iq) +(require 'jabber-xml) +(require 'jabber-util) + +;; jabber.el can perform browse requests, but will not answer them. + +(add-to-list 'jabber-jid-info-menu + (cons "Send browse query" 'jabber-get-browse)) +(defun jabber-get-browse (to) + "send a browse infoquery request to someone" + (interactive (list (jabber-read-jid-completing "browse: "))) + (jabber-send-iq to + "get" + '(query ((xmlns . "jabber:iq:browse"))) + #'jabber-process-data #'jabber-process-browse + #'jabber-process-data "Browse failed")) + +;; called from jabber-process-data +(defun jabber-process-browse (xml-data) + "Handle results from jabber:iq:browse requests." + (dolist (item (jabber-xml-node-children xml-data)) + (when (and (listp item) + (not (eq (jabber-xml-node-name item) 'ns))) + (let ((jid (jabber-xml-get-attribute item 'jid)) + (beginning (point))) + (cond + ((or + (eq (jabber-xml-node-name item) 'user) + (string= (jabber-xml-get-attribute item 'category) "user")) + (insert (jabber-propertize "$ USER" + 'face 'jabber-title-medium) + "\n\n")) + ((or + (eq (jabber-xml-node-name item) 'service) + (string= (jabber-xml-get-attribute item 'category) "service")) + (insert (jabber-propertize "* SERVICE" + 'face 'jabber-title-medium) + "\n\n")) + ((or + (eq (jabber-xml-node-name item) 'conference) + (string= (jabber-xml-get-attribute item 'category) "conference")) + (insert (jabber-propertize "@ CONFERENCE" + 'face 'jabber-title-medium) + "\n\n")) + (t + ;; So far I've seen "server" and "directory", both in the node-name. + ;; Those are actually service disco categories, but jabberd 2 seems + ;; to use them for browse results as well. It's not right (as in + ;; JEP-0011), but it's reasonable. + (let ((category (jabber-xml-get-attribute item 'category))) + (if (= (length category) 0) + (setq category (jabber-xml-node-name item))) + (insert (jabber-propertize (format "! OTHER: %s" category) + 'face 'jabber-title-medium) + "\n\n")))) + (dolist (attr '((type . "Type:\t\t") + (jid . "JID:\t\t") + (name . "Name:\t\t") + (version . "Version:\t"))) + (let ((data (jabber-xml-get-attribute item (car attr)))) + (if (> (length data) 0) + (insert (cdr attr) (jabber-unescape-xml data) "\n")))) + + (dolist (ns (jabber-xml-get-children item 'ns)) + (if (stringp (car (jabber-xml-node-children ns))) + (insert "Namespace:\t" (car (jabber-xml-node-children ns)) "\n"))) + + (insert "\n") + (put-text-property beginning (point) 'jabber-jid jid) + + ;; XXX: Is this kind of recursion really needed? + (if (listp (car (jabber-xml-node-children item))) + (jabber-process-browse item)))))) + +(provide 'jabber-browse) + +;;; arch-tag: be01ab34-96eb-4fcb-aa35-a0d3e6c446c3 diff --git a/jabber-chat.el b/jabber-chat.el new file mode 100644 index 0000000..67652ae --- /dev/null +++ b/jabber-chat.el @@ -0,0 +1,477 @@ +;; jabber-chat.el - one-to-one chats + +;; Copyright (C) 2005 - Magnus Henoch - mange@freemail.hu + +;; 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-chatbuffer) +(require 'jabber-history) + +(defgroup jabber-chat nil "chat display options" + :group 'jabber) + +(defcustom jabber-chat-buffer-format "*-jabber-chat-%n-*" + "The format specification for the name of chat buffers. + +These fields are available (all are about the person you are chatting +with): + +%n Nickname, or JID if no nickname set +%j Bare JID (without resource) +%r Resource" + :type 'string + :group 'jabber-chat) + +(defcustom jabber-chat-header-line-format + '(" " (:eval (jabber-jid-displayname jabber-chatting-with)) + "\t" (:eval (let ((buddy (jabber-jid-symbol jabber-chatting-with))) + (propertize + (or + (cdr (assoc (get buddy 'show) jabber-presence-strings)) + (get buddy 'show)) + 'face + (or (cdr (assoc (get buddy 'show) jabber-presence-faces)) + 'jabber-roster-user-online)))) + "\t" (:eval (get (jabber-jid-symbol jabber-chatting-with) 'status)) + "\t" jabber-events-message) ;see jabber-events.el + "The specification for the header line of chat buffers. + +The format is that of `mode-line-format' and `header-line-format'." + :type 'sexp + :group 'jabber-chat) + +(defcustom jabber-chat-time-format "%H:%M" + "The format specification for instant messages in the chat buffer. +See also `jabber-chat-delayed-time-format'. + +See `format-time-string' for valid values." + :type 'string + :group 'jabber-chat) + +(defcustom jabber-chat-delayed-time-format "%Y-%m-%d %H:%M" + "The format specification for delayed messages in the chat buffer. +See also `jabber-chat-time-format'. + +See `format-time-string' for valid values." + :type 'string + :group 'jabber-chat) + +(defcustom jabber-print-rare-time t + "Non-nil means to print \"rare time\" indications in chat buffers. +The default settings tell every new hour." + :type 'boolean + :group 'jabber-chat) + +(defcustom jabber-rare-time-format "%a %e %b %Y %H:00" + "The format specification for the rare time information. +Rare time information will be printed whenever the current time, +formatted according to this string, is different to the last +rare time printed." + :type 'string + :group 'jabber-chat) + +(defface jabber-rare-time-face + '((t (:foreground "darkgreen" :underline t))) + "face for displaying the rare time info" + :group 'jabber-chat) + +(defvar jabber-rare-time "" + "Latest rare time printed") +(make-variable-buffer-local 'jabber-rare-time) + +(defcustom jabber-chat-local-prompt-format "[%t] %n> " + "The format specification for lines you type in the chat buffer. + +These fields are available: + +%t Time, formatted according to `jabber-chat-time-format' + or `jabber-chat-delayed-time-format' +%n Nickname (`jabber-nickname') +%u Username +%r Resource +%j Bare JID (without resource)" + :type 'string + :group 'jabber-chat) + +(defcustom jabber-chat-foreign-prompt-format "[%t] %n> " + "The format specification for lines others type in the chat buffer. + +These fields are available: + +%t Time, formatted according to `jabber-chat-time-format' + or `jabber-chat-delayed-time-format' +%n Nickname, or JID if no nickname set +%u Username +%r Resource +%j Bare JID (without resource)" + :type 'string + :group 'jabber-chat) + +(defcustom jabber-chat-system-prompt-format "[%t] *** " + "The format specification for lines from the system or that are special in the chat buffer." + :type 'string + :group 'jabber-chat) + +(defface jabber-chat-prompt-local + '((t (:foreground "blue" :weight bold))) + "face for displaying the chat prompt for what you type in" + :group 'jabber-chat) + +(defface jabber-chat-prompt-foreign + '((t (:foreground "red" :weight bold))) + "face for displaying the chat prompt for what they send" + :group 'jabber-chat) + +(defface jabber-chat-prompt-system + '((t (:foreground "green" :weight bold))) + "face used for system and special messages" + :group 'jabber-chat) + +(defface jabber-chat-text-local nil + "Face used for text you write" + :group 'jabber-chat) + +(defface jabber-chat-text-foreign nil + "Face used for text others write" + :group 'jabber-chat) + +(defface jabber-chat-error + '((t (:foreground "red" :weight bold))) + "Face used for error messages" + :group 'jabber-chat) + +(defvar jabber-chatting-with nil + "JID of the person you are chatting with") + +(defvar jabber-chat-printers '(jabber-chat-print-subject + jabber-chat-print-body + jabber-chat-print-url + jabber-chat-goto-address) + "List of functions that may be able to print part of a message. +Each function receives the entire <message/> stanza as argument.") + +(defvar jabber-body-printers '(jabber-chat-normal-body) + "List of functions that may be able to print a body for a message. +Each function receives the entire <message/> stanza as argument, and +should either output a representation of the body part of the message +and return non-nil, or output nothing and return nil. These functions +are called in order, until one of them returns non-nil. + +Add a function to the beginning of this list if the tag it handles +replaces the contents of the <body/> tag.") + +(defvar jabber-chat-send-hooks nil + "List of functions called when a chat message is sent. +The arguments are the text to send, and the id attribute of the +message. + +The functions should return a list of XML nodes they want to be +added to the outgoing message.") + +(defvar jabber-chat-earliest-backlog nil + "Float-time of earliest backlog entry inserted into buffer. +nil if no backlog has been inserted.") + +(defun jabber-chat-get-buffer (chat-with) + "Return the chat buffer for chatting with CHAT-WITH (bare or full JID). +Either a string or a buffer is returned, so use `get-buffer' or +`get-buffer-create'." + (format-spec jabber-chat-buffer-format + (list + (cons ?n (jabber-jid-displayname chat-with)) + (cons ?j (jabber-jid-user chat-with)) + (cons ?r (jabber-jid-resource chat-with))))) + +(defun jabber-chat-create-buffer (chat-with) + "Prepare a buffer for chatting with CHAT-WITH. +This function is idempotent." + (with-current-buffer (get-buffer-create (jabber-chat-get-buffer chat-with)) + (if (not (eq major-mode 'jabber-chat-mode)) (jabber-chat-mode)) + (make-local-variable 'jabber-chatting-with) + (setq jabber-chatting-with chat-with) + (setq jabber-send-function 'jabber-chat-send) + (setq header-line-format jabber-chat-header-line-format) + + (make-local-variable 'jabber-chat-earliest-backlog) + + ;; insert backlog + (when (zerop (buffer-size)) + (let ((backlog-entries (jabber-history-backlog chat-with))) + (when backlog-entries + (setq jabber-chat-earliest-backlog + (jabber-float-time (jabber-parse-time + (aref (car backlog-entries) 0)))) + (mapc 'jabber-chat-insert-backlog-entry backlog-entries)))) + + (current-buffer))) + +(defun jabber-chat-insert-backlog-entry (msg) + "Insert backlog entry MSG at point." + (if (string= (aref msg 1) "in") + (let ((fake-stanza `(message ((from . ,(aref msg 2))) + (body nil ,(aref msg 4)) + (x ((xmlns . "jabber:x:delay") + (stamp . ,(jabber-encode-legacy-time (jabber-parse-time (aref msg 0))))))))) + (jabber-chat-buffer-display-at-point 'jabber-chat-print-prompt + fake-stanza + jabber-chat-printers + fake-stanza)) + (jabber-chat-buffer-display-at-point 'jabber-chat-self-prompt + (jabber-parse-time (aref msg 0)) + '(insert) + (jabber-propertize + (aref msg 4) + 'face 'jabber-chat-text-local)))) + +(add-to-list 'jabber-jid-chat-menu + (cons "Display more context" 'jabber-chat-display-more-backlog)) + +(defun jabber-chat-display-more-backlog (how-many) + (interactive "nHow many more messages? ") + (let* ((inhibit-read-only t) + (jabber-backlog-days nil) + (jabber-backlog-number how-many) + (backlog-entries (jabber-history-backlog + jabber-chatting-with jabber-chat-earliest-backlog))) + (when backlog-entries + (setq jabber-chat-earliest-backlog + (jabber-float-time (jabber-parse-time + (aref (car backlog-entries) 0)))) + (save-excursion + (goto-char (point-min)) + (mapc 'jabber-chat-insert-backlog-entry backlog-entries))))) + +(add-to-list 'jabber-message-chain 'jabber-process-chat) + +(defun jabber-process-chat (xml-data) + "If XML-DATA is a one-to-one chat message, handle it as such." + ;; XXX: there's more to being a chat message than not being MUC. + ;; Maybe make independent predicate. + (when (not (jabber-muc-message-p xml-data)) + ;; Note that we handle private MUC messages here. + (let ((from (jabber-xml-get-attribute xml-data 'from)) + (error-p (jabber-xml-get-children xml-data 'error)) + (body-text (car (jabber-xml-node-children + (car (jabber-xml-get-children + xml-data 'body)))))) + (with-current-buffer (if (jabber-muc-sender-p from) + (jabber-muc-private-create-buffer + (jabber-jid-user from) + (jabber-jid-resource from)) + (jabber-chat-create-buffer from)) + ;; Call alert hooks only when something is output + (when + (jabber-chat-buffer-display (if (jabber-muc-sender-p from) + 'jabber-muc-private-print-prompt + 'jabber-chat-print-prompt) + xml-data + (if error-p + '(jabber-chat-print-error) + jabber-chat-printers) + xml-data) + + (dolist (hook '(jabber-message-hooks jabber-alert-message-hooks)) + (run-hook-with-args hook + from (current-buffer) body-text + (funcall jabber-alert-message-function + from (current-buffer) body-text)))))))) + +(defun jabber-chat-send (body) + "Send BODY, and display it in chat buffer." + (let* ((id (apply 'format "emacs-msg-%d.%d.%d" (current-time))) + (stanza-to-send `(message + ((to . ,jabber-chatting-with) + (type . "chat") + (id . ,id)) + (body () ,(jabber-escape-xml body))))) + (dolist (hook jabber-chat-send-hooks) + (nconc stanza-to-send (funcall hook body id))) + (jabber-send-sexp stanza-to-send)) + + ;; Note that we pass a string, not an XML stanza, + ;; to the print functions. + (jabber-chat-buffer-display 'jabber-chat-self-prompt + nil + '(insert) + (jabber-propertize + body + 'face 'jabber-chat-text-local))) + +(defun jabber-maybe-print-rare-time (timestamp) + "Print rare time, if changed since last time printed." + (let ((new-time (format-time-string jabber-rare-time-format timestamp))) + (unless (string= new-time jabber-rare-time) + (setq jabber-rare-time new-time) + (when jabber-print-rare-time + (let ((inhibit-read-only t)) + (goto-char jabber-point-insert) + (insert (jabber-propertize jabber-rare-time 'face 'jabber-rare-time-face) "\n") + (setq jabber-point-insert (point))))))) + +(defun jabber-chat-print-prompt (xml-data) + "Print prompt for received message in XML-DATA." + (let ((from (jabber-xml-get-attribute xml-data 'from)) + (timestamp (car (delq nil (mapcar 'jabber-x-delay (jabber-xml-get-children xml-data 'x)))))) + (jabber-maybe-print-rare-time timestamp) + (insert (jabber-propertize + (format-spec jabber-chat-foreign-prompt-format + (list + (cons ?t (format-time-string + (if timestamp + jabber-chat-delayed-time-format + jabber-chat-time-format) + timestamp)) + (cons ?n (jabber-jid-displayname from)) + (cons ?u (or (jabber-jid-username from) from)) + (cons ?r (jabber-jid-resource from)) + (cons ?j (jabber-jid-user from)))) + 'face 'jabber-chat-prompt-foreign + 'help-echo + (concat (format-time-string "On %Y-%m-%d %H:%M:%S" timestamp) " from " from))))) + +(defun jabber-chat-self-prompt (timestamp) + "Print prompt for sent message. +TIMESTAMP is the timestamp to print, or nil for now." + (jabber-maybe-print-rare-time timestamp) + (insert (jabber-propertize + (format-spec jabber-chat-local-prompt-format + (list + (cons ?t (format-time-string + (if timestamp + jabber-chat-delayed-time-format + jabber-chat-time-format) + timestamp)) + (cons ?n jabber-nickname) + (cons ?u jabber-username) + (cons ?r jabber-resource) + (cons ?j (concat jabber-username "@" jabber-server)))) + 'face 'jabber-chat-prompt-local + 'help-echo + (concat (format-time-string "On %Y-%m-%d %H:%M:%S" timestamp) " from you")))) + +(defun jabber-chat-print-error (xml-data) + "Print error in given <message/> in a readable way." + (let ((the-error (car (jabber-xml-get-children xml-data 'error)))) + (insert + (jabber-propertize + (concat "Error: " (jabber-parse-error the-error)) + 'face 'jabber-chat-error)))) + +(defun jabber-chat-print-subject (xml-data) + "Print subject of given <message/>, if any." + (let ((subject (car + (jabber-xml-node-children + (car + (jabber-xml-get-children xml-data 'subject)))))) + (when (not (zerop (length subject))) + (insert (jabber-propertize + "Subject: " 'face 'jabber-chat-prompt-system) + (jabber-propertize + subject + 'face 'jabber-chat-text-foreign) + "\n")))) + +(defun jabber-chat-print-body (xml-data) + (run-hook-with-args-until-success 'jabber-body-printers xml-data)) + +(defun jabber-chat-normal-body (xml-data) + "Print body for received message in XML-DATA." + (let ((body (car + (jabber-xml-node-children + (car + (jabber-xml-get-children xml-data 'body)))))) + (when body + (if (string-match "^/me \\(.*\\)$" body) + (let ((action (match-string 1 body)) + (nick (if (jabber-muc-message-p xml-data) + (jabber-jid-resource (jabber-xml-get-attribute xml-data 'from)) + (jabber-jid-displayname (jabber-xml-get-attribute xml-data 'from))))) + (insert (jabber-propertize + (concat nick + " " + action) + 'face 'jabber-chat-prompt-system))) + (insert (jabber-propertize body + 'face 'jabber-chat-text-foreign))) + t))) + +(defun jabber-chat-print-url (xml-data) + "Print URLs provided in jabber:x:oob namespace." + (dolist (x (jabber-xml-node-children xml-data)) + (when (and (listp x) (eq (jabber-xml-node-name x) 'x) + (string= (jabber-xml-get-attribute x 'xmlns) "jabber:x:oob")) + + (let ((url (car (jabber-xml-node-children + (car (jabber-xml-get-children x 'url))))) + (desc (car (jabber-xml-node-children + (car (jabber-xml-get-children x 'desc)))))) + (insert (jabber-propertize + "URL: " 'face 'jabber-chat-prompt-system)) + (insert (format "%s <%s>" desc url)) + (insert "\n"))))) + +(defun jabber-chat-goto-address (&rest ignore) + "Call `goto-address' on the newly written text." + (goto-address)) + +(add-to-list 'jabber-jid-chat-menu + (cons "Send message" 'jabber-send-message)) + +(defun jabber-send-message (to subject body type) + "send a message tag to the server" + (interactive (list (jabber-read-jid-completing "to: ") + (jabber-read-with-input-method "subject: ") + (jabber-read-with-input-method "body: ") + (read-string "type: "))) + (jabber-send-sexp `(message ((to . ,to) + ,(if (> (length type) 0) + `(type . ,type))) + ,(if (> (length subject) 0) + `(subject () ,(jabber-escape-xml subject))) + ,(if (> (length body) 0) + `(body () ,(jabber-escape-xml body))))) + (if (and jabber-history-enabled (not (string= type "groupchat"))) + (jabber-history-log-message "out" nil to body (current-time)))) + +(add-to-list 'jabber-jid-chat-menu + (cons "Start chat" 'jabber-chat-with)) + +(defun jabber-chat-with (jid &optional other-window) + "Open an empty chat window for chatting with JID. +With a prefix argument, open buffer in other window." + (interactive (list (jabber-read-jid-completing "chat with:") + current-prefix-arg)) + (let ((buffer (jabber-chat-create-buffer jid))) + (if other-window + (switch-to-buffer-other-window buffer) + (switch-to-buffer buffer)))) + +(defun jabber-chat-with-jid-at-point (&optional other-window) + "Start chat with JID at point. +Signal an error if there is no JID at point. +With a prefix argument, open buffer in other window." + (interactive "P") + (let ((jid-at-point (get-text-property (point) + 'jabber-jid))) + (if jid-at-point + (jabber-chat-with jid-at-point other-window) + (error "No contact at point")))) + +(provide 'jabber-chat) + +;; arch-tag: f423eb92-aa87-475b-b590-48c93ccba9be diff --git a/jabber-chatbuffer.el b/jabber-chatbuffer.el new file mode 100644 index 0000000..e729bb1 --- /dev/null +++ b/jabber-chatbuffer.el @@ -0,0 +1,162 @@ +;; jabber-chatbuffer.el - functions common to all chat buffers + +;; Copyright (C) 2005 - Magnus Henoch - mange@freemail.hu + +;; 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-keymap) + +(defvar jabber-point-insert nil + "Position where the message being composed starts") + +(defvar jabber-send-function nil + "Function for sending a message from a chat buffer.") + +(defvar jabber-chat-mode-hook nil + "Hook called at the end of `jabber-chat-mode'. +Note that functions in this hook have no way of knowing +what kind of chat buffer is being created.") + +(defcustom jabber-chat-fill-long-lines t + "If non-nil, fill long lines in chat buffers. +Lines are broken at word boundaries at the width of the +window or at `fill-column', whichever is shorter." + :group 'jabber-chat + :type 'boolean) + +(defun jabber-chat-mode () + "\\{jabber-chat-mode-map}" + (kill-all-local-variables) + ;; Make sure to set this variable somewhere + (make-local-variable 'jabber-send-function) + + (make-local-variable 'scroll-conservatively) + (setq scroll-conservatively 5) + + (make-local-variable 'jabber-point-insert) + (setq jabber-point-insert (point-min)) + + ;;(setq header-line-format jabber-chat-header-line-format) + + (setq major-mode 'jabber-chat-mode + mode-name "jabber-chat") + (use-local-map jabber-chat-mode-map) + + (if (fboundp 'run-mode-hooks) + (run-mode-hooks 'jabber-chat-mode-hook) + (run-hooks 'jabber-chat-mode-hook))) + +(put 'jabber-chat-mode 'mode-class 'special) + +;; Spell check only what you're currently writing +(defun jabber-chat-mode-flyspell-verify () + (>= (point) jabber-point-insert)) +(put 'jabber-chat-mode 'flyspell-mode-predicate + 'jabber-chat-mode-flyspell-verify) + +(defvar jabber-chat-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map jabber-common-keymap) + (define-key map "\r" 'jabber-chat-buffer-send) + map)) + +(defun jabber-chat-buffer-send () + (interactive) + (let ((body (delete-and-extract-region jabber-point-insert (point-max)))) + ;; If user accidentally hits RET without writing anything, + ;; delete-and-extract-region returns "". In that case, + ;; no message should be sent. + (unless (zerop (length body)) + (funcall jabber-send-function body)))) + +(defun jabber-chat-buffer-display (prompt-function prompt-data output-functions output-data) + "Display a message in current buffer. +PROMPT-FUNCTION is a function that prints the correct prompt at +point. It is called with PROMPT-DATA as argument. +OUTPUT-FUNCTIONS is a list of functions that may or may not print something +at point. They are called in order with OUTPUT-DATA as argument. +If the OUTPUT-FUNCTIONS produce any output, PROMPT-FUNCTION is called +with point before that output. If there is no output, there is +no prompt. Return non-nil if there is output. + +If point is at or after jabber-point-insert, it is advanced. +If point is before jabber-point-insert, it is not moved." + (let ((at-insert-point (eq (point) jabber-point-insert)) + outputp) + (save-excursion + (goto-char jabber-point-insert) + (setq outputp + (jabber-chat-buffer-display-at-point prompt-function prompt-data output-functions output-data)) + (setq jabber-point-insert (point)) + (set-text-properties jabber-point-insert (point-max) nil)) + + (when at-insert-point + (goto-char jabber-point-insert)) + outputp)) + +(defun jabber-chat-buffer-display-at-point (prompt-function prompt-data output-functions output-data) + "Display a message at point. +Arguments are as to `jabber-chat-buffer-display'. +Return non-nil if any data was inserted." + (let ((inhibit-read-only t) + (beg (point)) + (point-insert (set-marker (make-marker) jabber-point-insert))) + (set-marker-insertion-type point-insert t) + + (dolist (printer output-functions) + (funcall printer output-data) + (unless (bolp) + (insert "\n"))) + + (unless (eq (point) beg) + (let ((end (point-marker))) + (goto-char beg) + (funcall prompt-function prompt-data) + (goto-char end) + (put-text-property beg end 'read-only t) + (put-text-property beg end 'front-sticky t) + (put-text-property beg end 'rear-nonsticky t) + (when jabber-chat-fill-long-lines + (save-restriction + (narrow-to-region beg end) + (jabber-chat-buffer-fill-long-lines))) + ;; this is always non-nil, so we return that + (setq jabber-point-insert (marker-position point-insert)))))) + +(defun jabber-chat-buffer-fill-long-lines () + "Fill lines that are wider than the window width." + ;; This was mostly stolen from article-fill-long-lines + (interactive) + (save-excursion + (let ((inhibit-read-only t) + (width (window-width (get-buffer-window (current-buffer))))) + (save-restriction + (goto-char (point-min)) + (let ((adaptive-fill-mode nil)) ;Why? -sm + (while (not (eobp)) + (end-of-line) + (when (>= (current-column) (min fill-column width)) + (narrow-to-region (min (1+ (point)) (point-max)) + (point-at-bol)) + (let ((goback (point-marker))) + (fill-paragraph nil) + (goto-char (marker-position goback))) + (widen)) + (forward-line 1))))))) + +(provide 'jabber-chatbuffer) +;; arch-tag: 917e5b60-5894-4c49-b3bc-12e1f97ffdc6 diff --git a/jabber-conn.el b/jabber-conn.el new file mode 100644 index 0000000..f3488ef --- /dev/null +++ b/jabber-conn.el @@ -0,0 +1,138 @@ +;; jabber-conn.el - Network transport functions + +;; Copyright (C) 2005 - Georg Lehner - jorge@magma.com.ni +;; mostly inspired by Gnus. + +;; 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 + +;; A collection of functions, that hide the details of transmitting to +;; and fro a Jabber Server + +(eval-when-compile (require 'cl)) + +;; Try two different TLS/SSL libraries, but don't fail if none available. +(or (ignore-errors (require 'tls)) + (ignore-errors (require 'ssl))) + +;; TODO: Add custom flag, to not complain about plain-text passwords +;; in encrypted connections +;; + +;; This variable holds the connection, which is used for further +;; input/output to the server +(defvar *jabber-connection* nil + "the process that does the actual connection") + +(defgroup jabber-conn nil "Jabber Connection Settings" + :group 'jabber) + +(defcustom jabber-network-server nil + "hostname or IP address of server to connect to, if different from `jabber-server'." + :type '(radio (const :tag "Same as `jabber-server'" nil) + (string :tag "Hostname or IP address")) + :group 'jabber-conn) + +(defcustom jabber-port nil + "jabber port +The default depends on the connection type: 5222 for ordinary connections +and 5223 for SSL connections." + :type '(choice (const :tag "Default" nil) + (integer :tag "Port number")) + :group 'jabber-conn) + +(defcustom jabber-connection-type 'network + "Type of connection to the jabber server, ssl or network most likely." + :type '(radio (const :tag "Encrypted connection, SSL" ssl) + (const :tag "Standard TCP/IP connection" network)) + :group 'jabber-conn) + +(defcustom jabber-connection-ssl-program nil + "Program used for SSL/TLS connections. +nil means prefer gnutls but fall back to openssl. +'gnutls' means use gnutls (through `open-tls-stream'). +'openssl means use openssl (through `open-ssl-stream')." + :type '(choice (const :tag "Prefer gnutls, fall back to openssl" nil) + (const :tag "Use gnutls" gnutls) + (const :tag "Use openssl" openssl)) + :group 'jabber-conn) + +(defvar jabber-connect-methods + '((network jabber-network-connect jabber-network-send) + (ssl jabber-ssl-connect jabber-ssl-send)) + "Alist of connection methods and functions. +First item is the symbol naming the method. +Second item is the connect function. +Third item is the send function.") + +(defvar jabber-connect-function nil + "function that connects to the jabber server") + +(defvar jabber-conn-send-function nil + "function that sends a line to the server") + +(defun jabber-setup-connect-method () + (let ((entry (assq jabber-connection-type jabber-connect-methods))) + (setq jabber-connect-function (nth 1 entry)) + (setq jabber-conn-send-function (nth 2 entry)))) + +;; Plain TCP/IP connection +(defun jabber-network-connect () + (let ((coding-system-for-read 'utf-8) + (coding-system-for-write 'utf-8)) + (setq *jabber-connection* + (open-network-stream + "jabber" + jabber-process-buffer + (or jabber-network-server jabber-server) + (or jabber-port 5222))))) + +(defun jabber-network-send (string) + "Send a string via a plain TCP/IP connection to the Jabber Server." + (process-send-string *jabber-connection* string)) + +;; SSL connection, we use openssl's s_client function for encryption +;; of the link +;; TODO: make this configurable +(defun jabber-ssl-connect () + "connect via OpenSSL or GnuTLS to a Jabber Server" + (let ((coding-system-for-read 'utf-8) + (coding-system-for-write 'utf-8) + (connect-function + (cond + ((and (memq jabber-connection-ssl-program '(nil gnutls)) + (fboundp 'open-tls-stream)) + 'open-tls-stream) + ((and (memq jabber-connection-ssl-program '(nil openssl)) + (fboundp 'open-ssl-stream)) + 'open-ssl-stream) + (t + (error "Neither TLS nor SSL connect functions available"))))) + (setq *jabber-connection* + (funcall connect-function + "jabber" + jabber-process-buffer + (or jabber-network-server jabber-server) + (or jabber-port 5223))))) + +(defun jabber-ssl-send (string) + "Send a string via an SSL-encrypted connection to the Jabber Server, + it seems we need to send a linefeed afterwards" + (process-send-string *jabber-connection* string) + (process-send-string *jabber-connection* "\n")) + +(provide 'jabber-conn) +;; arch-tag: f95ec240-8cd3-11d9-9dbf-000a95c2fcd0 diff --git a/jabber-core.el b/jabber-core.el new file mode 100644 index 0000000..257cd14 --- /dev/null +++ b/jabber-core.el @@ -0,0 +1,511 @@ +;; jabber-core.el - core functions + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; SSL-Connection Parts: +;; Copyright (C) 2005 - Georg Lehner - jorge@magma.com.ni + +;; 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 + +(eval-when-compile (require 'cl)) + +(require 'jabber-util) +(require 'jabber-logon) +(require 'jabber-conn) + +;; SASL depends on FLIM. +(eval-and-compile + (condition-case nil + (require 'jabber-sasl) + (error nil))) + +(defvar *jabber-roster* nil + "the roster list") + +(defvar jabber-jid-obarray (make-vector 127 0) + "obarray for keeping JIDs") + +(defvar *jabber-connected* nil + "boolean - are we connected") + +(defvar *jabber-authenticated* nil + "boolean - are we authenticated") + +(defvar *jabber-disconnecting* nil + "boolean - are we in the process of disconnecting by free will") + +(defvar *xmlq* "" + "a string with all the incoming xml that is waiting to be parsed") + +(defvar jabber-register-p nil + "Register a new account in this session?") + +(defvar jabber-session-id nil + "id of the current session") + +(defvar jabber-stream-version nil + "Stream version indicated by server") + +(defvar jabber-register-p nil + "Is account registration occurring in this session?") + +(defvar jabber-call-on-connection nil + "Function to be called on connection. +This is set by `jabber-connect' on each call, and later picked up in +`jabber-filter'.") + +(defvar jabber-short-circuit-input nil + "Function that receives all stanzas, instead of the usual ones. +Used for SASL authentication.") + +(defvar jabber-message-chain nil + "Incoming messages are sent to these functions, in order.") + +(defvar jabber-iq-chain nil + "Incoming infoqueries are sent to these functions, in order.") + +(defvar jabber-presence-chain nil + "Incoming presence notifications are sent to these functions, in order.") + +(defvar jabber-stream-error-chain '(jabber-process-stream-error) + "Stream errors are sent to these functions, in order") + +(defvar jabber-choked-count 0 + "Number of successive times that the process buffer has been nonempty.") + +(defvar jabber-choked-timer nil) + +(defgroup jabber-core nil "customize core functionality" + :group 'jabber) + +(defcustom jabber-post-connect-hook '(jabber-send-default-presence + jabber-muc-autojoin) + "*Hooks run after successful connection and authentication." + :type 'hook + :group 'jabber-core) + +(defcustom jabber-pre-disconnect-hook nil + "*Hooks run just before voluntary disconnection +This might be due to failed authentication. Check `*jabber-authenticated*'." + :type 'hook + :group 'jabber-core) + +(defcustom jabber-lost-connection-hook nil + "*Hooks run after involuntary disconnection" + :type 'hook + :group 'jabber-core) + +(defcustom jabber-post-disconnect-hook nil + "*Hooks run after disconnection" + :type 'hook + :group 'jabber-core) + +(defcustom jabber-roster-buffer "*-jabber-*" + "The name of the roster buffer" + :type 'string + :group 'jabber-core) + +(defvar jabber-process-buffer " *-jabber-process-*" + "The name of the process buffer") + +(defcustom jabber-use-sasl t + "If non-nil, use SASL if possible. +SASL will still not be used if the library for it is missing or +if the server doesn't support it. + +Disabling this shouldn't be necessary, but it may solve certain +problems." + :type 'boolean + :group 'jabber-core) + +(defsubst jabber-have-sasl-p () + "Return non-nil if SASL functions are available." + (fboundp 'jabber-sasl-start-auth)) + +(defun jabber-connect (&optional registerp) + "connect to the jabber server and start a jabber xml stream +With prefix argument, register a new account." + (interactive "P") + (if *jabber-connected* + (message "Already connected") + (setq *xmlq* "") + (setq *jabber-authenticated* nil) + (jabber-clear-roster) + (jabber-reset-choked) + + ;; Call the function responsible for establishing a bidirectional + ;; data stream to the Jabber Server, *jabber-connection* is set + ;; afterwards. + (jabber-setup-connect-method) + (funcall jabber-connect-function) + (unless *jabber-connection* + (error "Connection failed")) + + ;; TLS connections leave data in the process buffer, which + ;; the XML parser will choke on. + (with-current-buffer (process-buffer *jabber-connection*) + (erase-buffer)) + (set-process-filter *jabber-connection* #'jabber-pre-filter) + (set-process-sentinel *jabber-connection* #'jabber-sentinel) + + (setq jabber-short-circuit-input nil) + (setq jabber-register-p registerp) + + (setq jabber-call-on-connection (if registerp + #'(lambda (stream-features) (jabber-get-register jabber-server)) + #'jabber-auth-somehow)) + (let ((stream-header (concat "<?xml version='1.0'?><stream:stream to='" + jabber-server + "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'" + ;; Not supporting SASL is not XMPP compliant, + ;; so don't pretend we are. + (if (and (jabber-have-sasl-p) jabber-use-sasl) + " version='1.0'" + "") + "> +"))) + + (funcall jabber-conn-send-function stream-header) + (if jabber-debug-log-xml + (with-current-buffer (get-buffer-create "*-jabber-xml-log-*") + (save-excursion + (goto-char (point-max)) + (insert (format "sending %S\n\n" stream-header))))) + + (setq jabber-choked-timer + (run-with-timer 5 5 #'jabber-check-choked)) + + (accept-process-output *jabber-connection*)) + ;; Next thing happening is the server sending its own <stream:stream> start tag. + ;; That is handled in jabber-filter. + + (setq *jabber-connected* t))) + +(defun jabber-auth-somehow (stream-features) + "Start authentication with SASL if the server supports it, +otherwise JEP-0077. The STREAM-FEATURES argument is the stream features +tag, or nil if we're connecting to a pre-XMPP server." + (if (and stream-features + jabber-use-sasl + (jabber-have-sasl-p) + jabber-stream-version + (>= (string-to-number jabber-stream-version) 1.0)) + (jabber-sasl-start-auth stream-features) + (jabber-get-auth jabber-server))) + +(defun jabber-disconnect () + "disconnect from the jabber server and re-initialise the jabber package variables" + (interactive) + (unless *jabber-disconnecting* ; avoid reentry + (let ((*jabber-disconnecting* t)) + (when (and *jabber-connection* + (memq (process-status *jabber-connection*) '(open run))) + (run-hooks 'jabber-pre-disconnect-hook) + (funcall jabber-conn-send-function "</stream:stream>") + ;; let the server close the stream + (accept-process-output *jabber-connection* 3) + ;; and do it ourselves as well, just to be sure + (delete-process *jabber-connection*)) + (jabber-disconnected) + (if (interactive-p) + (message "Disconnected from Jabber server"))))) + +(defun jabber-disconnected () + "Re-initialise jabber package variables. +Call this function after disconnection." + (when jabber-choked-timer + (jabber-cancel-timer jabber-choked-timer) + (setq jabber-choked-timer nil)) + + (when (get-buffer jabber-roster-buffer) + (with-current-buffer (get-buffer jabber-roster-buffer) + (let ((inhibit-read-only t)) + (erase-buffer)))) + + (setq *jabber-connection* nil) + (jabber-clear-roster) + (setq *xmlq* "") + (setq *jabber-authenticated* nil) + (setq *jabber-connected* nil) + (setq *jabber-active-groupchats* nil) + (setq jabber-session-id nil) + (run-hooks 'jabber-post-disconnect-hook)) + +(defun jabber-sentinel (process event) + "alert user about lost connection" + (unless (or *jabber-disconnecting* (not *jabber-connected*)) + (beep) + (run-hooks 'jabber-lost-connection-hook) + (message "Jabber connection lost: `%s'" event) + ;; If there is data left (maybe a stream error) process it first + (with-current-buffer (process-buffer process) + (unless (zerop (buffer-size)) + (jabber-filter process))) + (jabber-disconnected))) + +(defun jabber-pre-filter (process string) + (with-current-buffer (process-buffer process) + ;; Append new data + (goto-char (point-max)) + (insert string) + + (unless (boundp 'jabber-filtering) + (let (jabber-filtering) + (jabber-filter process))))) + +(defun jabber-filter (process) + "the filter function for the jabber process" + (with-current-buffer (process-buffer process) + ;; Start from the beginning + (goto-char (point-min)) + (let (xml-data) + (loop + do + ;; Skip whitespace + (unless (zerop (skip-chars-forward " \t\r\n")) + (delete-region (point-min) (point))) + ;; Skip processing directive + (when (looking-at "<\\?xml[^?]*\\?>") + (delete-region (match-beginning 0) (match-end 0))) + + ;; Stream end? + (when (looking-at "</stream:stream>") + (return (jabber-disconnect))) + + ;; Stream header? + (when (looking-at "<stream:stream[^>]*>") + (let ((stream-header (match-string 0)) + (ending-at (match-end 0))) + ;; These regexps extract attribute values from the stream + ;; header, taking into account that the quotes may be either + ;; single or double quotes. + (setq jabber-session-id + (and (or (string-match "id='\\([^']+\\)'" stream-header) + (string-match "id=\"\\([^\"]+\\)\"" stream-header)) + (jabber-unescape-xml (match-string 1 stream-header)))) + (setq jabber-stream-version + (and (or + (string-match "version='\\([0-9.]+\\)'" stream-header) + (string-match "version=\"\\([0-9.]+\\)\"" stream-header)) + (match-string 1 stream-header))) + (if jabber-debug-log-xml + (with-current-buffer (get-buffer-create "*-jabber-xml-log-*") + (save-excursion + (goto-char (point-max)) + (insert (format "receive %S\n\n" stream-header))))) + + ;; If the server is XMPP compliant, i.e. there is a version attribute + ;; and it's >= 1.0, there will be a stream:features tag shortly, + ;; so just wait for that. + + ;; the stream feature is only sent if the initiating entity has + ;; sent 1.0 in the stream header. if sasl is not supported then + ;; we don't send 1.0 in the header and therefore we shouldn't wait + ;; even if 1.0 is present in the receiving stream. + (unless (and jabber-stream-version + (>= (string-to-number jabber-stream-version) 1.0) + jabber-use-sasl + (jabber-have-sasl-p)) + ;; Logon or register + (funcall jabber-call-on-connection nil)) + + (delete-region (point-min) ending-at))) + + ;; Normal tag + + ;; XXX: do these checks make sense? If so, reinstate them. + ;;(if (active-minibuffer-window) + ;; (run-with-idle-timer 0.01 nil #'jabber-filter process string) + + ;; This check is needed for xml.el of Emacs 21, as it chokes on + ;; empty attribute values. + (save-excursion + (while (search-forward-regexp " \\w+=''" nil t) + (replace-match ""))) + + (setq xml-data (and (catch 'unfinished + (jabber-xml-skip-tag-forward) + (> (point) (point-min))) + (xml-parse-region (point-min) (point)))) + (if xml-data + (jabber-reset-choked)) + + while xml-data + do + ;; If there's a problem with writing the XML log, + ;; make sure the stanza is delivered, at least. + (condition-case e + (if jabber-debug-log-xml + (with-current-buffer (get-buffer-create "*-jabber-xml-log-*") + (save-excursion + (goto-char (point-max)) + (insert (format "receive %S\n\n" (car xml-data)))))) + (error + (ding) + (message "Couldn't write XML log: %s" (error-message-string e)) + (sit-for 2))) + (delete-region (point-min) (point)) + ;; We explicitly don't catch errors in jabber-process-input, + ;; to facilitate debugging. + (jabber-process-input (car xml-data)))))) + +(defun jabber-reset-choked () + (setq jabber-choked-count 0)) + +(defun jabber-check-choked () + ;; "Choked" means that data is sitting in the process buffer + ;; without being parsed, despite several attempts. + (if (zerop (buffer-size (process-buffer *jabber-connection*))) + (jabber-reset-choked) + (incf jabber-choked-count) + (if (and (> jabber-choked-count 3) + ;; Now we're definitely choked. Take action. + ;; But ask user first. + (yes-or-no-p "jabber.el is severely confused. Bail out? ")) + (run-with-idle-timer 0.1 nil 'jabber-choked-bail-out) + (jabber-reset-choked)))) + +(defun jabber-choked-bail-out () + ;; So here we are. Something in the process buffer prevents us + ;; from continuing normally. Let's die honorably by providing + ;; bug report material. + (with-current-buffer (generate-new-buffer "*jabber-bug*") + (insert "jabber.el couldn't cope with the data received from the server. +This should never happen, but apparently it did. + +The information below will be helpful in tracking down and fixing +the bug. You may want to edit out any sensitive information. + +Please go to +http://sourceforge.net/tracker/?group_id=88346&atid=586350 and +submit a bug report, including the information below. + +") + (goto-address) + (emacs-version t) + (insert "\n\nThe following couldn't be parsed:\n") + (insert-buffer-substring (process-buffer *jabber-connection*)) + (switch-to-buffer (current-buffer))) + (jabber-disconnect)) + +(defun jabber-process-input (xml-data) + "process an incoming parsed tag" + (let* ((tag (jabber-xml-node-name xml-data)) + (functions (eval (cdr (assq tag '((iq . jabber-iq-chain) + (presence . jabber-presence-chain) + (message . jabber-message-chain) + (stream:error . jabber-stream-error-chain))))))) + ;; Special treatment of the stream:features tag. The first time we get it, + ;; it means that we should authenticate. The second time, we should + ;; establish a session. (The zeroth time it's STARTTLS, but that's not + ;; implemented yet.) + (if (eq tag 'stream:features) + (if *jabber-authenticated* + (jabber-bind-and-establish-session xml-data) + (funcall jabber-call-on-connection xml-data)) + (if jabber-short-circuit-input + (funcall jabber-short-circuit-input xml-data) + (dolist (f functions) + (funcall f xml-data)))))) + +(defun jabber-process-stream-error (xml-data) + "Process an incoming stream error." + (beep) + (run-hooks 'jabber-lost-connection-hook) + (message "Stream error, connection lost: %s" (jabber-parse-stream-error xml-data)) + (jabber-disconnect)) + +(defun jabber-bind-and-establish-session (xml-data) + ;; Now we have a stream:features tag. We expect it to contain bind and + ;; session tags. If it doesn't, the server we are connecting to is no + ;; IM server. + (unless (and (jabber-xml-get-children xml-data 'bind) + (jabber-xml-get-children xml-data 'session)) + (jabber-disconnect) + (error "Server doesn't permit resource binding and session establishing")) + + ;; So let's bind a resource. We can either pick a resource ourselves, + ;; or have the server pick one for us. + (jabber-send-iq nil "set" + `(bind ((xmlns . "urn:ietf:params:xml:ns:xmpp-bind")) + (resource () ,jabber-resource)) + #'jabber-process-bind t + #'jabber-process-bind nil)) + +(defun jabber-process-bind (xml-data successp) + (unless successp + (jabber-disconnect) + (error "Resource binding failed: %s" + (jabber-parse-error (car (jabber-xml-get-children xml-data 'error))))) + + (let ((jid (car + (jabber-xml-node-children + (car + (jabber-xml-get-children + (jabber-iq-query xml-data) 'jid)))))) + ;; Maybe this isn't the JID we asked for. + (setq jabber-username (jabber-jid-username jid)) + (setq jabber-server (jabber-jid-server jid)) + (setq jabber-resource (jabber-jid-resource jid))) + + ;; Been there, done that. Time to establish a session. + (jabber-send-iq nil "set" + '(session ((xmlns . "urn:ietf:params:xml:ns:xmpp-session"))) + #'jabber-process-session t + #'jabber-process-session nil)) + +(defun jabber-process-session (xml-data successp) + (unless successp + (jabber-disconnect) + (error "Session establishing failed: %s" + (jabber-parse-error (car (jabber-xml-get-children xml-data 'error))))) + + ;; Now, request roster. + (jabber-send-iq nil + "get" + '(query ((xmlns . "jabber:iq:roster"))) + #'jabber-process-roster 'initial + #'jabber-report-success "Roster retrieval") + + (run-hooks 'jabber-post-connect-hook)) + +(defun jabber-clear-roster () + "Clean up the roster." + ;; This is made complicated by the fact that the JIDs are symbols with properties. + (mapatoms #'(lambda (x) + (unintern x jabber-jid-obarray)) + jabber-jid-obarray) + (setq *jabber-roster* nil)) + +(defun jabber-send-sexp (sexp) + "send the xml corresponding to SEXP to the jabber server" + (condition-case e + (if jabber-debug-log-xml + (with-current-buffer (get-buffer-create "*-jabber-xml-log-*") + (save-excursion + (goto-char (point-max)) + (insert (format "sending %S\n\n" sexp))))) + (error + (ding) + (message "Couldn't write XML log: %s" (error-message-string e)) + (sit-for 2))) + (funcall jabber-conn-send-function (jabber-sexp2xml sexp))) + +(provide 'jabber-core) + +;;; arch-tag: 9d273ce6-c45a-447b-abf3-21d3ce73a51a diff --git a/jabber-disco.el b/jabber-disco.el new file mode 100644 index 0000000..226be6e --- /dev/null +++ b/jabber-disco.el @@ -0,0 +1,202 @@ +;; jabber-disco.el - service discovery functions + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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 + + +;;; All the client part should be seriously rewritten, or at least +;;; reconsidered. I'm imagining a separation between backend and +;;; frontend, so that various functions can perform disco queries for +;;; their own purposes, and maybe some caching with that. + +(require 'jabber-iq) +(require 'jabber-xml) +(require 'jabber-menu) + +;; Advertise your features here. Add the namespace to this list. +(defvar jabber-advertised-features + (list "http://jabber.org/protocol/disco#info") + "Features advertised on service discovery requests") + +(defvar jabber-disco-items-nodes + (list + (list "" nil nil)) + "Alist of node names and information about returning disco item data. +Key is node name as a string, or \"\" for no node specified. Value is +a list of two items. + +First item is data to return. If it is a function, that function is +called and its return value is used; if it is a list, that list is +used. The list should be the XML data to be returned inside the +<query/> element, like this: + +((item ((name . \"Name of first item\") + (jid . \"first.item\") + (node . \"node\")))) + +Second item is access control function. That function is passed the +JID, and returns non-nil if access is granted. If the second item is +nil, access is always granted.") + +(defvar jabber-disco-info-nodes + (list + (list "" #'jabber-disco-return-client-info nil)) + "Alist of node names and information returning disco info data. +Key is node name as a string, or \"\" for no node specified. Value is +a list of two items. + +First item is data to return. If it is a function, that function is +called and its return value is used; if it is a list, that list is +used. The list should be the XML data to be returned inside the +<query/> element, like this: + +((identity ((category . \"client\") + (type . \"pc\") + (name . \"Jabber client\"))) + (feature ((var . \"some-feature\")))) + +Second item is access control function. That function is passed the +JID, and returns non-nil if access is granted. If the second item is +nil, access is always granted.") + +(defun jabber-process-disco-info (xml-data) + "Handle results from info disco requests." + + (let ((beginning (point))) + (dolist (x (jabber-xml-node-children (jabber-iq-query xml-data))) + (cond + ((eq (jabber-xml-node-name x) 'identity) + (let ((name (jabber-xml-get-attribute x 'name)) + (category (jabber-xml-get-attribute x 'category)) + (type (jabber-xml-get-attribute x 'type))) + (insert (jabber-propertize (if name + (jabber-unescape-xml name) + "Unnamed") + 'face 'jabber-title-medium) + "\n\nCategory:\t" category "\n") + (if type + (insert "Type:\t\t" type "\n")) + (insert "\n"))) + ((eq (jabber-xml-node-name x) 'feature) + (let ((var (jabber-xml-get-attribute x 'var))) + (insert "Feature:\t" var "\n"))))) + (put-text-property beginning (point) 'jabber-jid (jabber-xml-get-attribute xml-data 'from)))) + +(defun jabber-process-disco-items (xml-data) + "Handle results from items disco requests." + + (let ((items (jabber-xml-get-children (jabber-iq-query xml-data) 'item))) + (if items + (dolist (item items) + (let ((jid (jabber-xml-get-attribute item 'jid)) + (name (jabber-xml-get-attribute item 'name)) + (node (jabber-xml-get-attribute item 'node))) + (insert + (jabber-propertize + (concat + (jabber-propertize + (concat jid "\n" (if node (format "Node: %s\n" node))) + 'face 'jabber-title-medium) + (jabber-unescape-xml name) "\n\n") + 'jabber-jid jid + 'jabber-node node)))) + (insert "No items found.\n")))) + +(add-to-list 'jabber-iq-get-xmlns-alist + (cons "http://jabber.org/protocol/disco#info" 'jabber-return-disco-info)) +(add-to-list 'jabber-iq-get-xmlns-alist + (cons "http://jabber.org/protocol/disco#items" 'jabber-return-disco-info)) +(defun jabber-return-disco-info (xml-data) + "Respond to a service discovery request. +See JEP-0030." + (let* ((to (jabber-xml-get-attribute xml-data 'from)) + (id (jabber-xml-get-attribute xml-data 'id)) + (xmlns (jabber-iq-xmlns xml-data)) + (which-alist (eval (cdr (assoc xmlns + (list + (cons "http://jabber.org/protocol/disco#info" 'jabber-disco-info-nodes) + (cons "http://jabber.org/protocol/disco#items" 'jabber-disco-items-nodes)))))) + (node (or + (jabber-xml-get-attribute (jabber-iq-query xml-data) 'node) + "")) + (return-list (cdr (assoc node which-alist))) + (func (nth 0 return-list)) + (access-control (nth 1 return-list))) + (if return-list + (if (and (functionp access-control) + (not (funcall access-control to))) + (jabber-signal-error "cancel" 'not-allowed) + ;; Access control passed + (let ((result (if (functionp func) + (funcall func xml-data) + func))) + (jabber-send-iq to "result" + `(query ((xmlns . ,xmlns)) + ,@result) + nil nil nil nil id))) + + ;; No such node + (jabber-signal-error "cancel" 'item-not-found)))) + +(defun jabber-disco-return-client-info (xml-data) + `( + ;; If running under a window system, this is + ;; a GUI client. If not, it is a console client. + (identity ((category . "client") + (name . "Emacs Jabber client") + (type . ,(if (memq window-system + '(x w32 mac)) + "pc" + "console")))) + ,@(mapcar + #'(lambda (featurename) + `(feature ((var . ,featurename)))) + jabber-advertised-features))) + +(add-to-list 'jabber-jid-info-menu + (cons "Send items disco query" 'jabber-get-disco-items)) +(defun jabber-get-disco-items (to &optional node) + "Send a service discovery request for items" + (interactive (list (jabber-read-jid-completing "Send items disco request to: ") + (jabber-read-node "Node (or leave empty): "))) + (jabber-send-iq to + "get" + (list 'query (append (list (cons 'xmlns "http://jabber.org/protocol/disco#items")) + (if (> (length node) 0) + (list (cons 'node node))))) + #'jabber-process-data #'jabber-process-disco-items + #'jabber-process-data "Item discovery failed")) + +(add-to-list 'jabber-jid-info-menu + (cons "Send info disco query" 'jabber-get-disco-info)) +(defun jabber-get-disco-info (to &optional node) + "Send a service discovery request for info" + (interactive (list (jabber-read-jid-completing "Send info disco request to: ") + (jabber-read-node "Node (or leave empty): "))) + (jabber-send-iq to + "get" + (list 'query (append (list (cons 'xmlns "http://jabber.org/protocol/disco#info")) + (if (> (length node) 0) + (list (cons 'node node))))) + #'jabber-process-data #'jabber-process-disco-info + #'jabber-process-data "Info discovery failed")) + +(provide 'jabber-disco) + +;;; arch-tag: 71f5c76f-2956-4ed2-b871-9f5fe198092d diff --git a/jabber-events.el b/jabber-events.el new file mode 100644 index 0000000..b9fc556 --- /dev/null +++ b/jabber-events.el @@ -0,0 +1,227 @@ +;;; jabber-events.el --- Message events (JEP-0022) implementation + +;; Copyright (C) 2005 Magnus Henoch + +;; Author: Magnus Henoch <mange@freemail.hu> + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +(require 'cl) + +(defgroup jabber-events nil + "Message events and notifications." + :group 'jabber) + +;;; INCOMING +;;; Code for requesting event notifications from others and handling +;;; them. + +(defcustom jabber-events-request-these '(offline + delivered + displayed + composing) + "Request these kinds of event notifications from others." + :type '(set (const :tag "Delivered to offline storage" offline) + (const :tag "Delivered to user's client" delivered) + (const :tag "Displayed to user" displayed) + (const :tag "User is typing a reply" composing)) + :group 'jabber-events) + +(defvar jabber-events-composing-p nil + "Is the other person composing a message?") +(make-variable-buffer-local 'jabber-events-composing-p) + +(defvar jabber-events-arrived nil + "In what way has the message reached the recipient? +Possible values are nil (no information available), offline +\(queued for delivery when recipient is online), delivered +\(message has reached the client) and displayed (user is +probably reading the message).") +(make-variable-buffer-local 'jabber-events-arrived) + +(defvar jabber-events-message "" + "Human-readable presentation of event information") +(make-variable-buffer-local 'jabber-events-message) + +(defun jabber-events-update-message () + (setq jabber-events-message + (concat (cdr (assq jabber-events-arrived + '((offline . "In offline storage") + (delivered . "Delivered") + (displayed . "Displayed")))) + (when jabber-events-composing-p + " (typing a message)")))) + +(add-hook 'jabber-chat-send-hooks 'jabber-events-when-sending) +(defun jabber-events-when-sending (text id) + (setq jabber-events-arrived nil) + (jabber-events-update-message) + `((x ((xmlns . "jabber:x:event")) + ,@(mapcar #'list jabber-events-request-these)))) + +;;; OUTGOING +;;; Code for handling requests for event notifications and providing +;;; them, modulo user preferences. + +(defcustom jabber-events-confirm-delivered t + "Send delivery confirmation if requested?" + :group 'jabber-events + :type 'boolean) + +(defcustom jabber-events-confirm-displayed t + "Send display confirmation if requested?" + :group 'jabber-events + :type 'boolean) + +(defcustom jabber-events-confirm-composing t + "Send notifications about typing a reply?" + :group 'jabber-events + :type 'boolean) + +(defvar jabber-events-requested () + "List of events requested") +(make-variable-buffer-local 'jabber-events-requested) + +(defvar jabber-events-last-id nil + "Id of last message received, or nil if none.") +(make-variable-buffer-local 'jabber-events-last-id) + +(defvar jabber-events-delivery-confirmed nil + "Has delivery confirmation been sent?") +(make-variable-buffer-local 'jabber-events-delivery-confirmed) + +(defvar jabber-events-display-confirmed nil + "Has display confirmation been sent?") +(make-variable-buffer-local 'jabber-events-display-confirmed) + +(defvar jabber-events-composing-sent nil + "Has composing notification been sent? +It can be sent and cancelled several times.") + +(add-hook 'window-configuration-change-hook + 'jabber-events-confirm-display) +(defun jabber-events-confirm-display () + "Send display confirmation if appropriate. +That is, if user allows it, if the other user requested it, +and it hasn't been sent before." + (walk-windows #'jabber-events-confirm-display-in-window)) + +(defun jabber-events-confirm-display-in-window (window) + (with-current-buffer (window-buffer window) + (when (and jabber-events-confirm-displayed + (not jabber-events-display-confirmed) + (memq 'displayed jabber-events-requested) + ;; don't send to bare jids + (jabber-jid-resource jabber-chatting-with)) + (jabber-send-sexp + `(message + ((to . ,jabber-chatting-with)) + (x ((xmlns . "jabber:x:event")) + (displayed) + (id () ,jabber-events-last-id)))) + (setq jabber-events-display-confirmed t)))) + +(defun jabber-events-after-change () + (let ((composing-now (not (eq (point-max) jabber-point-insert)))) + (when (and jabber-events-confirm-composing + jabber-chatting-with + (not (eq composing-now jabber-events-composing-sent))) + (jabber-send-sexp + `(message + ((to . ,jabber-chatting-with)) + (x ((xmlns . "jabber:x:event")) + ,@(if composing-now '((composing)) nil) + (id () ,jabber-events-last-id)))) + (setq jabber-events-composing-sent composing-now)))) + +;;; COMMON + +(add-to-list 'jabber-chat-printers 'jabber-handle-incoming-message-events) + +(defun jabber-handle-incoming-message-events (xml-data) + (let ((x (find "jabber:x:event" + (jabber-xml-get-children xml-data 'x) + :key #'(lambda (x) (jabber-xml-get-attribute x 'xmlns)) + :test #'string=))) + ;; If there's a body, it's not an incoming message event. + (if (jabber-xml-get-children xml-data 'body) + ;; User is done composing, obviously. + (progn + (setq jabber-events-composing-p nil) + (jabber-events-update-message) + + ;; Reset variables + (setq jabber-events-display-confirmed nil) + (setq jabber-events-delivery-confirmed nil) + + ;; User requests message events + (setq jabber-events-requested + ;; There might be empty strings in the XML data, + ;; which car chokes on. Having nil values in + ;; the list won't hurt, therefore car-safe. + (mapcar #'car-safe + (jabber-xml-node-children x))) + (setq jabber-events-last-id (jabber-xml-get-attribute + xml-data 'id)) + + ;; Send notifications we already know about + (flet ((send-notification + (type) + (jabber-send-sexp + `(message + ((to . ,(jabber-xml-get-attribute xml-data 'from))) + (x ((xmlns . "jabber:x:event")) + (,type) + (id () ,jabber-events-last-id)))))) + ;; Send delivery confirmation if appropriate + (when (and jabber-events-confirm-delivered + (memq 'delivered jabber-events-requested)) + (send-notification 'delivered) + (setq jabber-events-delivery-confirmed t)) + + ;; Send display confirmation if appropriate + (when (and jabber-events-confirm-displayed + (get-buffer-window (current-buffer) 'visible) + (memq 'displayed jabber-events-requested)) + (send-notification 'displayed) + (setq jabber-events-display-confirmed t)) + + ;; Set up hooks for composition notification + (when (and jabber-events-confirm-composing + (memq 'composing jabber-events-requested)) + (add-hook 'post-command-hook 'jabber-events-after-change + nil t)))) + ;; So it has no body. If it's a message event, + ;; the <x/> node should be the only child of the + ;; message, and it should contain an <id/> node. + ;; We check the latter. + (when (and x (jabber-xml-get-children x 'id)) + ;; Currently we don't care about the <id/> node. + + ;; There's only one node except for the id. + (unless + (dolist (possible-node '(offline delivered displayed)) + (when (jabber-xml-get-children x possible-node) + (setq jabber-events-arrived possible-node) + (jabber-events-update-message) + (return t))) + ;; Or maybe even zero, which is a negative composing node. + (setq jabber-events-composing-p + (not (null (jabber-xml-get-children x 'composing)))) + (jabber-events-update-message)))))) + +(provide 'jabber-events) +;; arch-tag: 7b6e61fe-a9b3-11d9-afca-000a95c2fcd0 diff --git a/jabber-export.el b/jabber-export.el new file mode 100644 index 0000000..a71f729 --- /dev/null +++ b/jabber-export.el @@ -0,0 +1,234 @@ +;;; jabber-export.el --- export Jabber roster to file + +;; Copyright (C) 2005 Magnus Henoch + +;; Author: Magnus Henoch <mange@freemail.hu> + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +(require 'cl) + +(defvar jabber-export-roster-widget nil) + +(defvar jabber-import-subscription-p-widget nil) + +(defun jabber-export-roster (&optional roster) + "Create buffer from which roster can be exported to a file." + (interactive) + (with-current-buffer (get-buffer-create "Export roster") + (jabber-init-widget-buffer nil) + + (widget-insert (jabber-propertize "Export roster\n" + 'face 'jabber-title-large)) + (widget-insert "You are about to save your roster to a file. Here +you can edit it before saving. Changes done here will +not affect your actual roster. + +") + + (widget-create 'push-button :notify #'jabber-export-save "Save to file") + (widget-insert " ") + (widget-create 'push-button :notify #'jabber-export-remove-regexp "Remove by regexp") + (widget-insert "\n\n") + (make-local-variable 'jabber-export-roster-widget) + + (jabber-export-display (or roster (jabber-roster-to-sexp *jabber-roster*))) + + (widget-setup) + (widget-minor-mode 1) + (goto-char (point-min)) + (switch-to-buffer (current-buffer)))) + +(defun jabber-import-roster (file) + "Create buffer for roster import from FILE." + (interactive "fImport roster from file: ") + (let ((roster + (with-temp-buffer + (let ((coding-system-for-read 'utf-8)) + (jabber-roster-xml-to-sexp + (car (xml-parse-file file))))))) + (with-current-buffer (get-buffer-create "Import roster") + (jabber-init-widget-buffer nil) + + (widget-insert (jabber-propertize "Import roster\n" + 'face 'jabber-title-large)) + (widget-insert "You are about to import the contacts below to your roster. + +") + + (make-local-variable 'jabber-import-subscription-p-widget) + (setq jabber-import-subscription-p-widget + (widget-create 'checkbox)) + (widget-insert "Adjust subscriptions\n") + + (widget-create 'push-button :notify #'jabber-import-doit "Import to roster") + (widget-insert " ") + (widget-create 'push-button :notify #'jabber-export-remove-regexp "Remove by regexp") + (widget-insert "\n\n") + (make-local-variable 'jabber-export-roster-widget) + + (jabber-export-display roster) + + (widget-setup) + (widget-minor-mode 1) + (goto-char (point-min)) + (switch-to-buffer (current-buffer))))) + +(defun jabber-export-remove-regexp (&rest ignore) + (let* ((value (widget-value jabber-export-roster-widget)) + (length-before (length value)) + (regexp (read-string "Remove JIDs matching regexp: "))) + (setq value (delete-if + #'(lambda (a) + (string-match regexp (nth 0 a))) + value)) + (widget-value-set jabber-export-roster-widget value) + (widget-setup) + (message "%d items removed" (- length-before (length value))))) + +(defun jabber-export-save (&rest ignore) + "Export roster to file." + (let ((items (mapcar #'jabber-roster-sexp-to-xml (widget-value jabber-export-roster-widget))) + (coding-system-for-write 'utf-8)) + (with-temp-file (read-file-name "Export roster to file: ") + (insert "<iq xmlns='jabber:client'><query xmlns='jabber:iq:roster'>\n") + (dolist (item items) + (insert (jabber-sexp2xml item) "\n")) + (insert "</query></iq>\n")) + (message "Roster saved"))) + +(defun jabber-import-doit (&rest ignore) + "Import roster being edited in widget." + (let (roster-delta) + (dolist (n (widget-value jabber-export-roster-widget)) + (let* ((jid (nth 0 n)) + (name (and (not (zerop (length (nth 1 n)))) + (nth 1 n))) + (subscription (nth 2 n)) + (groups (nth 3 n)) + (jid-symbol (jabber-jid-symbol jid)) + (in-roster-p (memq jid-symbol *jabber-roster*)) + (jid-name (and in-roster-p (get jid-symbol 'name))) + (jid-subscription (and in-roster-p (get jid-symbol 'subscription))) + (jid-groups (and in-roster-p (get jid-symbol 'groups)))) + ;; Do we need to change the roster? + (when (or + ;; If the contact is not in the roster already, + (not in-roster-p) + ;; or if the import introduces a name, + (and name (not jid-name)) + ;; or changes a name, + (and name jid-name (not (string= name jid-name))) + ;; or introduces new groups. + (set-difference groups jid-groups :test #'string=)) + (push (jabber-roster-sexp-to-xml + (list jid (or name jid-name) nil (union groups jid-groups :test #'string=)) + t) + roster-delta)) + ;; And adujst subscription. + (when (widget-value jabber-import-subscription-p-widget) + (let ((want-to (member subscription '("to" "both"))) + (want-from (member subscription '("from" "both"))) + (have-to (member jid-subscription '("to" "both"))) + (have-from (member jid-subscription '("from" "both")))) + (flet ((request-subscription + (type) + (jabber-send-sexp `(presence ((to . ,jid) + (type . ,type)))))) + (cond + ((and want-to (not have-to)) + (request-subscription "subscribe")) + ((and have-to (not want-to)) + (request-subscription "unsubscribe"))) + (cond + ((and want-from (not have-from)) + ;; not much to do here + ) + ((and have-from (not want-from)) + (request-subscription "unsubscribed")))))))) + (when roster-delta + (jabber-send-iq nil "set" + `(query ((xmlns . "jabber:iq:roster")) ,@roster-delta) + #'jabber-report-success "Roster import" + #'jabber-report-success "Roster import")))) + +(defun jabber-roster-to-sexp (roster) + "Convert ROSTER to simpler sexp format. +Return a list, where each item is a vector: +\[jid name subscription groups] +where groups is a list of strings." + (mapcar + #'(lambda (n) + (list + (symbol-name n) + (or (get n 'name) "") + (get n 'subscription) + (get n 'groups))) + roster)) + +(defun jabber-roster-sexp-to-xml (sexp &optional omit-subscription) + "Convert SEXP to XML format. +Return an XML node." + `(item ((jid . ,(nth 0 sexp)) + ,@(let ((name (nth 1 sexp))) + (unless (zerop (length name)) + `((name . ,name)))) + ,@(unless omit-subscription + `((subscription . ,(nth 2 sexp))))) + ,@(mapcar + #'(lambda (g) + (list 'group nil g)) + (nth 3 sexp)))) + +(defun jabber-roster-xml-to-sexp (xml-data) + "Convert XML-DATA to simpler sexp format. +XML-DATA is an <iq> node with a <query xmlns='jabber:iq:roster'> child. +See `jabber-roster-to-sexp' for description of output format." + (assert (eq (jabber-xml-node-name xml-data) 'iq)) + (let ((query (car (jabber-xml-get-children xml-data 'query)))) + (assert query) + (mapcar + #'(lambda (n) + (list + (jabber-xml-get-attribute n 'jid) + (or (jabber-xml-get-attribute n 'name) "") + (jabber-xml-get-attribute n 'subscription) + (mapcar + #'(lambda (g) + (car (jabber-xml-node-children g))) + (jabber-xml-get-children n 'group)))) + (jabber-xml-get-children query 'item)))) + +(defun jabber-export-display (roster) + (setq jabber-export-roster-widget + (widget-create + '(repeat + :tag "Roster" + (list :format "%v" + (string :tag "JID") + (string :tag "Name") + (choice :tag "Subscription" + (const "none") + (const "both") + (const "to") + (const "from")) + (repeat :tag "Groups" + (string :tag "Group")))) + :value roster))) + +(provide 'jabber-export) + +;;; arch-tag: 9c6b94a9-290a-4c0f-9286-72bd9c1fb8a3 diff --git a/jabber-feature-neg.el b/jabber-feature-neg.el new file mode 100644 index 0000000..1fb8853 --- /dev/null +++ b/jabber-feature-neg.el @@ -0,0 +1,125 @@ +;; jabber-feature-neg.el - Feature Negotiation by JEP-0020 + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-disco) +(require 'cl) + +(add-to-list 'jabber-advertised-features "http://jabber.org/protocol/feature-neg") + +(defun jabber-fn-parse (xml-data type) + "Parse a Feature Negotiation request, return alist representation. +XML-DATA should have one child element, <x/>, in the jabber:x:data +namespace. + +TYPE is either 'request or 'response. + +Returned alist has field name as key, and value is a list of offered +alternatives." + (let ((x (car (jabber-xml-get-children xml-data 'x)))) + (unless (and x + (string= (jabber-xml-get-attribute x 'xmlns) "jabber:x:data")) + (jabber-signal-error "modify" 'bad-request "Malformed Feature Negotiation")) + + (let (alist + (fields (jabber-xml-get-children x 'field))) + (dolist (field fields) + (let ((var (jabber-xml-get-attribute field 'var)) + (value (car (jabber-xml-get-children field 'value))) + (options (jabber-xml-get-children field 'option))) + (setq alist (cons + (cons var + (cond + ((eq type 'request) + (mapcar #'(lambda (option) + (car (jabber-xml-node-children + (car (jabber-xml-get-children + option 'value))))) + options)) + ((eq type 'response) + (jabber-xml-node-children value)) + (t + (error "Incorrect Feature Negotiation type: %s" type)))) + alist)))) + ;; return alist + alist))) + +(defun jabber-fn-encode (alist type) + "Transform a feature alist into an <x/> node int the jabber:x:data namespace. +Note that this is not the reverse of `jabber-fn-parse'. + +TYPE is either 'request or 'response." + (let ((requestp (eq type 'request))) + `(x ((xmlns . "jabber:x:data") + (type . ,(if requestp "form" "submit"))) + ,@(mapcar #'(lambda (field) + `(field + ((type . "list-single") + (var . ,(car field))) + ,@(if requestp + (mapcar + #'(lambda (option) + `(option nil (value nil ,option))) + (cdr field)) + (list `(value nil ,(cadr field)))))) + alist)))) + +(defun jabber-fn-intersection (mine theirs) + "Find values acceptable to both parties. + +MINE and THEIRS are alists, as returned by `jabber-fn-parse'. + +An alist is returned, where the keys are the negotiated variables, +and the values are lists containing the preferred option. If +negotiation is impossible, an error is signalled. The errors are as +specified in JEP-0020, and not necessarily the ones of higher-level +protocols." + + (let ((vars (mapcar #'car mine)) + (their-vars (mapcar #'car theirs))) + + ;; are the same variables being negotiated? + (sort vars 'string-lessp) + (sort their-vars 'string-lessp) + (let ((mine-but-not-theirs (set-difference vars their-vars :test 'string=)) + (theirs-but-not-mine (set-difference their-vars vars :test 'string=))) + (when mine-but-not-theirs + (jabber-signal-error "modify" 'not-acceptable (car mine-but-not-theirs))) + (when theirs-but-not-mine + (jabber-signal-error "cancel" 'feature-not-implemented (car theirs-but-not-mine)))) + + (let (alist) + (dolist (var vars) + (let ((my-options (cdr (assoc var mine))) + (their-options (cdr (assoc var theirs)))) + (let ((common-options (intersection my-options their-options :test 'string=))) + (if common-options + ;; we have a match; but which one to use? + ;; the first one will probably work + (setq alist + (cons (list var (car common-options)) + alist)) + ;; no match + (jabber-signal-error "modify" 'not-acceptable var))))) + alist))) + +(provide 'jabber-feature-neg) + +;;; arch-tag: 65b2cdcc-7a5f-476b-a613-84ec8e590186 diff --git a/jabber-festival.el b/jabber-festival.el new file mode 100644 index 0000000..23b0b96 --- /dev/null +++ b/jabber-festival.el @@ -0,0 +1,33 @@ +;;; jabber-festival.el --- Festival alert hooks + +;; Copyright (C) 2005 Magnus Henoch + +;; 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, 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 GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +(condition-case e + (progn + ;; Most people don't have Festival, so this will often fail + (require 'festival) + (define-jabber-alert festival "Voice messages through Festival" + 'festival-say-string)) + (error nil)) + +(provide 'jabber-festival) +;; arch-tag: 8922D096-5D07-11D9-B4C2-000A95C2FCD0 + + diff --git a/jabber-ft-client.el b/jabber-ft-client.el new file mode 100644 index 0000000..c3b9434 --- /dev/null +++ b/jabber-ft-client.el @@ -0,0 +1,54 @@ +;; jabber-ft-client.el - send file transfer requests, by JEP-0096 + +;; Copyright (C) 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-si-client) +(require 'jabber-util) + +(defun jabber-ft-send (jid filename desc) + "Attempt to send FILENAME to JID." + (interactive (list (jabber-read-jid-completing "Send file to: ") + (read-file-name "Send which file: " nil nil t) + (jabber-read-with-input-method "Description (optional): "))) + (if (zerop (length desc)) (setq desc nil)) + (setq filename (expand-file-name filename)) + (access-file filename "Couldn't open file") + + (let* ((attributes (file-attributes filename)) + (size (nth 7 attributes)) + (date (nth 5 attributes))) + (jabber-si-initiate jid "http://jabber.org/protocol/si/profile/file-transfer" + `(file ((xmlns . "http://jabber.org/protocol/si/profile/file-transfer") + (name . ,(file-name-nondirectory filename)) + (size . ,size) + (date . ,(jabber-encode-time date))) + (desc () ,desc)) + `(lambda (jid sid send-data-function) + (jabber-ft-do-send jid sid send-data-function ,filename))))) + +(defun jabber-ft-do-send (jid sid send-data-function filename) + (with-temp-buffer + (insert-file-contents-literally filename) + + ;; Ever heard of buffering? + (funcall send-data-function (buffer-string)) + (message "File transfer completed"))) + +(provide 'jabber-ft-client) +;;; arch-tag: fba686d5-37b5-4165-86c5-49b76fa0ea6e diff --git a/jabber-ft-server.el b/jabber-ft-server.el new file mode 100644 index 0000000..09405f9 --- /dev/null +++ b/jabber-ft-server.el @@ -0,0 +1,104 @@ +;; jabber-ft-server.el - handle incoming file transfers, by JEP-0096 + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-si-server) +(require 'jabber-util) + +(defvar jabber-ft-sessions nil + "Alist, where keys are (sid jid), and values are buffers of the files.") + +(defvar jabber-ft-size nil + "Size of the file that is being downloaded") + +(add-to-list 'jabber-advertised-features "http://jabber.org/protocol/si/profile/file-transfer") + +(add-to-list 'jabber-si-profiles + (list "http://jabber.org/protocol/si/profile/file-transfer" + 'jabber-ft-accept + 'jabber-ft-data)) + +(defun jabber-ft-accept (xml-data) + "Receive IQ stanza containing file transfer request, ask user" + (let* ((from (jabber-xml-get-attribute xml-data 'from)) + (query (jabber-iq-query xml-data)) + (si-id (jabber-xml-get-attribute query 'id)) + ;; TODO: check namespace + (file (car (jabber-xml-get-children query 'file))) + (name (jabber-xml-get-attribute file 'name)) + (size (jabber-xml-get-attribute file 'size)) + (date (jabber-xml-get-attribute file 'date)) + (md5-hash (jabber-xml-get-attribute file 'hash)) + (desc (car (jabber-xml-node-children + (car (jabber-xml-get-children file 'desc))))) + (range (car (jabber-xml-get-children file 'range)))) + (unless (and name size) + ;; both name and size must be present + (jabber-signal-error "modify" 'bad-request)) + + (let ((question (format + "%s is sending you the file %s (%s bytes).%s Accept? " + (jabber-jid-displayname from) + name + size + (if (not (zerop (length desc))) + (concat " Description: '" desc "'") + "")))) + (unless (yes-or-no-p question) + (jabber-signal-error "cancel" 'forbidden))) + + ;; default is to save with given name, in current directory. + ;; maybe that's bad; maybe should be customizable. + (let* ((file-name (read-file-name "Download to: " nil nil nil name)) + (buffer (create-file-buffer file-name))) + (message "Starting download of %s..." (file-name-nondirectory file-name)) + (with-current-buffer buffer + (setq buffer-file-coding-system 'binary) + ;; For Emacs, switch buffer to unibyte _before_ anything goes into it, + ;; otherwise binary files are corrupted. For XEmacs, it isn't needed, + ;; and it also doesn't have set-buffer-multibyte. + (if (fboundp 'set-buffer-multibyte) + (set-buffer-multibyte nil)) + (set-visited-file-name file-name t) + (make-local-variable 'jabber-ft-size) + (setq jabber-ft-size (string-to-number size))) + (add-to-list 'jabber-ft-sessions + (cons (list si-id from) buffer))) + + ;; to support range, return something sensible here + nil)) + +(defun jabber-ft-data (jid sid data) + "Receive chunk of transferred file." + (let ((buffer (cdr (assoc (list sid jid) jabber-ft-sessions)))) + (with-current-buffer buffer + ;; If data is nil, there is no more data. + ;; But maybe the remote entity doesn't close the stream - + ;; then we have to keep track of file size to know when to stop. + (when data + (insert data)) + (if (and data (< (buffer-size) jabber-ft-size)) + t + (basic-save-buffer) + (message "%s downloaded" (file-name-nondirectory buffer-file-name)))))) + +(provide 'jabber-ft-server) + +;;; arch-tag: 334adcff-6210-496e-8382-8f49ae0248a1 diff --git a/jabber-history.el b/jabber-history.el new file mode 100644 index 0000000..669fe2a --- /dev/null +++ b/jabber-history.el @@ -0,0 +1,244 @@ +;; jabber-history.el - recording message history + +;; Copyright (C) 2004 - Mathias Dahl +;; Copyright (C) 2004 - Magnus Henoch - mange@freemail.hu + +;; 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 + +;;; Log format: +;; Each message is on one separate line, represented as a vector with +;; five elements. The first element is time encoded according to +;; JEP-0082. The second element is direction, "in" or "out". +;; The third element is the sender, "me" or a JID. The fourth +;; element is the recipient. The fifth element is the text +;; of the message. + +;; FIXME: when rotation is enabled, jabber-history-query won't look +;; for older history files if the current history file doesn't contain +;; enough backlog entries. + +(require 'jabber-core) + +(defgroup jabber-history nil "Customization options for Emacs +Jabber history files." + :group 'jabber) + +(defcustom jabber-history-enabled nil + "Non-nil means message logging is enabled." + :type 'boolean + :group 'jabber-history) + +(defcustom jabber-use-global-history t + "Indicate whether Emacs Jabber should use a global file for + store messages. If non-nil, jabber-global-history-filename is + used, otherwise, messages are stored in per-user files under + the jabber-history-dir directory." + :type 'boolean + :group 'jabber-history) + +(defcustom jabber-history-dir "~/.emacs-jabber" + "Base directory where per-contact history files are stored. + Used only when jabber-use-global-history is not true." + :type 'directory + :group 'jabber-history) + +(defcustom jabber-global-history-filename "~/.jabber_global_message_log" + "Global file where all messages are logged. Used when + jabber-use-global-history is non-nil." + :type 'file + :group 'jabber-history) + +(defcustom jabber-history-enable-rotation nil + "Whether history files should be renamed when reach + jabber-history-size-limit kilobytes. If nil, history files + will grow indefinitely, otherwise they'll be renamed to + <history-file>-<number>, where <number> is 1 or the smallest + number after the last rotation." + :type 'boolean + :group 'jabber-history) + +(defcustom jabber-history-size-limit 1024 + "Maximum history file size in kilobytes. When history file + reaches this limit, it is renamed to <history-file>-<number>, + where <number> is 1 or the smallest number after the last + rotation." + :type 'integer + :group 'jabber-history) + + +(defun jabber-rotate-history-p (history-file) + "Return true if HISTORY-FILE should be rotated." + (when (and jabber-history-enable-rotation + (file-exists-p history-file)) + (> (/ (nth 7 (file-attributes history-file)) 1024) + jabber-history-size-limit))) + +(defun jabber-history-rotate (history-file &optional try) + "Rename HISTORY-FILE to HISTORY-FILE-TRY." + (let ((suffix (number-to-string (or try 1)))) + (if (file-exists-p (concat history-file "-" suffix)) + (jabber-history-rotate history-file (if try (1+ try) 1)) + (rename-file history-file (concat history-file "-" suffix))))) + +(add-to-list 'jabber-message-chain 'jabber-message-history) +(defun jabber-message-history (xml-data) + "Log message to log file." + (when (and (not jabber-use-global-history) + (not (file-directory-p jabber-history-dir))) + (make-directory jabber-history-dir)) + (if (and jabber-history-enabled (not (jabber-muc-message-p xml-data))) + (let ((from (jabber-xml-get-attribute xml-data 'from)) + (text (car (jabber-xml-node-children + (car (jabber-xml-get-children xml-data 'body))))) + (timestamp (car (delq nil (mapcar 'jabber-x-delay (jabber-xml-get-children xml-data 'x)))))) + (when (and from text) + (jabber-history-log-message "in" from nil text timestamp))))) + +(add-hook 'jabber-chat-send-hooks 'jabber-history-send-hook) + +(defun jabber-history-send-hook (body id) + "Log outgoing message to log file." + (when (and (not jabber-use-global-history) + (not (file-directory-p jabber-history-dir))) + (make-directory jabber-history-dir)) + ;; This function is called from a chat buffer, so jabber-chatting-with + ;; contains the desired value. + (if jabber-history-enabled + (jabber-history-log-message "out" nil jabber-chatting-with body (current-time)))) + +(defun jabber-history-filename (contact) + "Return a history filename for CONTACT if the per-user file + loggin strategy is used or the global history filename." + (if jabber-use-global-history + jabber-global-history-filename + (concat jabber-history-dir "/" (jabber-jid-user contact)))) + +(defun jabber-history-log-message (direction from to body timestamp) + "Log a message" + (with-temp-buffer + ;; Remove properties + (set-text-properties 0 (length body) nil body) + ;; Encode text as Lisp string - get decoding for free + (setq body (prin1-to-string body)) + ;; Encode LF and CR + (while (string-match "\n" body) + (setq body (replace-match "\\n" nil t body nil))) + (while (string-match "\r" body) + (setq body (replace-match "\\r" nil t body nil))) + (insert (format "[\"%s\" \"%s\" \"%s\" \"%s\" %s]\n" + (jabber-encode-time (or timestamp (current-time))) + (or direction + "in") + (or from + "me") + (or to + "me") + body)) + (let ((coding-system-for-write 'utf-8) + (history-file (jabber-history-filename (or from to)))) + (when (and (not jabber-use-global-history) + (not (file-directory-p jabber-history-dir))) + (make-directory jabber-history-dir)) + (when (jabber-rotate-history-p history-file) + (jabber-history-rotate history-file)) + (write-region (point-min) (point-max) history-file t 'quiet)))) + +(defun jabber-history-query (start-time + end-time + number + direction + jid-regexp + history-file) + "Return a list of vectors, one for each message matching the criteria. +START-TIME and END-TIME are floats as obtained from `float-time'. +Either or both may be nil, meaning no restriction. +NUMBER is the maximum number of messages to return, or t for +unlimited. +DIRECTION is either \"in\" or \"out\", or t for no limit on direction. +JID-REGEXP is a regexp which must match the JID. +HISTORY-FILE is the file in which to search. + +Currently jabber-history-query performs a linear search from the end +of the log file." + (when (file-readable-p history-file) + (with-temp-buffer + (let ((coding-system-for-read 'utf-8)) + (insert-file-contents history-file)) + (let (collected current-line) + (goto-char (point-max)) + (catch 'beginning-of-file + (while (progn + (backward-sexp) + (setq current-line (car (read-from-string + (buffer-substring + (point) + (save-excursion + (forward-sexp) + (point)))))) + (and (or (null start-time) + (> (jabber-float-time (jabber-parse-time + (aref current-line 0))) + start-time)) + (or (eq number t) + (< (length collected) number)))) + (if (and (or (eq direction t) + (string= direction (aref current-line 1))) + (or (null end-time) + (> end-time (jabber-float-time (jabber-parse-time + (aref current-line 0))))) + (string-match + jid-regexp + (car + (remove "me" + (list (aref current-line 2) + (aref current-line 3)))))) + (push current-line collected)) + (when (bobp) + (throw 'beginning-of-file nil)))) + collected)))) + +(defcustom jabber-backlog-days 3.0 + "Age limit on messages in chat buffer backlog, in days" + :group 'jabber + :type '(choice (number :tag "Number of days") + (const :tag "No limit" nil))) + +(defcustom jabber-backlog-number 10 + "Maximum number of messages in chat buffer backlog" + :group 'jabber + :type 'integer) + +(defun jabber-history-backlog (jid &optional before) + "Fetch context from previous chats with JID. +Return a list of history entries (vectors), limited by +`jabber-backlog-days' and `jabber-backlog-number'. +If BEFORE is non-nil, it should be a float-time after which +no entries will be fetched. `jabber-backlog-days' still +applies, though." + (interactive) + (jabber-history-query + (and jabber-backlog-days + (- (jabber-float-time) (* jabber-backlog-days 86400.0))) + before + jabber-backlog-number + t ; both incoming and outgoing + (concat "^" (regexp-quote (jabber-jid-user jid)) "\\(/.*\\)?$") + (jabber-history-filename jid))) + +(provide 'jabber-history) + +;; arch-tag: 0AA0C235-3FC0-11D9-9FE7-000A95C2FCD0 diff --git a/jabber-iq.el b/jabber-iq.el new file mode 100644 index 0000000..a165517 --- /dev/null +++ b/jabber-iq.el @@ -0,0 +1,177 @@ +;; jabber-iq.el - infoquery functions + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-util) +(require 'jabber-keymap) + +(defvar *jabber-open-info-queries* nil + "an alist of open query id and their callback functions") + +(defvar jabber-iq-get-xmlns-alist nil + "Mapping from XML namespace to handler for IQ GET requests.") + +(defvar jabber-iq-set-xmlns-alist nil + "Mapping from XML namespace to handler for IQ SET requests.") + +(defvar jabber-browse-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map jabber-common-keymap) + (define-key map [mouse-2] 'jabber-popup-combined-menu) + map)) + +(defcustom jabber-browse-mode-hook nil + "Hook run when entering Browse mode." + :group 'jabber + :type 'hook) + +(defun jabber-browse-mode () +"\\{jabber-browse-mode-map}" + (kill-all-local-variables) + (setq major-mode 'jabber-browse-mode + mode-name "jabber-browse") + (use-local-map jabber-browse-mode-map) + (setq buffer-read-only t) + (if (fboundp 'run-mode-hooks) + (run-mode-hooks 'jabber-browse-mode-hook) + (run-hooks 'jabber-browse-mode-hook))) + +(put 'jabber-browse-mode 'mode-class 'special) + +(add-to-list 'jabber-iq-chain 'jabber-process-iq) +(defun jabber-process-iq (xml-data) + "process an incoming iq stanza" + (let* ((id (jabber-xml-get-attribute xml-data 'id)) + (type (jabber-xml-get-attribute xml-data 'type)) + (from (jabber-xml-get-attribute xml-data 'from)) + (query (jabber-iq-query xml-data)) + (callback (assoc id *jabber-open-info-queries*))) + (cond + ;; if type is "result" or "error", this is a response to a query we sent. + ((or (string= type "result") + (string= type "error")) + (let ((callback-cons (nth (cdr (assoc type '(("result" . 0) + ("error" . 1)))) (cdr callback)))) + (if (consp callback-cons) + (funcall (car callback-cons) xml-data (cdr callback-cons)))) + (setq *jabber-open-info-queries* (delq callback *jabber-open-info-queries*))) + + ;; if type is "get" or "set", correct action depends on namespace of request. + ((and (listp query) + (or (string= type "get") + (string= type "set"))) + (let* ((which-alist (eval (cdr (assoc type + (list + (cons "get" 'jabber-iq-get-xmlns-alist) + (cons "set" 'jabber-iq-set-xmlns-alist)))))) + (handler (cdr (assoc (jabber-xml-get-attribute query 'xmlns) which-alist)))) + (if handler + (condition-case error-var + (funcall handler xml-data) + (jabber-error + (apply 'jabber-send-iq-error from id query (cdr error-var))) + (error (jabber-send-iq-error from id query "wait" 'internal-server-error (error-message-string error-var)))) + (jabber-send-iq-error from id query "cancel" 'feature-not-implemented))))))) + +(defun jabber-send-iq (to type query success-callback success-closure-data + error-callback error-closure-data &optional result-id) + "Send an iq stanza to the specified entity, and optionally set up a callback. +TO is the addressee. +TYPE is one of \"get\", \"set\", \"result\" or \"error\". +QUERY is a list containing the child of the iq node in the format `jabber-sexp2xml' +accepts. +SUCCESS-CALLBACK is the function to be called when a successful result arrives. +SUCCESS-CLOSURE-DATA is the second argument to SUCCESS-CALLBACK. +ERROR-CALLBACK is the function to be called when an error arrives. +ERROR-CLOSURE-DATA is the second argument to ERROR-CALLBACK. +RESULT-ID is the id to be used for a response to a received iq message. +`jabber-report-success' and `jabber-process-data' are common callbacks." + (let ((id (or result-id (apply 'format "emacs-iq-%d.%d.%d" (current-time))))) + (if (or success-callback error-callback) + (setq *jabber-open-info-queries* (cons (list id + (cons success-callback success-closure-data) + (cons error-callback error-closure-data)) + + *jabber-open-info-queries*))) + (jabber-send-sexp (list 'iq (append + (if to (list (cons 'to to))) + (list (cons 'type type)) + (list (cons 'id id))) + query)))) + +(defun jabber-send-iq-error (to id original-query error-type condition + &optional text app-specific) + "Send an error iq stanza to the specified entity in response to a +previously sent iq stanza. +TO is the addressee. +ID is the id of the iq stanza that caused the error. +ORIGINAL-QUERY is the original query, which should be included in the +error, or nil. +ERROR-TYPE is one of \"cancel\", \"continue\", \"modify\", \"auth\" +and \"wait\". +CONDITION is a symbol denoting a defined XMPP condition. +TEXT is a string to be sent in the error message, or nil for no text. +APP-SPECIFIC is a list of extra XML tags. + +See section 9.3 of XMPP Core." + (jabber-send-sexp `(iq ((to . ,to) + (type . "error") + (id . ,id)) + ,original-query + (error ((type . ,error-type)) + (,condition ((xmlns . "urn:ietf:params:xml:ns:xmpp-stanzas"))) + ,(if text + `(text ((xmlns . "urn:ietf:params:xml:ns:xmpp-stanzas")) + ,text)) + ,@app-specific)))) + +(defun jabber-process-data (xml-data closure-data) + "Process random results from various requests." + (let ((from (or (jabber-xml-get-attribute xml-data 'from) jabber-server)) + (xmlns (jabber-iq-xmlns xml-data)) + (type (jabber-xml-get-attribute xml-data 'type))) + (with-current-buffer (get-buffer-create (concat "*-jabber-browse-:-" from "-*")) + (if (not (eq major-mode 'jabber-browse-mode)) + (jabber-browse-mode)) + + (setq buffer-read-only nil) + (goto-char (point-max)) + + (insert (jabber-propertize from + 'face 'jabber-title-large) "\n\n") + + ;; If closure-data is a function, call it. If it is a string, + ;; output it along with a description of the error. For other + ;; values (e.g. nil), just dump the XML. + (cond + ((functionp closure-data) + (funcall closure-data xml-data)) + ((stringp closure-data) + (insert closure-data ": " (jabber-parse-error (jabber-iq-error xml-data)) "\n\n")) + (t + (insert (format "%S\n\n" xml-data)))) + + (dolist (hook '(jabber-info-message-hooks jabber-alert-info-message-hooks)) + (run-hook-with-args hook 'browse (current-buffer) (funcall jabber-alert-info-message-function 'browse (current-buffer))))))) + +(provide 'jabber-iq) + +;;; arch-tag: 5585dfa3-b59a-42ee-9292-803652c85e26 diff --git a/jabber-keepalive.el b/jabber-keepalive.el new file mode 100644 index 0000000..7c43ecb --- /dev/null +++ b/jabber-keepalive.el @@ -0,0 +1,95 @@ +;; jabber-keepalive.el - try to detect lost connection + +;; Copyright (C) 2004 - Magnus Henoch - mange@freemail.hu + +;; 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 + +;;; These keepalive functions send a jabber:iq:time request to the +;;; server every X minutes, and considers the connection broken if +;;; they get no answer within Y seconds. + +(defgroup jabber-keepalive nil + "Keepalive functions try to detect lost connection" + :group 'jabber) + +(defcustom jabber-keepalive-interval 600 + "Interval in seconds between connection checks." + :type 'integer + :group 'jabber-keepalive) + +(defcustom jabber-keepalive-timeout 20 + "Seconds to wait for response from server." + :type 'integer + :group 'jabber-keepalive) + +(defvar jabber-keepalive-timer nil + "Timer object for keepalive function") + +(defvar jabber-keepalive-timeout-timer nil + "Timer object for keepalive timeout function") + +(defun jabber-keepalive-start () + "Activate keepalive" + (interactive) + + (when jabber-keepalive-timer + (jabber-keepalive-stop)) + + (setq jabber-keepalive-timer + (run-with-timer 5 + jabber-keepalive-interval + 'jabber-keepalive-do)) + (add-hook 'jabber-post-disconnect-hook 'jabber-keepalive-stop)) + +(defun jabber-keepalive-stop() + "Deactivate keepalive" + (interactive) + + (when jabber-keepalive-timer + (jabber-cancel-timer jabber-keepalive-timer) + (setq jabber-keepalive-timer nil))) + +(defun jabber-keepalive-do () + (message "%s: sending keepalive packet" (current-time-string)) + (setq jabber-keepalive-timeout-timer + (run-with-timer jabber-keepalive-timeout + nil + 'jabber-keepalive-timeout)) + + ;; Whether we get an error or not is not interesting. + ;; Getting a response at all is. + (jabber-send-iq jabber-server "get" + '(query ((xmlns . "jabber:iq:time"))) + 'jabber-keepalive-got-response nil + 'jabber-keepalive-got-response nil)) + +(defun jabber-keepalive-got-response (&rest args) + (message "%s: got keepalive response" (current-time-string)) + (jabber-cancel-timer jabber-keepalive-timeout-timer) + (setq jabber-keepalive-timeout-timer nil)) + +(defun jabber-keepalive-timeout () + (message "%s: keepalive timeout, connection considered lost" (current-time-string)) + (jabber-cancel-timer jabber-keepalive-timer) + (setq jabber-keepalive-timer nil) + + (run-hooks jabber-lost-connection-hook) + (jabber-disconnect)) + +(provide 'jabber-keepalive) + +;;; arch-tag: d19ca743-75a1-475f-9217-83bd18012146 diff --git a/jabber-keymap.el b/jabber-keymap.el new file mode 100644 index 0000000..f8b50f4 --- /dev/null +++ b/jabber-keymap.el @@ -0,0 +1,58 @@ +;; jabber-keymap.el - common keymap for many modes + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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 + + +;; button.el was introduced in Emacs 22 +(condition-case e + (require 'button) + (error nil)) + +(defvar jabber-common-keymap + (let ((map (make-sparse-keymap))) + (define-key map "\C-c\C-c" 'jabber-popup-chat-menu) + (define-key map "\C-c\C-r" 'jabber-popup-roster-menu) + (define-key map "\C-c\C-i" 'jabber-popup-info-menu) + (define-key map "\C-c\C-m" 'jabber-popup-muc-menu) + (define-key map "\C-c\C-s" 'jabber-popup-service-menu) + ;; note that {forward,backward}-button are not autoloaded. + ;; thus the `require' above. + (when (fboundp 'forward-button) + (define-key map [?\t] 'forward-button) + (define-key map [backtab] 'backward-button)) + map)) + +(defvar jabber-global-keymap + (let ((map (make-sparse-keymap))) + (define-key map "\C-c" 'jabber-connect) + (define-key map "\C-d" 'jabber-disconnect) + (define-key map "\C-r" 'jabber-switch-to-roster-buffer) + (define-key map "\C-j" 'jabber-chat-with) + (define-key map "\C-a" 'jabber-send-away-presence) + (define-key map "\C-o" 'jabber-send-default-presence) + (define-key map "\C-x" 'jabber-send-xa-presence) + map) + "Global Jabber keymap (usually under C-x C-j)") + +(define-key ctl-x-map "\C-j" jabber-global-keymap) + +(provide 'jabber-keymap) + +;;; arch-tag: 22a9993d-a4a7-40ef-a025-7cff6c3f5587 diff --git a/jabber-logon.el b/jabber-logon.el new file mode 100644 index 0000000..f87aceb --- /dev/null +++ b/jabber-logon.el @@ -0,0 +1,88 @@ +;; jabber-logon.el - logon functions + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-xml) +(require 'jabber-util) +;; sha1-el is known under two names +(condition-case e + (require 'sha1) + (error (require 'sha1-el))) + +(defun jabber-get-auth (to) + "Send IQ get request in namespace \"jabber:iq:auth\"." + (jabber-send-iq to + "get" + `(query ((xmlns . "jabber:iq:auth")) + (username () ,jabber-username)) + #'jabber-do-logon nil + #'jabber-report-success "Impossible error - auth field request")) + +(defun jabber-do-logon (xml-data closure-data) + "send username and password in logon attempt" + (cond + ((string= (jabber-xml-get-attribute xml-data 'type) "result") + (let (auth) + (if (jabber-xml-get-children (jabber-iq-query xml-data) 'digest) + ;; SHA1 digest passwords allowed + (let ((passwd (jabber-read-passwd))) + (if passwd + (setq auth `(digest () ,(sha1 (concat jabber-session-id passwd)))))) + (if (yes-or-no-p "Jabber server only allows cleartext password transmission! Continue? ") + (let ((passwd (jabber-read-passwd))) + (if passwd + (setq auth `(password () ,passwd)))))) + + ;; If auth is still nil, user cancelled process somewhere + (if auth + (jabber-send-iq jabber-server + "set" + `(query ((xmlns . "jabber:iq:auth")) + (username () ,jabber-username) + ,auth + (resource () ,jabber-resource)) + #'jabber-process-logon t + #'jabber-process-logon nil) + (jabber-disconnect)))) + (t + (error "Logon error ended up in the wrong place")))) + +(defun jabber-process-logon (xml-data closure-data) + "receive login success or failure, and request roster. +CLOSURE-DATA should be t on success and nil on failure." + (if closure-data + ;; Logon success + (progn + (setq *jabber-authenticated* t) + (jabber-send-iq nil + "get" + '(query ((xmlns . "jabber:iq:roster"))) + #'jabber-process-roster 'initial + #'jabber-report-success "Roster retrieval") + + (run-hooks 'jabber-post-connect-hook)) + + ;; Logon failure + (jabber-report-success xml-data "Logon") + (jabber-disconnect))) + +(provide 'jabber-logon) + +;;; arch-tag: f24ebe5e-3420-44bb-af81-d4de21f378b0 diff --git a/jabber-menu.el b/jabber-menu.el new file mode 100644 index 0000000..4387c76 --- /dev/null +++ b/jabber-menu.el @@ -0,0 +1,141 @@ +;; jabber-menu.el - menu definitions + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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 + +(defvar jabber-menu (make-sparse-keymap "jabber-menu")) + +(defun jabber-menu (&optional remove) + "Put \"Jabber\" menu on menubar. +With prefix argument, remove it." + (interactive "P") + (define-key global-map + [menu-bar jabber-menu] + (and (not remove) (cons "Jabber" jabber-menu)))) + +(define-key jabber-menu + [jabber-menu-connect] + '("Connect" . jabber-connect)) + +(define-key jabber-menu + [jabber-menu-disconnect] + '("Disconnect" . jabber-disconnect)) + +(define-key jabber-menu + [jabber-menu-roster] + '("Switch to roster" . jabber-switch-to-roster-buffer)) + +(define-key jabber-menu + [jabber-menu-customize] + '("Customize" . jabber-customize)) + +(define-key jabber-menu + [jabber-menu-info] + '("Help" . jabber-info)) + +(define-key jabber-menu + [jabber-menu-status] + (cons "Set Status" (make-sparse-keymap "set-status"))) + +(defmacro jabber-define-status-key (title show) + (list 'let (list ( list 'func (list 'make-symbol (list 'concat "jabber-send-presence-" show))) + (list 'menu-item (list 'make-symbol (list 'concat "jabber-menu-status-" show)))) + (list 'fset 'func `(lambda () (interactive) + (jabber-send-presence ,show + (jabber-read-with-input-method "status message: " *jabber-current-status* '*jabber-status-history*) + (format "%d" *jabber-current-priority*)))) + (list 'define-key 'jabber-menu + (list 'vector ''jabber-menu-status 'menu-item) + (list 'cons title 'func)))) + +;;;(dolist (presence jabber-presence-strings) +;;; (jabber-define-status-key (cdr presence) (car presence))) +;;(jabber-define-status-key "Online" "") + +(jabber-define-status-key "Chatty" "chat") +;;(jabber-define-status-key "Away" "away") +;;(jabber-define-status-key "Extended Away" "xa") +(jabber-define-status-key "Do not Disturb" "dnd") +(define-key jabber-menu + [jabber-menu-status jabber-menu-status-xa] + '("Extended Away" . jabber-send-xa-presence)) +(define-key jabber-menu + [jabber-menu-status jabber-menu-status-away] + '("Away" . jabber-send-away-presence)) +(define-key jabber-menu + [jabber-menu-status jabber-menu-status-online] + '("Online" . jabber-send-default-presence)) + +(defvar jabber-jid-chat-menu nil + "Menu items for chat menu") + +(defvar jabber-jid-info-menu nil + "Menu item for info menu") + +(defvar jabber-jid-roster-menu nil + "Menu items for roster menu") + +(defvar jabber-jid-muc-menu nil + "Menu items for MUC menu") + +(defvar jabber-jid-service-menu nil + "Menu items for service menu") + +(defun jabber-popup-menu (which-menu) + "Popup specified menu" + (let* ((mouse-event (and (listp last-input-event) last-input-event)) + (choice (widget-choose "Actions" which-menu mouse-event))) + (if mouse-event + (mouse-set-point mouse-event)) + (if choice + (call-interactively choice)))) + +(defun jabber-popup-chat-menu () + "Popup chat menu" + (interactive) + (jabber-popup-menu jabber-jid-chat-menu)) + +(defun jabber-popup-info-menu () + "Popup info menu" + (interactive) + (jabber-popup-menu jabber-jid-info-menu)) + +(defun jabber-popup-roster-menu () + "Popup roster menu" + (interactive) + (jabber-popup-menu jabber-jid-roster-menu)) + +(defun jabber-popup-muc-menu () + "Popup MUC menu" + (interactive) + (jabber-popup-menu jabber-jid-muc-menu)) + +(defun jabber-popup-service-menu () + "Popup service menu" + (interactive) + (jabber-popup-menu jabber-jid-service-menu)) + +(defun jabber-popup-combined-menu () + "Popup combined menu" + (interactive) + (jabber-popup-menu (append jabber-jid-chat-menu jabber-jid-info-menu jabber-jid-roster-menu jabber-jid-muc-menu))) + +(provide 'jabber-menu) + +;;; arch-tag: 5147f52f-de47-4348-86ff-b799d7a75e3f diff --git a/jabber-modeline.el b/jabber-modeline.el new file mode 100644 index 0000000..6d41093 --- /dev/null +++ b/jabber-modeline.el @@ -0,0 +1,95 @@ +;; jabber-modeline.el - display jabber status in modeline + +;; Copyright (C) 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-presence) +(require 'jabber-alert) +(eval-when-compile (require 'cl)) + +(defgroup jabber-mode-line nil + "Display Jabber status in mode line" + :group 'jabber) + +(defcustom jabber-mode-line-compact t + "Count contacts in fewer categories for compact view" + :group 'jabber-mode-line + :type 'boolean) + +(defvar jabber-mode-line-string nil) +(defvar jabber-mode-line-presence nil) +(defvar jabber-mode-line-contacts nil) + +(defadvice jabber-send-presence (after jsp-update-mode-line + (show status priority)) + (jabber-mode-line-presence-update)) + +(defun jabber-mode-line-presence-update () + (setq jabber-mode-line-presence (if (and *jabber-connected* (not *jabber-disconnecting*)) + (cdr (assoc *jabber-current-show* jabber-presence-strings)) + "Offline"))) + +(defun jabber-mode-line-count-contacts (&rest ignore) + (let ((count (list (cons "chat" 0) + (cons "" 0) + (cons "away" 0) + (cons "xa" 0) + (cons "dnd" 0) + (cons nil 0)))) + (dolist (buddy *jabber-roster*) + (when (assoc (get buddy 'show) count) + (incf (cdr (assoc (get buddy 'show) count))))) + (setq jabber-mode-line-contacts + (if jabber-mode-line-compact + (format "(%d/%d/%d)" + (+ (cdr (assoc "chat" count)) + (cdr (assoc "" count))) + (+ (cdr (assoc "away" count)) + (cdr (assoc "xa" count)) + (cdr (assoc "dnd" count))) + (cdr (assoc nil count))) + (apply 'format "(%d/%d/%d/%d/%d/%d)" + (mapcar 'cdr count)))))) + +(define-minor-mode jabber-mode-line-mode + "Toggle display of Jabber status in mode lines. +Display consists of your own status, and six numbers +meaning the number of chatty, online, away, xa, dnd +and offline contacts, respectively." + :global t :group 'jabber-mode-line + (setq jabber-mode-line-string "") + (or global-mode-string (setq global-mode-string '(""))) + (if jabber-mode-line-mode + (progn + (add-to-list 'global-mode-string 'jabber-mode-line-string t) + + (setq jabber-mode-line-string (list " " + 'jabber-mode-line-presence + " " + 'jabber-mode-line-contacts)) + (jabber-mode-line-presence-update) + (jabber-mode-line-count-contacts) + (ad-activate 'jabber-send-presence) + (add-hook 'jabber-post-disconnect-hook + 'jabber-mode-line-presence-update) + (add-hook 'jabber-presence-hooks + 'jabber-mode-line-count-contacts)))) + +(provide 'jabber-modeline) + +;;; arch-tag: c03a7d3b-8811-49d4-b0e0-7ffd661d7925 diff --git a/jabber-muc.el b/jabber-muc.el new file mode 100644 index 0000000..b09fe15 --- /dev/null +++ b/jabber-muc.el @@ -0,0 +1,745 @@ +;; jabber-muc.el - advanced MUC functions + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-chat) +(require 'jabber-widget) + +(require 'cl) + +(defvar *jabber-active-groupchats* nil + "alist of groupchats and nicknames +Keys are strings, the bare JID of the room. +Values are strings.") + +(defvar jabber-muc-participants nil + "alist of groupchats and participants +Keys are strings, the bare JID of the room. +Values are lists of nickname strings.") + +(defvar jabber-group nil + "the groupchat you are participating in") + +(defvar jabber-muc-topic "" + "The topic of the current MUC room.") + +(defcustom jabber-muc-default-nicknames nil + "Default nickname for specific MUC rooms." + :group 'jabber-chat + :type '(repeat + (cons :format "%v" + (string :tag "JID of room") + (string :tag "Nickname")))) + +(defcustom jabber-muc-autojoin nil + "List of MUC rooms to automatically join on connection." + :group 'jabber-chat + :type '(repeat (string :tag "JID of room"))) + +(defcustom jabber-groupchat-buffer-format "*-jabber-groupchat-%n-*" + "The format specification for the name of groupchat buffers. + +These fields are available (all are about the group you are chatting +in): + +%n Roster name of group, or JID if no nickname set +%j Bare JID (without resource)" + :type 'string + :group 'jabber-chat) + +(defcustom jabber-groupchat-prompt-format "[%t] %n> " + "The format specification for lines in groupchat. + +These fields are available: + +%t Time, formatted according to `jabber-chat-time-format' +%n, %u, %r + Nickname in groupchat +%j Full JID (room@server/nick)" + :type 'string + :group 'jabber-chat) + +(defcustom jabber-muc-header-line-format + '(" " (:eval (jabber-jid-displayname jabber-group)) + "\t" jabber-muc-topic) + "The specification for the header line of MUC buffers. + +The format is that of `mode-line-format' and `header-line-format'." + :type 'sexp + :group 'jabber-chat) + +(defcustom jabber-muc-private-buffer-format "*-jabber-muc-priv-%g-%n-*" + "The format specification for the buffer name for private MUC messages. + +These fields are available: + +%g Roster name of group, or JID if no nickname set +%n Nickname of the group member you're chatting with" + :type 'string + :group 'jabber-chat) + +(defcustom jabber-muc-private-foreign-prompt-format "[%t] %g/%n> " + "The format specification for lines others type in a private MUC buffer. + +These fields are available: + +%t Time, formatted according to `jabber-chat-time-format' +%n Nickname in room +%g Short room name (either roster name or username part of JID)" + :type 'string + :group 'jabber-chat) + +(defcustom jabber-muc-private-header-line-format + '(" " (:eval (jabber-jid-resource jabber-chatting-with)) + " in " (:eval (jabber-jid-displayname (jabber-jid-user jabber-chatting-with))) + "\t" jabber-events-message) + "The specification for the header line of private MUC chat buffers. + +The format is that of `mode-line-format' and `header-line-format'." + :type 'sexp + :group 'jabber-chat) + +(defvar jabber-muc-printers '(jabber-muc-snarf-topic) + "List of functions that may be able to print part of a MUC message. +This gets prepended to `jabber-chat-printers', which see.") + +(defun jabber-muc-get-buffer (group) + "Return the chat buffer for chatroom GROUP. +Either a string or a buffer is returned, so use `get-buffer' or +`get-buffer-create'." + (format-spec jabber-groupchat-buffer-format + (list + (cons ?n (jabber-jid-displayname group)) + (cons ?j (jabber-jid-user group))))) + +(defun jabber-muc-create-buffer (group) + "Prepare a buffer for chatroom GROUP. +This function is idempotent." + (with-current-buffer (get-buffer-create (jabber-muc-get-buffer group)) + (if (not (eq major-mode 'jabber-chat-mode)) (jabber-chat-mode)) + (make-local-variable 'jabber-group) + (make-local-variable 'jabber-muc-topic) + (setq jabber-group group) + (setq jabber-send-function 'jabber-muc-send) + (setq header-line-format jabber-muc-header-line-format) + (current-buffer))) + +(defun jabber-muc-private-get-buffer (group nickname) + "Return the chat buffer for private chat with NICKNAME in GROUP. +Either a string or a buffer is returned, so use `get-buffer' or +`get-buffer-create'." + (format-spec jabber-muc-private-buffer-format + (list + (cons ?g (jabber-jid-displayname group)) + (cons ?n nickname)))) + +(defun jabber-muc-private-create-buffer (group nickname) + "Prepare a buffer for chatting with NICKNAME in GROUP. +This function is idempotent." + (with-current-buffer (get-buffer-create (jabber-muc-private-get-buffer group nickname)) + (if (not (eq major-mode 'jabber-chat-mode)) (jabber-chat-mode)) + (make-local-variable 'jabber-chatting-with) + (setq jabber-chatting-with (concat group "/" nickname)) + (setq jabber-send-function 'jabber-chat-send) + (setq header-line-format jabber-muc-private-header-line-format) + + (current-buffer))) + +(defun jabber-muc-send (body) + "Send BODY to MUC room in current buffer." + ;; There is no need to display the sent message in the buffer, as + ;; we will get it back from the MUC server. + (jabber-send-sexp `(message + ((to . ,jabber-group) + (type . "groupchat")) + (body () ,(jabber-escape-xml body))))) + +(defun jabber-muc-add-groupchat (group nickname) + "Remember participating in GROUP under NICKNAME." + (let ((whichgroup (assoc group *jabber-active-groupchats*))) + (if whichgroup + (setcdr whichgroup nickname) + (add-to-list '*jabber-active-groupchats* (cons group nickname))))) + +(defun jabber-muc-remove-groupchat (group) + "Remove GROUP from internal bookkeeping." + (let ((whichgroup (assoc group *jabber-active-groupchats*)) + (whichparticipants (assoc group jabber-muc-participants))) + (setq *jabber-active-groupchats* + (delq whichgroup *jabber-active-groupchats*)) + (setq jabber-muc-participants + (delq whichparticipants jabber-muc-participants)))) + +(defun jabber-muc-participant-plist (group nickname) + "Return plist associated with NICKNAME in GROUP. +Return nil if nothing known about that combination." + (let ((whichparticipants (assoc group jabber-muc-participants))) + (when whichparticipants + (cdr (assoc nickname whichparticipants))))) + +(defun jabber-muc-modify-participant (group nickname new-plist) + "Assign properties in NEW-PLIST to NICKNAME in GROUP." + (let ((participants (assoc group jabber-muc-participants))) + ;; either we have a list of participants already... + (if participants + (let ((participant (assoc nickname participants))) + ;; and maybe this participant is already in the list + (if participant + ;; if so, just update role, affiliation, etc. + ;; XXX: calculate delta and report to user? e.g. "X was given voice" + (setf (cdr participant) new-plist) + (push (cons nickname new-plist) (cdr participants)))) + ;; or we don't + (push (cons group (list (cons nickname new-plist))) jabber-muc-participants)))) + +(defun jabber-muc-remove-participant (group nickname) + "Forget everything about NICKNAME in GROUP." + (let ((participants (assoc group jabber-muc-participants))) + (when participants + (let ((participant (assoc nickname (cdr participants)))) + (setf (cdr participants) (delq participant (cdr participants))))))) + +(defun jabber-muc-read-completing (prompt) + "Read the name of a joined chatroom, or use chatroom of current buffer, if any." + (or jabber-group + (jabber-read-jid-completing prompt + (if (null *jabber-active-groupchats*) + (error "You haven't joined any group") + (mapcar (lambda (x) (jabber-jid-symbol (car x))) + *jabber-active-groupchats*)) + t + jabber-group))) + +(defun jabber-muc-read-nickname (group prompt) + "Read the nickname of a participant in GROUP." + (let ((nicknames (cdr (assoc group jabber-muc-participants)))) + (unless nicknames + (error "Unknown group: %s" group)) + (completing-read prompt nicknames nil t))) + +(add-to-list 'jabber-jid-muc-menu + (cons "Configure groupchat" 'jabber-groupchat-get-config)) +(defun jabber-groupchat-get-config (group) + "Ask for MUC configuration form" + (interactive (list (jabber-muc-read-completing "Configure group: "))) + (jabber-send-iq group + "get" + '(query ((xmlns . "http://jabber.org/protocol/muc#owner"))) + #'jabber-process-data #'jabber-groupchat-render-config + #'jabber-process-data "MUC configuration request failed")) + +(defun jabber-groupchat-render-config (xml-data) + "Render MUC configuration form" + + (let ((query (jabber-iq-query xml-data)) + xdata) + (dolist (x (jabber-xml-get-children query 'x)) + (if (string= (jabber-xml-get-attribute x 'xmlns) "jabber:x:data") + (setq xdata x))) + (if (not xdata) + (insert "No configuration possible.\n") + + (jabber-init-widget-buffer (jabber-xml-get-attribute xml-data 'from)) + + (jabber-render-xdata-form xdata) + + (widget-create 'push-button :notify #'jabber-groupchat-submit-config "Submit") + (widget-insert "\t") + (widget-create 'push-button :notify #'jabber-groupchat-cancel-config "Cancel") + (widget-insert "\n") + + (widget-setup) + (widget-minor-mode 1)))) + +(defun jabber-groupchat-submit-config (&rest ignore) + "Submit MUC configuration form." + + (jabber-send-iq jabber-submit-to + "set" + `(query ((xmlns . "http://jabber.org/protocol/muc#owner")) + ,(jabber-parse-xdata-form)) + #'jabber-report-success "MUC configuration" + #'jabber-report-success "MUC configuration")) + +(defun jabber-groupchat-cancel-config (&rest ignore) + "Cancel MUC configuration form." + + (jabber-send-iq jabber-submit-to + "set" + '(query ((xmlns . "http://jabber.org/protocol/muc#owner")) + (x ((xmlns . "jabber:x:data") (type . "cancel")))) + nil nil nil nil)) + +(add-to-list 'jabber-jid-muc-menu + (cons "Join groupchat" 'jabber-groupchat-join)) + +(defun jabber-groupchat-join (group nickname) + "join a groupchat, or change nick" + (interactive + (let ((group (jabber-read-jid-completing "group: "))) + (list group (jabber-muc-read-my-nickname group)))) + ;; Remember that this is a groupchat _before_ sending the stanza. + ;; The response might come quicker than you think. + (let ((whichgroup (assoc group *jabber-active-groupchats*))) + (if whichgroup + (setcdr whichgroup nickname) + (add-to-list '*jabber-active-groupchats* (cons group nickname)))) + + (jabber-send-sexp `(presence ((to . ,(format "%s/%s" group nickname))) + (x ((xmlns . "http://jabber.org/protocol/muc"))))) + + (let ((buffer (jabber-muc-create-buffer group))) + ;; We don't want to switch to autojoined groupchats + (when (interactive-p) + (switch-to-buffer buffer)))) + +(defun jabber-muc-read-my-nickname (group) + "Read nickname for joining GROUP." + (let ((default-nickname (or + (cdr (assoc group jabber-muc-default-nicknames)) + jabber-nickname))) + (jabber-read-with-input-method (format "Nickname: (default %s) " + default-nickname) + nil nil default-nickname))) + +(add-to-list 'jabber-jid-muc-menu + (cons "Change nickname" 'jabber-muc-nick)) + +(defalias 'jabber-muc-nick 'jabber-groupchat-join) + +(add-to-list 'jabber-jid-muc-menu + (cons "Leave groupchat" 'jabber-groupchat-leave)) + +(defun jabber-groupchat-leave (group) + "leave a groupchat" + (interactive (list (jabber-muc-read-completing "Leave which group: "))) + (let ((whichgroup (assoc group *jabber-active-groupchats*))) + ;; send unavailable presence to our own nick in room + (jabber-send-sexp `(presence ((to . ,(format "%s/%s" group (cdr whichgroup))) + (type . "unavailable")))))) + +(add-to-list 'jabber-jid-muc-menu + (cons "List participants" 'jabber-muc-names)) + +(defun jabber-muc-names (group) + "Print names, affiliations, and roles of participants in GROUP." + (interactive (list (jabber-muc-read-completing "Group: "))) + (with-current-buffer (jabber-muc-create-buffer group) + (let ((jabber-chat-fill-long-lines nil)) + (jabber-chat-buffer-display 'jabber-muc-system-prompt nil + '(jabber-muc-print-names) + (cdr (assoc group jabber-muc-participants)))))) + +(defun jabber-muc-print-names (participants) + "Format and insert data in PARTICIPANTS." + (apply 'insert "Participants:\n" + (format "%-15s %-15s %-11s %s\n" "Nickname" "Role" "Affiliation" "JID") + (mapcar (lambda (x) + (let ((plist (cdr x))) + (format "%-15s %-15s %-11s %s\n" + (car x) + (plist-get plist 'role) + (plist-get plist 'affiliation) + (or (plist-get plist 'jid) "")))) + participants))) + +(add-to-list 'jabber-jid-muc-menu + (cons "Set topic" 'jabber-muc-set-topic)) + +(defun jabber-muc-set-topic (group topic) + "Set topic of GROUP to TOPIC." + (interactive + (let ((group (jabber-muc-read-completing "Group: "))) + (list group + (jabber-read-with-input-method "New topic: " jabber-muc-topic)))) + (jabber-send-message group topic nil "groupchat")) + +(defun jabber-muc-snarf-topic (xml-data) + "Record subject (topic) of the given <message/>, if any." + (let ((new-topic (jabber-xml-path xml-data '(subject "")))) + (when new-topic + (setq jabber-muc-topic new-topic)))) + +(add-to-list 'jabber-jid-muc-menu + (cons "Set role (kick, voice, op)" 'jabber-muc-set-role)) + +(defun jabber-muc-set-role (group nickname role reason) + "Set role of NICKNAME in GROUP to ROLE, specifying REASON." + (interactive + (let* ((group (jabber-muc-read-completing "Group: ")) + (nickname (jabber-muc-read-nickname group "Nickname: "))) + (list group nickname + (completing-read "New role: " '(("none") ("visitor") ("participant") ("moderator")) nil t) + (read-string "Reason: ")))) + (unless (or (zerop (length nickname)) (zerop (length role))) + (jabber-send-iq group "set" + `(query ((xmlns . "http://jabber.org/protocol/muc#admin")) + (item ((nick . ,nickname) + (role . ,role)) + ,(unless (zerop (length reason)) + `(reason () ,reason)))) + 'jabber-report-success "Role change" + 'jabber-report-success "Role change"))) + +(add-to-list 'jabber-jid-muc-menu + (cons "Set affiliation (ban, member, admin)" 'jabber-muc-set-affiliation)) + +(defun jabber-muc-set-affiliation (group nickname-or-jid nickname-p affiliation reason) + "Set affiliation of NICKNAME-OR-JID in GROUP to AFFILIATION. +If NICKNAME-P is non-nil, NICKNAME-OR-JID is a nickname in the +group, else it is a JID." + (interactive + (let ((group (jabber-muc-read-completing "Group: ")) + (nickname-p (y-or-n-p "Specify user by room nickname? "))) + (list + group + (if nickname-p + (jabber-muc-read-nickname group "Nickname: ") + (jabber-read-jid-completing "User: ")) + nickname-p + (completing-read "New affiliation: " + '(("none") ("outcast") ("member") ("admin") ("owner")) nil t) + (read-string "Reason: ")))) + (let ((jid + (if nickname-p + (let ((participants (cdr (assoc group jabber-muc-participants)))) + (unless participants + (error "Couldn't find group %s" group)) + (let ((participant (cdr (assoc nickname-or-jid participants)))) + (unless participant + (error "Couldn't find %s in group %s" nickname-or-jid group)) + (or (plist-get participant 'jid) + (error "JID of %s in group %s is unknown" nickname-or-jid group)))) + nickname-or-jid))) + (jabber-send-iq group "set" + `(query ((xmlns . "http://jabber.org/protocol/muc#admin")) + (item ((jid . ,jid) + (affiliation . ,affiliation)) + ,(unless (zerop (length reason)) + `(reason () ,reason)))) + 'jabber-report-success "Affiliation change" + 'jabber-report-success "Affiliation change"))) + +(add-to-list 'jabber-jid-muc-menu + (cons "Invite someone to chatroom" 'jabber-muc-invite)) + +(defun jabber-muc-invite (jid group reason) + "Invite JID to GROUP, stating REASON." + (interactive + (list (jabber-read-jid-completing "Invite whom: ") + (jabber-muc-read-completing "To group: ") + (jabber-read-with-input-method "Reason: "))) + (jabber-send-sexp + `(message ((to . ,group)) + (x ((xmlns . "http://jabber.org/protocol/muc#user")) + (invite ((to . ,jid)) + ,(unless (zerop (length reason)) + `(reason nil ,reason))))))) + +(add-to-list 'jabber-body-printers 'jabber-muc-print-invite) + +(defun jabber-muc-print-invite (xml-data) + "Print MUC invitation" + (dolist (x (jabber-xml-get-children xml-data 'x)) + (when (string= (jabber-xml-get-attribute x 'xmlns) "http://jabber.org/protocol/muc#user") + (let ((invitation (car (jabber-xml-get-children x 'invite)))) + (when invitation + (let ((group (jabber-xml-get-attribute xml-data 'from)) + (inviter (jabber-xml-get-attribute invitation 'from)) + (reason (car (jabber-xml-node-children (car (jabber-xml-get-children invitation 'reason)))))) + ;; XXX: password + (insert "You have been invited to MUC room " (jabber-jid-displayname group)) + (when inviter + (insert " by " (jabber-jid-displayname inviter))) + (insert ".") + (when reason + (insert " Reason: " reason)) + (insert "\n\n") + + (let ((action + `(lambda (&rest ignore) (interactive) + (jabber-groupchat-join ,group + (jabber-muc-read-my-nickname ,group))))) + (if (fboundp 'insert-button) + (insert-button "Accept" + 'action action) + ;; Simple button replacement + (let ((keymap (make-keymap))) + (define-key keymap "\r" action) + (insert (jabber-propertize "Accept" + 'keymap keymap + 'face 'highlight)))) + + (insert "\t") + + (let ((action + `(lambda (&rest ignore) (interactive) + (let ((reason + (jabber-read-with-input-method + "Reason: "))) + (jabber-send-sexp + (list 'message + (list (cons 'to ,group)) + (list 'x + (list (cons 'xmlns "http://jabber.org/protocol/muc#user")) + (list 'decline + (list (cons 'to ,inviter)) + (unless (zerop (length reason)) + (list 'reason nil reason)))))))))) + (if (fboundp 'insert-button) + (insert-button "Decline" + 'action action) + ;; Simple button replacement + (let ((keymap (make-keymap))) + (define-key keymap "\r" action) + (insert (jabber-propertize "Decline" + 'keymap keymap + 'face 'highlight))))))) + (return t)))))) + +(defun jabber-muc-autojoin () + "Join rooms specified in variable `jabber-muc-autojoin'." + (interactive) + (dolist (group jabber-muc-autojoin) + (jabber-groupchat-join group (or + (cdr (assoc group jabber-muc-default-nicknames)) + jabber-nickname)))) + +(defun jabber-muc-message-p (message) + "Return non-nil if MESSAGE is a groupchat message. +That does not include private messages in a groupchat." + ;; Public groupchat messages have type "groupchat" and are from + ;; room@server/nick. Public groupchat errors have type "error" and + ;; are from room@server. + (let ((from (jabber-xml-get-attribute message 'from)) + (type (jabber-xml-get-attribute message 'type))) + (or + (string= type "groupchat") + (and (string= type "error") + (assoc from *jabber-active-groupchats*))))) + +(defun jabber-muc-sender-p (jid) + "Return non-nil if JID is a full JID of an MUC participant." + (and (assoc (jabber-jid-user jid) *jabber-active-groupchats*) + (jabber-jid-resource jid))) + +(defun jabber-muc-private-message-p (message) + "Return non-nil if MESSAGE is a private message in a groupchat." + (let ((from (jabber-xml-get-attribute message 'from)) + (type (jabber-xml-get-attribute message 'type))) + (and + (not (string= type "groupchat")) + (jabber-muc-sender-p from)))) + +(add-to-list 'jabber-jid-muc-menu + (cons "Open private chat" 'jabber-muc-private)) + +(defun jabber-muc-private (group nickname) + "Open private chat with NICKNAME in GROUP." + (interactive + (let* ((group (jabber-muc-read-completing "Group: ")) + (nickname (jabber-muc-read-nickname group "Nickname: "))) + (list group nickname))) + (switch-to-buffer (jabber-muc-private-create-buffer group nickname))) + +(defun jabber-muc-presence-p (presence) + "Return non-nil if PRESENCE is presence from groupchat." + (let ((from (jabber-xml-get-attribute presence 'from))) + (assoc (jabber-jid-user from) *jabber-active-groupchats*))) + +(defun jabber-muc-parse-affiliation (x-muc) + "Parse X-MUC in the muc#user namespace and return a plist. +Return nil if X-MUC is nil." + ;; XXX: parse <actor/> and <reason/> tags? or maybe elsewhere? + (apply 'nconc (mapcar (lambda (prop) (list (car prop) (cdr prop))) + (jabber-xml-node-attributes + (car (jabber-xml-get-children x-muc 'item)))))) + +(defun jabber-muc-print-prompt (xml-data) + "Print MUC prompt for message in XML-DATA." + (let ((nick (jabber-jid-resource (jabber-xml-get-attribute xml-data 'from))) + (timestamp (car (delq nil (mapcar 'jabber-x-delay (jabber-xml-get-children xml-data 'x)))))) + (jabber-maybe-print-rare-time timestamp) + (if (stringp nick) + (insert (jabber-propertize + (format-spec jabber-groupchat-prompt-format + (list + (cons ?t (format-time-string + (if timestamp + jabber-chat-delayed-time-format + jabber-chat-time-format) + timestamp)) + (cons ?n nick) + (cons ?u nick) + (cons ?r nick) + (cons ?j (concat jabber-group "/" nick)))) + 'face 'jabber-chat-prompt-foreign + 'help-echo (concat (format-time-string "On %Y-%m-%d %H:%M:%S" timestamp) " from " nick " in " jabber-group))) + (jabber-muc-system-prompt)))) + +(defun jabber-muc-private-print-prompt (xml-data) + "Print prompt for private MUC message in XML-DATA." + (let ((nick (jabber-jid-resource (jabber-xml-get-attribute xml-data 'from))) + (group (jabber-jid-user (jabber-xml-get-attribute xml-data 'from))) + (timestamp (car (delq nil (mapcar 'jabber-x-delay (jabber-xml-get-children xml-data 'x)))))) + (jabber-maybe-print-rare-time timestamp) + (insert (jabber-propertize + (format-spec jabber-muc-private-foreign-prompt-format + (list + (cons ?t (format-time-string + (if timestamp + jabber-chat-delayed-time-format + jabber-chat-time-format) + timestamp)) + (cons ?n nick) + (cons ?g (or (jabber-jid-rostername group) + (jabber-jid-username group))))) + 'face 'jabber-chat-prompt-foreign + 'help-echo (concat (format-time-string "On %Y-%m-%d %H:%M:%S" timestamp) " from " nick " in " jabber-group))))) + +(defun jabber-muc-system-prompt (&rest ignore) + "Print system prompt for MUC." + (jabber-maybe-print-rare-time nil) + (insert (jabber-propertize + (format-spec jabber-groupchat-prompt-format + (list + (cons ?t (format-time-string jabber-chat-time-format)) + (cons ?n "") + (cons ?u "") + (cons ?r "") + (cons ?j jabber-group))) + 'face 'jabber-chat-prompt-system + 'help-echo (format-time-string "System message on %Y-%m-%d %H:%M:%S")))) + +(add-to-list 'jabber-message-chain 'jabber-muc-process-message) + +(defun jabber-muc-process-message (xml-data) + "If XML-DATA is a groupchat message, handle it as such." + (when (jabber-muc-message-p xml-data) + (let* ((from (jabber-xml-get-attribute xml-data 'from)) + (group (jabber-jid-user from)) + (nick (jabber-jid-resource from)) + (error-p (jabber-xml-get-children xml-data 'error)) + (body-text (car (jabber-xml-node-children + (car (jabber-xml-get-children + xml-data 'body)))))) + (with-current-buffer (jabber-muc-create-buffer group) + ;; Call alert hooks only when something is output + (when + (jabber-chat-buffer-display 'jabber-muc-print-prompt + xml-data + (if error-p + '(jabber-chat-print-error) + (append jabber-muc-printers + jabber-chat-printers)) + xml-data) + + (dolist (hook '(jabber-muc-hooks jabber-alert-muc-hooks)) + (run-hook-with-args hook + nick group (current-buffer) body-text + (funcall jabber-alert-muc-function + nick group (current-buffer) body-text)))))))) + +(defun jabber-muc-process-presence (presence) + (let* ((from (jabber-xml-get-attribute presence 'from)) + (type (jabber-xml-get-attribute presence 'type)) + (x-muc (find-if + (lambda (x) (equal (jabber-xml-get-attribute x 'xmlns) + "http://jabber.org/protocol/muc#user")) + (jabber-xml-get-children presence 'x))) + (group (jabber-jid-user from)) + (nickname (jabber-jid-resource from)) + (symbol (jabber-jid-symbol from)) + (item (car (jabber-xml-get-children x-muc 'item))) + (actor (jabber-xml-get-attribute (car (jabber-xml-get-children item 'actor)) 'jid)) + (reason (car (jabber-xml-node-children (car (jabber-xml-get-children item 'reason))))) + (status-code (jabber-xml-get-attribute + (car (jabber-xml-get-children x-muc 'status)) + 'code)) + (error-node (car (jabber-xml-get-children presence 'error)))) + ;; handle leaving a room + (cond + ((or (string= type "unavailable") (string= type "error")) + ;; are we leaving? + (if (string= nickname (cdr (assoc group *jabber-active-groupchats*))) + (progn + (jabber-muc-remove-groupchat group) + ;; If there is no buffer for this groupchat, don't bother + ;; creating one just to tell that user left the room. + (let ((buffer (get-buffer (jabber-muc-get-buffer group)))) + (when buffer + (with-current-buffer buffer + (jabber-chat-buffer-display + 'jabber-muc-system-prompt + nil + '(insert) + (cond + ((string= type "error") + (jabber-propertize + (concat "Error entering room" + (when error-node + (concat ": " (jabber-parse-error error-node)))) + 'face 'jabber-chat-error)) + ((equal status-code "301") + (concat "You have been banned" + (when actor (concat " by " actor)) + (when reason (concat " - '" reason "'")))) + ((equal status-code "307") + (concat "You have been kicked" + (when actor (concat " by " actor)) + (when reason (concat " - '" reason "'")))) + (t + "You have left the chatroom"))))))) + ;; or someone else? + (jabber-muc-remove-participant group nickname) + (with-current-buffer (jabber-muc-create-buffer group) + (jabber-chat-buffer-display + 'jabber-muc-system-prompt + nil + '(insert) + (cond + ((equal status-code "301") + (concat nickname " has been banned" + (when actor (concat " by " actor)) + (when reason (concat " - '" reason "'")))) + ((equal status-code "307") + (concat nickname " has been kicked" + (when actor (concat " by " actor)) + (when reason (concat " - '" reason "'")))) + ((equal status-code "303") + (concat nickname " changes nickname to " + (jabber-xml-get-attribute item 'nick))) + (t + (concat nickname " has left the chatroom"))))))) + (t + ;; someone is entering + (let ((new-participant (not (jabber-muc-participant-plist group nickname))) + (new-plist (jabber-muc-parse-affiliation x-muc))) + (jabber-muc-modify-participant group nickname new-plist) + (when new-participant + (with-current-buffer (jabber-muc-create-buffer group) + (jabber-chat-buffer-display 'jabber-muc-system-prompt + nil + '(insert) + (format "%s enters the chatroom" nickname))))))))) + +(provide 'jabber-muc) + +;;; arch-tag: 1ff7ab35-1717-46ae-b803-6f5b3fb2cd7d diff --git a/jabber-presence.el b/jabber-presence.el new file mode 100644 index 0000000..a08e680 --- /dev/null +++ b/jabber-presence.el @@ -0,0 +1,346 @@ +;; jabber-presence.el - roster and presence bookkeeping + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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) + +(add-to-list 'jabber-iq-set-xmlns-alist + (cons "jabber:iq:roster" (function (lambda (x) (jabber-process-roster x nil))))) +(defun jabber-process-roster (xml-data closure-data) + "process an incoming roster infoquery result +CLOSURE-DATA should be 'initial if initial roster push, nil otherwise." + + ;; Perform sanity check on "from" attribute: it should be either absent + ;; or match our own JID. + (let ((from (jabber-xml-get-attribute xml-data 'from)) + (type (jabber-xml-get-attribute xml-data 'type)) + (id (jabber-xml-get-attribute xml-data 'id))) + (if (not (or (null from) + (string= from (concat jabber-username "@" jabber-server)) + (string= from (concat jabber-username "@" jabber-server "/" jabber-resource)))) + (message "Roster push with invalid \"from\": \"%s\"" from) + + ;; If *jabber-roster* is empty, we just fill up the roster with + ;; the given data. If not, we have received a partial roster + ;; update, so just fill in that data. These cases can be + ;; differentiated by the type attribute of the iq tag: if + ;; type='result', we asked for the whole roster. If type='set', + ;; we are getting a "roster push". + (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)))) + + ;; Find contact if already in roster + (setq roster-item (car (memq jid *jabber-roster*))) + + ;; If not found, create a new roster item. + (when (null roster-item) + (message "%s added to roster" jid) + (setq roster-item jid) + (setq *jabber-roster* (cons roster-item *jabber-roster*))) + + ;; 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) + + ;; xml-parse-tag will put "" as the only child element of an + ;; empty element, (e.g. <item jid="foo@bar"/> as opposed to + ;; <item jid="foo@bar"><group>baz</group></item>) which + ;; xml-get-children subsequently will choke on. We want to + ;; avoid that with an extra check. + (put roster-item 'groups (mapcar #'(lambda (foo) (nth 2 foo)) (jabber-xml-get-children item 'group))) + + ;; If subscripton="remove", contact is to be removed from roster + (when (string= (get roster-item 'subscription) "remove") + (message "%s removed from roster" jid) + (setq *jabber-roster* (delq roster-item *jabber-roster*))) + + ))) + (jabber-display-roster) + (if (and id (string= type "set")) + (jabber-send-iq jabber-server "result" nil + nil nil nil nil id)))) + +(add-to-list 'jabber-presence-chain 'jabber-process-presence) +(defun jabber-process-presence (xml-data) + "process incoming presence tags" + (let ((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 from presence-status)) + + ((jabber-muc-presence-p xml-data) + (jabber-muc-process-presence xml-data)) + + (t + ;; XXX: Think about what to do about out-of-roster presences. + (let ((buddy (jabber-jid-symbol from))) + (if (memq buddy *jabber-roster*) + (let* ((oldstatus (get buddy 'show)) + (resource (or (jabber-jid-resource from) "")) + (resource-plist (cdr (assoc resource + (get buddy 'resources)))) + newstatus) + (cond + ((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 + (jabber-unescape-xml 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) + (jabber-unescape-xml 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 + (jabber-unescape-xml presence-status))) + (setq resource-plist + (plist-put resource-plist 'priority priority)) + (setq newstatus (or presence-show "")))) + + ;; 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) + + (dolist (hook '(jabber-presence-hooks jabber-alert-presence-hooks)) + (run-hook-with-args hook + buddy + oldstatus + newstatus + (jabber-unescape-xml + (plist-get resource-plist 'status)) + (funcall jabber-alert-presence-message-function + buddy + oldstatus + newstatus + (jabber-unescape-xml + (plist-get resource-plist 'status)))))))))))) + +(defun jabber-process-subscription-request (from presence-status) + "process an incoming subscription request" + (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))) + (jabber-send-sexp + (list 'presence + (list (cons 'to from) + (cons 'type + (if (yes-or-no-p (format "the user - %s - has requested to subscribe to your presence (%s). allow? " + (jabber-jid-displayname from) + (jabber-unescape-xml presence-status))) + "subscribed" + "unsubscribed"))))) + (when (yes-or-no-p (format "Do you want to subscribe to %s's presence? " from)) + (jabber-send-sexp + (list 'presence (list (cons 'to from) + (cons 'type "subscribe")))))) + +(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)) + +(defun jabber-send-presence (show status priority) + "send a presence tag to the server" + (interactive (list (completing-read "show:" + '(("" . nil) + ("away" . nil) + ("xa" . nil) + ("dnd" . nil) + ("chat" . nil)) + nil t) + (jabber-read-with-input-method "status message: " *jabber-current-status* '*jabber-status-history*) + (read-string "priority: " (progn + (unless *jabber-current-priority* + (setq *jabber-current-priority* + jabber-default-priority)) + (int-to-string *jabber-current-priority*))))) + (if (numberp priority) + (setq priority (int-to-string priority))) + (setq *jabber-current-status* status) + (setq *jabber-current-show* show) + (setq *jabber-current-priority* (string-to-int priority)) + (jabber-send-sexp `(presence () + ,(if (> (length status) 0) + `(status () ,(jabber-escape-xml status))) + ,(if (> (length show) 0) + `(show () ,(jabber-escape-xml show))) + (priority () ,(jabber-escape-xml (int-to-string *jabber-current-priority*))))) + (jabber-display-roster)) + +(defun jabber-send-away-presence () + "Set status to away. +Status description is empty. Priority is unchanged." + (interactive) + (jabber-send-presence "away" "" *jabber-current-priority*)) + +(defun jabber-send-xa-presence () + "Send extended away presence. +Status description is empty. Priority is unchanged." + (interactive) + (jabber-send-presence "xa" "" *jabber-current-priority*)) + +(defun jabber-send-default-presence () + "Send default presence. +Default presence is specified by `jabber-default-priority', `jabber-default-show', +and `jabber-default-status'." + (interactive) + (jabber-send-presence jabber-default-show jabber-default-status jabber-default-priority)) + +(add-to-list 'jabber-jid-roster-menu + (cons "Send subscription request" 'jabber-send-subscription-request)) +(defun jabber-send-subscription-request (to &optional request) + "send a subscription request to jid, showing him your request text, if specified" + (interactive (list (jabber-read-jid-completing "to: ") + (jabber-read-with-input-method "request: "))) + (jabber-send-sexp `(presence ((to . ,to) + (type . "subscribe")) + ,(if (and request (> (length request) 0)) + request)))) + +(add-to-list 'jabber-jid-roster-menu + (cons "Add/modify roster entry" 'jabber-roster-change)) +(defun jabber-roster-change (jid name groups) + "Add or change a roster item." + (interactive (let* ((jid (jabber-jid-symbol + (jabber-read-jid-completing "Add/change JID: "))) + (name (get jid 'name)) + (groups (get jid 'groups))) + (list jid (jabber-read-with-input-method (format "Name: (default `%s') " name) nil nil name) + (car (read-from-string (jabber-read-with-input-method (format "Groups: (default `%S') " groups) nil nil (format "%S" groups))))))) + ;; If new fields are added to the roster XML structure in a future standard, + ;; they will be clobbered by this function. + (jabber-send-iq nil "set" + (list 'query (list (cons 'xmlns "jabber:iq:roster")) + (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 (jid) + (interactive (list (jabber-read-jid-completing "Delete from roster: "))) + (jabber-send-iq 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))) + (if (and jid-at-point + (yes-or-no-p (format "Really delete %s from roster? " jid-at-point))) + (jabber-roster-delete jid-at-point) + (error "No contact at point")))) + +(provide 'jabber-presence) + +;;; arch-tag: b8616d4c-dde8-423e-86c7-da7b4928afc3 diff --git a/jabber-ratpoison.el b/jabber-ratpoison.el new file mode 100644 index 0000000..141f290 --- /dev/null +++ b/jabber-ratpoison.el @@ -0,0 +1,34 @@ +;; jabber-ratpoison.el - emacs-jabber interface to ratpoison + +;; Copyright (C) 2005 - Magnus Henoch - mange@freemail.hu + +;; 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 + +(defun jabber-ratpoison-message (msg) + "Show MSG in Ratpoison" + ;; Possible errors include not finding the ratpoison binary, and + ;; too many pipes open because of message flood. + (condition-case e + (let ((process-connection-type)) + (start-process "ratpoison" nil "ratpoison" "-c" (concat "echo " msg))) + (error nil))) + +(define-jabber-alert ratpoison "Show a message through the Ratpoison window manager" + 'jabber-ratpoison-message) + +(provide 'jabber-ratpoison) +;; arch-tag: 19650075-5D05-11D9-B80F-000A95C2FCD0 diff --git a/jabber-register.el b/jabber-register.el new file mode 100644 index 0000000..4acdea2 --- /dev/null +++ b/jabber-register.el @@ -0,0 +1,143 @@ +;; jabber-register.el - registration according to JEP-0077 + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-iq) +(require 'jabber-widget) + +(add-to-list 'jabber-jid-service-menu + (cons "Register with service" 'jabber-get-register)) +(defun jabber-get-register (to) + "Send IQ get request in namespace \"jabber:iq:register\"." + (interactive (list (jabber-read-jid-completing "Register with: "))) + (jabber-send-iq to + "get" + '(query ((xmlns . "jabber:iq:register"))) + #'jabber-process-data #'jabber-process-register-or-search + #'jabber-report-success "Registration")) + +(defun jabber-process-register-or-search (xml-data) + "Display results from jabber:iq:{register,search} query as a form." + + (let ((query (jabber-iq-query xml-data)) + (have-xdata nil) + (type (cond + ((string= (jabber-iq-xmlns xml-data) "jabber:iq:register") + 'register) + ((string= (jabber-iq-xmlns xml-data) "jabber:iq:search") + 'search) + (t + (error "Namespace %s not handled by jabber-process-register-or-search" (jabber-iq-xmlns xml-data)))))) + + (cond + ((eq type 'register) + ;; If there is no `from' attribute, we are registering with the server + (jabber-init-widget-buffer (or (jabber-xml-get-attribute xml-data 'from) jabber-server))) + + ((eq type 'search) + ;; no such thing here + (jabber-init-widget-buffer (jabber-xml-get-attribute xml-data 'from)))) + + (widget-insert (if (eq type 'register) "Register with " "Search ") jabber-submit-to "\n\n") + (when (and (eq type 'register) + jabber-register-p) + (widget-insert "Don't change the username here unless you also change ") + (widget-create 'link + :notify (lambda (&rest ignore) + (customize-variable 'jabber-username)) + "jabber-username") + (widget-insert ".\n\n")) + + (dolist (x (jabber-xml-get-children query 'x)) + (when (string= (jabber-xml-get-attribute x 'xmlns) "jabber:x:data") + (setq have-xdata t) + ;; If the registration form obeys JEP-0068, we know + ;; for sure how to put a default username in it. + (jabber-render-xdata-form x + (if (and jabber-register-p + (string= (jabber-xdata-formtype x) "jabber:iq:register")) + (list (cons "username" jabber-username)) + nil)))) + (if (not have-xdata) + (jabber-render-register-form query)) + + (widget-create 'push-button :notify (if (eq type 'register) + #'jabber-submit-register + #'jabber-submit-search) "Submit") + (when (eq type 'register) + (widget-insert "\t") + (widget-create 'push-button :notify #'jabber-remove-register "Cancel registration")) + (widget-insert "\n") + (widget-setup) + (widget-minor-mode 1))) + +(defun jabber-submit-register (&rest ignore) + "Submit registration input. See `jabber-process-register-or-search'." + + (let ((handler (if jabber-register-p + #'jabber-process-register-secondtime + #'jabber-report-success)) + (text (concat "Registration with " jabber-submit-to))) + (jabber-send-iq jabber-submit-to + "set" + + (cond + ((eq jabber-form-type 'register) + `(query ((xmlns . "jabber:iq:register")) + ,@(jabber-parse-register-form))) + ((eq jabber-form-type 'xdata) + `(query ((xmlns . "jabber:iq:register")) + ,(jabber-parse-xdata-form))) + (t + (error "Unknown form type: %s" jabber-form-type))) + handler (if jabber-register-p 'success text) + handler (if jabber-register-p 'failure text))) + + (message "Registration sent")) + +(defun jabber-process-register-secondtime (xml-data closure-data) + "Receive registration success or failure. +CLOSURE-DATA is either 'success or 'error." + (setq jabber-register-p nil) + (cond + ((eq closure-data 'success) + (message "Registration successful. Your JID is %s@%s." + jabber-username jabber-server) + (sit-for 3) + (jabber-get-auth jabber-server)) + (t + (jabber-report-success xml-data "Account registration") + (sit-for 3) + (jabber-disconnect)))) + +(defun jabber-remove-register (&rest ignore) + "Cancel registration. See `jabber-process-register-or-search'." + + (if (yes-or-no-p (concat "Are you sure that you want to cancel your registration to " jabber-submit-to "? ")) + (jabber-send-iq jabber-submit-to + "set" + '(query ((xmlns . "jabber:iq:register")) + (remove)) + #'jabber-report-success "Unregistration" + #'jabber-report-success "Unregistration"))) + +(provide 'jabber-register) + +;;; arch-tag: e6b349d6-b1ad-4d19-a412-74459dfae239 diff --git a/jabber-roster.el b/jabber-roster.el new file mode 100644 index 0000000..1cd151d --- /dev/null +++ b/jabber-roster.el @@ -0,0 +1,456 @@ +;; jabber-roster.el - displaying the roster -*- coding: utf-8; -*- + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-presence) +(require 'jabber-util) +(require 'jabber-alert) +(require 'jabber-keymap) +(require 'format-spec) + +(defgroup jabber-roster nil "roster display options" + :group 'jabber) + +(defcustom jabber-roster-line-format " %c %-25n %u %-8s %S\n" + "The format specification of the lines in the roster display. + +These fields are available: + +%c \"*\" if the contact is connected, or \" \" if not +%u sUbscription state - see below +%n Nickname of contact, or JID if no nickname +%j Bare JID of contact (without resource) +%r Highest-priority resource of contact +%s Availability of contact as string (\"Online\", \"Away\" etc) +%S Status string specified by contact + +%u is replaced by one of the strings given by +`jabber-roster-subscription-display'." + :type 'string + :group 'jabber-roster) + +(defcustom jabber-roster-subscription-display '(("none" . " ") + ("from" . "< ") + ("to" . " >") + ("both" . "<->")) + "Strings used for indicating subscription status of contacts. +\"none\" means that there is no subscription between you and the +contact. +\"from\" means that the contact has a subscription to you, but you +have no subscription to the contact. +\"to\" means that you have a subscription to the contact, but the +contact has no subscription to you. +\"both\" means a mutual subscription. + +Having a \"presence subscription\" means being able to see the +other person's presence. + +Some fancy arrows you might want to use, if your system can +display them: ← → ⇄ ↔" + :type '(list (cons :format "%v" (const :format "" "none") (string :tag "None")) + (cons :format "%v" (const :format "" "from") (string :tag "From")) + (cons :format "%v" (const :format "" "to") (string :tag "To")) + (cons :format "%v" (const :format "" "both") (string :tag "Both"))) + :group 'jabber-roster) + +(defcustom jabber-resource-line-format " %r - %s (%S), priority %p\n" + "The format specification of resource lines in the roster display. +These are displayed when `jabber-show-resources' permits it. + +These fields are available: + +%c \"*\" if the contact is connected, or \" \" if not +%n Nickname of contact, or JID if no nickname +%j Bare JID of contact (without resource) +%p Priority of this resource +%r Name of this resource +%s Availability of resource as string (\"Online\", \"Away\" etc) +%S Status string specified by resource" + :type 'string + :group 'jabber-roster) + +(defcustom jabber-roster-sort-functions + '(jabber-roster-sort-by-status jabber-roster-sort-by-displayname) + "Sort roster according to these criteria. + +These functions should take two roster items A and B, and return: +<0 if A < B +0 if A = B +>0 if A > B" + :type 'hook + :options '(jabber-roster-sort-by-status + jabber-roster-sort-by-displayname) + :group 'jabber-roster) + +(defcustom jabber-sort-order '("chat" "" "away" "dnd" "xa") + "Sort by status in this order. Anything not in list goes last. +Offline is represented as nil." + :type '(repeat (restricted-sexp :match-alternatives (stringp nil))) + :group 'jabber-roster) + +(defcustom jabber-show-resources 'sometimes + "Show resources in roster?" + :type '(radio (const :tag "Never" nil) + (const :tag "When more than one connected resource" sometimes) + (const :tag "Always" always)) + :group 'jabber-roster) + +(defcustom jabber-remove-newlines t + "Remove newlines in status messages? +Newlines in status messages mess up the roster display. However, +they are essential to status message poets. Therefore, you get to +choose the behaviour. + +Trailing newlines are always removed, regardless of this variable." + :type 'boolean + :group 'jabber-roster) + +(defcustom jabber-roster-show-bindings t + "Show keybindings in roster buffer?" + :type 'boolean + :group 'jabber-roster) + +(defcustom jabber-roster-mode-hook nil + "Hook run when entering Roster mode." + :group 'jabber-roster + :type 'hook) + +(defface jabber-roster-user-online + '((t (:foreground "blue" :weight bold :slant normal))) + "face for displaying online users" + :group 'jabber-roster) + +(defface jabber-roster-user-xa + '((((background dark)) (:foreground "magenta" :weight normal :slant italic)) + (t (:foreground "black" :weight normal :slant italic))) + "face for displaying extended away users" + :group 'jabber-roster) + +(defface jabber-roster-user-dnd + '((t (:foreground "red" :weight normal :slant italic))) + "face for displaying do not disturb users" + :group 'jabber-roster) + +(defface jabber-roster-user-away + '((t (:foreground "dark green" :weight normal :slant italic))) + "face for displaying away users" + :group 'jabber-roster) + +(defface jabber-roster-user-chatty + '((t (:foreground "dark orange" :weight bold :slant normal))) + "face for displaying chatty users" + :group 'jabber-roster) + +(defface jabber-roster-user-error + '((t (:foreground "red" :weight light :slant italic))) + "face for displaying users sending presence errors" + :group 'jabber-roster) + +(defface jabber-roster-user-offline + '((t (:foreground "dark grey" :weight light :slant italic))) + "face for displaying offline users" + :group 'jabber-roster) + +(defvar jabber-roster-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map jabber-common-keymap) + (define-key map [mouse-2] 'jabber-popup-combined-menu) + (define-key map (kbd "TAB") 'jabber-go-to-next-jid) + (define-key map (kbd "RET") 'jabber-chat-with-jid-at-point) + (define-key map (kbd "C-k") 'jabber-roster-delete-jid-at-point) + + (define-key map "e" 'jabber-roster-change) + (define-key map "s" 'jabber-send-subscription-request) + (define-key map "q" 'bury-buffer) + (define-key map "i" 'jabber-get-disco-items) + (define-key map "j" 'jabber-groupchat-join) + (define-key map "I" 'jabber-get-disco-info) + (define-key map "b" 'jabber-get-browse) + (define-key map "v" 'jabber-get-version) + (define-key map "a" 'jabber-send-presence) + (define-key map "g" 'jabber-display-roster) + ;;(define-key map "D" 'jabber-disconnect) + map)) + +(defun jabber-roster-mode () + "Major mode for Jabber roster display. +Use the keybindings (mnemonic as Chat, Roster, Info, MUC, Service) to +bring up menus of actions. +\\{jabber-roster-mode-map}" + (kill-all-local-variables) + (setq major-mode 'jabber-roster-mode + mode-name "jabber-roster") + (use-local-map jabber-roster-mode-map) + (setq buffer-read-only t) + (if (fboundp 'run-mode-hooks) + (run-mode-hooks 'jabber-roster-mode-hook) + (run-hooks 'jabber-roster-mode-hook))) + +(put 'jabber-roster-mode 'mode-class 'special) + +(defun jabber-switch-to-roster-buffer () + "Switch to roster buffer." + (interactive) + (switch-to-buffer jabber-roster-buffer)) + +(defun jabber-sort-roster () + "sort roster according to online status" + (setq *jabber-roster* + (sort *jabber-roster* #'jabber-roster-sort-items))) + +(defun jabber-roster-sort-items (a b) + "Sort roster items A and B according to `jabber-roster-sort-functions'. +Return t if A is less than B." + (dolist (fn jabber-roster-sort-functions) + (let ((comparison (funcall fn a b))) + (cond + ((< comparison 0) + (return t)) + ((> comparison 0) + (return nil)))))) + +(defun jabber-roster-sort-by-status (a b) + "Sort roster items by online status. +See `jabber-sort-order' for order used." + (flet ((order (item) (length (member (get item 'show) jabber-sort-order)))) + (let ((a-order (order a)) + (b-order (order b))) + ;; Note reversed test. Items with longer X-order go first. + (cond + ((< a-order b-order) + 1) + ((> a-order b-order) + -1) + (t + 0))))) + +(defun jabber-roster-sort-by-displayname (a b) + "Sort roster items by displayed name." + (cond + ((string-lessp a b) -1) + ((string= a b) 0) + (t 1))) + +(defun jabber-fix-status (status) + "Make status strings more readable" + (when (string-match "\n+$" status) + (setq status (replace-match "" t t status))) + (when jabber-remove-newlines + (while (string-match "\n" status) + (setq status (replace-match " " t t status)))) + status) + +(defvar jabber-roster-positions nil + "Alist tracking positions of items in the roster. +Keys are bare JID symbols. Values are conses of markers, +marking the extent of the roster entry.") + +(defun jabber-display-roster () + "switch to the main jabber buffer and refresh the roster display to reflect the current information" + (interactive) + (with-current-buffer (get-buffer-create jabber-roster-buffer) + (if (not (eq major-mode 'jabber-roster-mode)) + (jabber-roster-mode)) + (setq buffer-read-only nil) + ;; line-number-at-pos is in Emacs >= 21.4. Only used to avoid + ;; excessive scrolling when updating roster, so not absolutely + ;; necessary. + (let ((current-line (and (fboundp 'line-number-at-pos) (line-number-at-pos))) + (current-column (current-column))) + (erase-buffer) + (setq jabber-roster-positions nil) + (insert (jabber-propertize jabber-server 'face 'jabber-title-large) "\n") + (when jabber-roster-show-bindings + (insert "RET Open chat buffer C-k Delete roster item +e Edit item s Send subscription request +q Bury buffer i Get disco items +I Get disco info b Browse +j Join groupchat (MUC) v Get client version +a Send presence +C-c C-c Chat menu C-c C-m Multi-User Chat menu +C-c C-i Info menu C-c C-r Roster menu +C-c C-s Service menu +")) + (insert "__________________________________\n\n") + (let ((map (make-sparse-keymap))) + (define-key map [mouse-2] #'jabber-send-presence) + (insert (jabber-propertize (concat (format " - %s" + (cdr (assoc *jabber-current-show* jabber-presence-strings))) + (if (not (zerop (length *jabber-current-status*))) + (format " (%s)" + (jabber-fix-status *jabber-current-status*))) + " -") + 'face (or (cdr (assoc *jabber-current-show* jabber-presence-faces)) + 'jabber-roster-user-online) + ;;'mouse-face (cons 'background-color "light grey") + 'keymap map) + "\n__________________________________\n\n")) + + (jabber-sort-roster) + (dolist (buddy *jabber-roster*) + (let ((entry-start (point))) + (jabber-display-roster-entry buddy) + + ;; Keep track of this roster entry's position + (let ((entry (assq buddy jabber-roster-positions))) + (unless entry + (setq entry (cons buddy nil)) + (push entry jabber-roster-positions)) + (let ((marker-start (set-marker (make-marker) entry-start)) + (marker-end (set-marker (make-marker) (point)))) + ;; Text is inserted before start markers, but after + ;; end markers. + (set-marker-insertion-type marker-start t) + (setcdr entry (cons marker-start marker-end)))))) + (insert "__________________________________") + (goto-char (point-min)) + (setq buffer-read-only t) + (if (interactive-p) + (dolist (hook '(jabber-info-message-hooks jabber-alert-info-message-hooks)) + (run-hook-with-args hook 'roster (current-buffer) (funcall jabber-alert-info-message-function 'roster (current-buffer))))) + (when current-line + (goto-line current-line) + (move-to-column current-column))))) + +(defun jabber-display-roster-entry (buddy) + "Format and insert a roster entry for BUDDY at point." + (let ((buddy-str (format-spec jabber-roster-line-format + (list + (cons ?c (if (get buddy 'connected) "*" " ")) + (cons ?u (cdr (assoc (or (get buddy 'subscription) "none") + jabber-roster-subscription-display))) + (cons ?n (if (> (length (get buddy 'name)) 0) + (get buddy 'name) + (symbol-name buddy))) + (cons ?j (symbol-name buddy)) + (cons ?r (or (get buddy 'resource) "")) + (cons ?s (or + (cdr (assoc (get buddy 'show) jabber-presence-strings)) + (get buddy 'show))) + (cons ?S (if (get buddy 'status) + (jabber-fix-status (get buddy 'status)) + "")))))) + (add-text-properties 0 + (length buddy-str) + (list + 'face + (or (cdr (assoc (get buddy 'show) jabber-presence-faces)) + 'jabber-roster-user-online) + ;;'mouse-face + ;;(cons 'background-color "light grey") + 'help-echo + (symbol-name buddy) + 'jabber-jid + (symbol-name buddy)) + buddy-str) + ;; (let ((map (make-sparse-keymap)) + ;; (chat-with-func (make-symbol (concat "jabber-chat-with" (symbol-name buddy))))) + ;; (fset chat-with-func `(lambda () (interactive) (jabber-chat-with ,(symbol-name buddy)))) + ;; (define-key map [mouse-2] chat-with-func) + ;; (put-text-property 0 + ;; (length buddy-str) + ;; 'keymap + ;; map + ;; buddy-str)) + (insert buddy-str) + + (when (or (eq jabber-show-resources 'always) + (and (eq jabber-show-resources 'sometimes) + (> (jabber-count-connected-resources buddy) 1))) + (dolist (resource (get buddy 'resources)) + (when (plist-get (cdr resource) 'connected) + (let ((resource-str (format-spec jabber-resource-line-format + (list + (cons ?c "*") + (cons ?n (if (> (length (get buddy 'name)) 0) + (get buddy 'name) + (symbol-name buddy))) + (cons ?j (symbol-name buddy)) + (cons ?r (if (> (length (car resource)) 0) + (car resource) + "empty")) + (cons ?s (or + (cdr (assoc (plist-get (cdr resource) 'show) jabber-presence-strings)) + (plist-get (cdr resource) 'show))) + (cons ?S (if (plist-get (cdr resource) 'status) + (jabber-fix-status (plist-get (cdr resource) 'status)) + "")) + (cons ?p (number-to-string (plist-get (cdr resource) 'priority))))))) + (add-text-properties 0 + (length resource-str) + (list + 'face + (or (cdr (assoc (plist-get (cdr resource) 'show) jabber-presence-faces)) + 'jabber-roster-user-online) + 'jabber-jid + (format "%s/%s" (symbol-name buddy) (car resource))) + resource-str) + (insert resource-str))))))) + +(defun jabber-presence-update-roster (who &rest ignore) + "Update roster without redrawing all of it, if possible." + + (let* ((bare-jid (jabber-jid-symbol + (jabber-jid-user + (symbol-name who)))) + (entry (assq bare-jid jabber-roster-positions)) + (inhibit-read-only t)) + (jabber-sort-roster) + (if (null entry) + (jabber-display-roster) + (let ((old-start (cadr entry)) + (old-end (cddr entry)) + (insert-before-this (cadr (memq bare-jid *jabber-roster*)))) + (with-current-buffer jabber-roster-buffer + (delete-region old-start old-end) + (save-excursion + (let ((new-start + (marker-position + (if insert-before-this + ;; If this is not the last entry, go to start + ;; position of next entry. + (cadr (assq insert-before-this jabber-roster-positions)) + ;; If this is the last entry, go to end position of second + ;; to last entry. + (cddr (car (last jabber-roster-positions 2))))))) + (goto-char new-start) + (jabber-display-roster-entry bare-jid) + (let ((marker-start (set-marker (make-marker) new-start)) + (marker-end (set-marker (make-marker) (point)))) + ;; Text is inserted before start markers, but after + ;; end markers. + (set-marker-insertion-type marker-start t) + (setcdr entry (cons marker-start marker-end)))))))))) + +(defun jabber-go-to-next-jid () + "Move the cursor to the next jid in the buffer" + (interactive) + (let ((next (next-single-property-change (point) 'jabber-jid))) + (when (and next + (not (get-text-property next 'jabber-jid))) + (setq next (next-single-property-change next 'jabber-jid))) + (unless next + (setq next (next-single-property-change (point-min) 'jabber-jid))) + (if next (goto-char (1+ next)) + (goto-char (point-min))))) + +(provide 'jabber-roster) + +;;; arch-tag: 096af063-0526-4dd2-90fd-bc6b5ba07d32 diff --git a/jabber-sasl.el b/jabber-sasl.el new file mode 100644 index 0000000..9dbfdb4 --- /dev/null +++ b/jabber-sasl.el @@ -0,0 +1,116 @@ +;; jabber-sasl.el - SASL authentication + +;; Copyright (C) 2004 - Magnus Henoch - mange@freemail.hu + +;; 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 'cl) + +;;; This file uses sasl.el from FLIM, and expects to find it. If it +;;; can't be found, jabber-core.el catches the error. +(require 'sasl) + +;;; Alternatives to FLIM would be the command line utility of GNU SASL, +;;; or anything the Gnus people decide to use. + +;;; See XMPP-CORE and XMPP-IM for details about the protocol. + +(require 'jabber-xml) + +(defvar jabber-sasl-mechanism nil) +(defvar jabber-sasl-client nil) +(defvar jabber-sasl-step nil) + +(defun jabber-sasl-start-auth (stream-features) + ;; This shouldn't be necessary + ;;(setq jabber-call-on-connection nil) + + ;; Reset our own state. + (setq jabber-sasl-mechanism nil) + (setq jabber-sasl-client nil) + (setq jabber-sasl-step nil) + + ;; Hijack all stanzas for a while. + (setq jabber-short-circuit-input #'jabber-sasl-process-input) + + ;; Find a suitable common mechanism. + (let ((mechanisms (car (jabber-xml-get-children stream-features 'mechanisms)))) + (setq jabber-sasl-mechanism + (sasl-find-mechanism + (mapcar + (lambda (tag) + (car (jabber-xml-node-children tag))) + (jabber-xml-get-children mechanisms 'mechanism))))) + (if (null jabber-sasl-mechanism) + ;; Maybe we can use legacy authentication + (let ((node (find "http://jabber.org/features/iq-auth" + (jabber-xml-get-children stream-features 'auth) + :key #'(lambda (node) (jabber-xml-get-attribute node 'xmlns)) + :test #'string=))) + (if node + (progn + (setq jabber-short-circuit-input nil) + (jabber-get-auth jabber-server)) + (error "No suitable SASL mechanism found"))) + + ;; Start authentication. + (setq jabber-sasl-client (sasl-make-client jabber-sasl-mechanism jabber-username "xmpp" jabber-server)) + (setq jabber-sasl-step (sasl-next-step jabber-sasl-client nil)) + (jabber-send-sexp + `(auth ((xmlns . "urn:ietf:params:xml:ns:xmpp-sasl") + (mechanism . ,(sasl-mechanism-name jabber-sasl-mechanism))) + ,(when (sasl-step-data jabber-sasl-step) + (base64-encode-string (sasl-step-data jabber-sasl-step) t)))))) + +(defun jabber-sasl-stop () + (setq jabber-short-circuit-input nil)) + +(defun jabber-sasl-process-input (xml-data) + (let ((sasl-read-passphrase #'jabber-read-passwd)) + (cond + ((eq (car xml-data) 'challenge) + (sasl-step-set-data jabber-sasl-step (base64-decode-string (car (jabber-xml-node-children xml-data)))) + (setq jabber-sasl-step (sasl-next-step jabber-sasl-client jabber-sasl-step)) + (jabber-send-sexp + `(response ((xmlns . "urn:ietf:params:xml:ns:xmpp-sasl")) + ,(when (sasl-step-data jabber-sasl-step) + (base64-encode-string (sasl-step-data jabber-sasl-step) t))))) + + ((eq (car xml-data) 'failure) + (ding) + (message "SASL authentication failure: %s" + (jabber-xml-node-name (car (jabber-xml-node-children xml-data)))) + (sit-for 3) + (jabber-disconnect) + (jabber-sasl-stop)) + + ((eq (car xml-data) 'success) + (message "Authentication succeeded") + (setq *jabber-authenticated* t) + (jabber-sasl-stop) + + ;; Now, we send another stream header. + (funcall jabber-conn-send-function + (concat + "<stream:stream to='" + jabber-server + "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>")) + ;; now see what happens +)))) + +(provide 'jabber-sasl) +;;; arch-tag: 2a4a234d-34d3-49dd-950d-518c899c0fd0 diff --git a/jabber-sawfish.el b/jabber-sawfish.el new file mode 100644 index 0000000..378eb20 --- /dev/null +++ b/jabber-sawfish.el @@ -0,0 +1,40 @@ +;; jabber-sawfish.el - emacs-jabber interface to sawfish + +;; Copyright (C) 2005 - Mario Domenech Goulart + +;; 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 + +(defcustom jabber-sawfish-display-time 3 + "Time in seconds for displaying a jabber message through the +Sawfish window manager." + :type 'integer + :group 'jabber-alerts) + +(defun jabber-sawfish-display-message (message) + "Displays MESSAGE through the Sawfish window manager." + (let ((process-connection-type nil)) + (start-process-shell-command + "jabber-sawfish" nil "echo" + (concat "'(progn (display-message \"" + message + "\")(make-timer (lambda () (display-message nil)) 3))' | sawfish-client - &> /dev/null")))) + +(define-jabber-alert sawfish "Display a message through the Sawfish window manager" + 'jabber-sawfish-display-message) + +(provide 'jabber-sawfish) +;; arch-tag: 4F0154ED-5D05-11D9-9E6B-000A95C2FCD0 diff --git a/jabber-screen.el b/jabber-screen.el new file mode 100644 index 0000000..7b00320 --- /dev/null +++ b/jabber-screen.el @@ -0,0 +1,29 @@ +;; jabber-screen.el - emacs-jabber interface to screen + +;; Copyright (C) 2005 - Magnus Henoch - mange@freemail.hu + +;; 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 + +(defun jabber-screen-message (msg) + "Show MSG in screen" + (call-process "screen" nil nil nil "-X" "echo" msg)) + +(define-jabber-alert screen "Show a message through the Screen terminal manager" + 'jabber-screen-message) + +(provide 'jabber-screen) +;; arch-tag: B576ADDA-5D04-11D9-AA52-000A95C2FCD0 diff --git a/jabber-search.el b/jabber-search.el new file mode 100644 index 0000000..d88a466 --- /dev/null +++ b/jabber-search.el @@ -0,0 +1,115 @@ +;; jabber-search.el - searching by JEP-0055, with x:data support + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-register) + +(add-to-list 'jabber-jid-service-menu + (cons "Search directory" 'jabber-get-search)) +(defun jabber-get-search (to) + "Send IQ get request in namespace \"jabber:iq:search\"." + (interactive (list (jabber-read-jid-completing "Search what database: "))) + (jabber-send-iq to + "get" + '(query ((xmlns . "jabber:iq:search"))) + #'jabber-process-data #'jabber-process-register-or-search + #'jabber-report-success "Search field retrieval")) + +;; jabber-process-register-or-search logically comes here, rendering +;; the search form, but since register and search are so similar, +;; having two functions would be serious code duplication. See +;; jabber-register.el. + +;; jabber-submit-search is called when the "submit" button of the +;; search form is activated. +(defun jabber-submit-search (&rest ignore) + "Submit search. See `jabber-process-register-or-search'." + + (let ((text (concat "Search at " jabber-submit-to))) + (jabber-send-iq jabber-submit-to + "set" + + (cond + ((eq jabber-form-type 'register) + `(query ((xmlns . "jabber:iq:search")) + ,@(jabber-parse-register-form))) + ((eq jabber-form-type 'xdata) + `(query ((xmlns . "jabber:iq:search")) + ,(jabber-parse-xdata-form))) + (t + (error "Unknown form type: %s" jabber-form-type))) + #'jabber-process-data #'jabber-process-search-result + #'jabber-report-success text)) + + (message "Search sent")) + +(defun jabber-process-search-result (xml-data) + "Receive and display search results." + + ;; This function assumes that all search results come in one packet, + ;; which is not necessarily the case. + (let ((query (jabber-iq-query xml-data)) + (have-xdata nil) + xdata fields (jid-fields 0)) + + ;; First, check for results in jabber:x:data form. + (dolist (x (jabber-xml-get-children query 'x)) + (when (string= (jabber-xml-get-attribute x 'xmlns) "jabber:x:data") + (setq have-xdata t) + (setq xdata x))) + + (if have-xdata + (jabber-render-xdata-search-results xdata) + + (insert (jabber-propertize "Search results" 'face 'jabber-title-medium) "\n") + + (setq fields '((first . (label "First name" column 0)) + (last . (label "Last name" column 15)) + (nick . (label "Nickname" column 30)) + (jid . (label "JID" column 45)) + (email . (label "E-mail" column 65)))) + (setq jid-fields 1) + + (dolist (field-cons fields) + (indent-to (plist-get (cdr field-cons) 'column) 1) + (insert (jabber-propertize (plist-get (cdr field-cons) 'label) 'face 'bold))) + (insert "\n\n") + + ;; Now, the items + (dolist (item (jabber-xml-get-children query 'item)) + (let ((start-of-line (point)) + jid) + + (dolist (field-cons fields) + (let ((field-plist (cdr field-cons)) + (value (if (eq (car field-cons) 'jid) + (setq jid (jabber-xml-get-attribute item 'jid)) + (car (jabber-xml-node-children (car (jabber-xml-get-children item (car field-cons)))))))) + (indent-to (plist-get field-plist 'column) 1) + (if value (insert value)))) + + (if jid + (put-text-property start-of-line (point) + 'jabber-jid jid)) + (insert "\n")))))) + +(provide 'jabber-search) + +;;; arch-tag: c39e9241-ab6f-4ac5-b1ba-7908bbae009c diff --git a/jabber-si-client.el b/jabber-si-client.el new file mode 100644 index 0000000..5925698 --- /dev/null +++ b/jabber-si-client.el @@ -0,0 +1,74 @@ +;; jabber-si-client.el - send stream requests, by JEP-0095 + +;; Copyright (C) 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-iq) +(require 'jabber-feature-neg) + +(defvar jabber-si-client-methods nil + "Supported SI stream methods for initiation. + +Each entry is a list, containing: + * The namespace URI of the stream method + * A function taking three arguments: JID, SID and profile function to call") + +(defun jabber-si-initiate (jid profile-namespace profile-data profile-function &optional mime-type) + "Try to initiate a stream to JID. +PROFILE-NAMESPACE is, well, the namespace of the profile to use. +PROFILE-DATA is the XML data to send within the SI request. +PROFILE-FUNCTION is the function to call upon success. +MIME-TYPE is the MIME type to specify. +Returns the SID." + + (let ((sid (apply 'format "emacs-sid-%d.%d.%d" (current-time)))) + (jabber-send-iq jid "set" + `(si ((xmlns . "http://jabber.org/protocol/si") + (id . ,sid) + ,(if mime-type + (cons 'mime-type mime-type)) + (profile . ,profile-namespace)) + ,profile-data + (feature ((xmlns . "http://jabber.org/protocol/feature-neg")) + ,(jabber-fn-encode (list + (cons "stream-method" + (mapcar 'car jabber-si-client-methods))) + 'request))) + #'jabber-si-initiate-process (cons profile-function sid) + ;; XXX: use other function here? + #'jabber-report-success "Stream initiation") + sid)) + +(defun jabber-si-initiate-process (xml-data closure-data) + "Act on response to our SI query." + + (let* ((profile-function (car closure-data)) + (sid (cdr closure-data)) + (from (jabber-xml-get-attribute xml-data 'from)) + (query (jabber-iq-query xml-data)) + (feature-node (car (jabber-xml-get-children query 'feature))) + (feature-alist (jabber-fn-parse feature-node 'response)) + (chosen-method (cadr (assoc "stream-method" feature-alist))) + (method-data (assoc chosen-method jabber-si-client-methods))) + ;; Our work is done. Hand it over to the stream method. + (let ((stream-negotiate (nth 1 method-data))) + (funcall stream-negotiate from sid profile-function)))) + +(provide 'jabber-si-client) + +;;; arch-tag: e14ec451-3f18-4f36-b92a-e8a8aa1f5acd diff --git a/jabber-si-server.el b/jabber-si-server.el new file mode 100644 index 0000000..6c91b01 --- /dev/null +++ b/jabber-si-server.el @@ -0,0 +1,100 @@ +;; jabber-si-server.el - handle incoming stream requests, by JEP-0095 + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-iq) +(require 'jabber-disco) +(require 'jabber-feature-neg) + +(add-to-list 'jabber-advertised-features "http://jabber.org/protocol/si") + +;; Now, stream methods push data to profiles. It could be the other +;; way around; not sure which is better. +(defvar jabber-si-profiles nil + "Supported SI profiles. + +Each entry is a list, containing: + * The namespace URI of the profile + * Accept function, taking entire IQ stanza, and signalling a 'forbidden' + error if request is declined; returning an XML node to return in + response, or nil of none needed + * Data function, taking JID of initiator, stream ID, and string + containing received data in binary form; receives `nil' on EOF. + Returns non-nil to keep connection; nil to close it.") + +(defvar jabber-si-stream-methods nil + "Supported SI stream methods. + +Each entry is a list, containing: + * The namespace URI of the stream method + * Accept function, taking JID of initiator, stream ID, profile + data function (as above), preparing to accept a request") + +(add-to-list 'jabber-iq-set-xmlns-alist + (cons "http://jabber.org/protocol/si" 'jabber-si-process)) +(defun jabber-si-process (xml-data) + + (let* ((to (jabber-xml-get-attribute xml-data 'from)) + (id (jabber-xml-get-attribute xml-data 'id)) + (query (jabber-iq-query xml-data)) + (profile (jabber-xml-get-attribute query 'profile)) + (si-id (jabber-xml-get-attribute query 'id)) + (feature (car (jabber-xml-get-children query 'feature)))) + (message "Receiving SI with profile '%s'" profile) + + (let (stream-method + ;; Find profile + (profile-data (assoc profile jabber-si-profiles))) + ;; Now, feature negotiation for stream type (errors + ;; don't match JEP-0095, so convert) + (condition-case err + (setq stream-method (jabber-fn-intersection + (jabber-fn-parse feature 'request) + (list (cons "stream-method" (mapcar 'car jabber-si-stream-methods))))) + (jabber-error + (jabber-signal-error "cancel" 'bad-request nil + '((no-valid-streams ((xmlns . "http://jabber.org/protocol/si"))))))) + (unless profile-data + ;; profile not understood + (jabber-signal-error "cancel" 'bad-request nil + '((bad-profile ((xmlns . "http://jabber.org/protocol/si")))))) + (let* ((profile-accept-function (nth 1 profile-data)) + ;; accept-function might throw a "forbidden" error + ;; on user cancel + (profile-response (funcall profile-accept-function xml-data)) + (profile-data-function (nth 2 profile-data)) + (stream-method-id (nth 1 (assoc "stream-method" stream-method))) + (stream-data (assoc stream-method-id jabber-si-stream-methods)) + (stream-accept-function (nth 1 stream-data))) + ;; prepare stream for the transfer + (funcall stream-accept-function to si-id profile-data-function) + ;; return result of feature negotiation of stream type + (jabber-send-iq to "result" + `(si ((xmlns . "http://jabber.org/protocol/si")) + ,@profile-response + (feature ((xmlns . "http://jabber.org/protocol/feature-neg")) + ,(jabber-fn-encode stream-method 'response))) + nil nil nil nil + id) + )))) + +(provide 'jabber-si-server) + +;;; arch-tag: d3c75c66-4052-4cf5-8f04-8765adfc8b96 diff --git a/jabber-socks5.el b/jabber-socks5.el new file mode 100644 index 0000000..2045351 --- /dev/null +++ b/jabber-socks5.el @@ -0,0 +1,310 @@ +;; jabber-socks5.el - SOCKS5 bytestreams by JEP-0065 + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-iq) +(require 'jabber-disco) +(require 'jabber-si-server) +(require 'jabber-si-client) + +(defvar jabber-socks5-pending-sessions nil + "List of pending sessions. + +Each entry is a list, containing: + * Stream ID + * Full JID of initiator + * Profile data function, to be called when data is received") + +(defvar jabber-socks5-active-sessions nil + "List of active sessions. + +Each entry is a list, containing: + * Network connection + * Stream ID + * Full JID of initiator + * Profile data function") + +(defcustom jabber-socks5-proxies nil + "JIDs of JEP-0065 proxies to use for file transfer. +Put preferred ones first." + :type '(repeat string) + :group 'jabber +; :set 'jabber-socks5-set-proxies) + ) + +(defvar jabber-socks5-proxies-data nil + "Alist containing information about proxies. +Keys of the alist are strings, the JIDs of the proxies. +Values are \"streamhost\" XML nodes.") + +(add-to-list 'jabber-advertised-features "http://jabber.org/protocol/bytestreams") + +(add-to-list 'jabber-si-stream-methods + (list "http://jabber.org/protocol/bytestreams" + 'jabber-socks5-accept)) + +(add-to-list 'jabber-si-client-methods + (list "http://jabber.org/protocol/bytestreams" + 'jabber-socks5-client-1)) + +(defun jabber-socks5-set-proxies (symbol value) + "Set `jabber-socks5-proxies' and query proxies. +This is the set function of `jabber-socks5-proxies-data'." + (set-default symbol value) + (when *jabber-connected* + (jabber-socks5-query-all-proxies))) + +(defun jabber-socks5-query-all-proxies () + "Ask all proxies in `jabber-socks5-proxies' for connection information." + (interactive) + (setq jabber-socks5-proxies-data nil) + (dolist (proxy jabber-socks5-proxies) + (jabber-socks5-query-proxy proxy))) + +(defun jabber-socks5-query-proxy (jid) + "Query the SOCKS5 proxy specified by JID for IP and port number." + (jabber-send-iq jid "get" + '(query ((xmlns . "http://jabber.org/protocol/bytestreams"))) + #'jabber-socks5-process-proxy-response t + #'jabber-socks5-process-proxy-response nil)) + +(defun jabber-socks5-process-proxy-response (xml-data successp) + "Process response from proxy query." + (let* ((query (jabber-iq-query xml-data)) + (from (jabber-xml-get-attribute xml-data 'from)) + (streamhosts (jabber-xml-get-children query 'streamhost))) + + (let ((existing-entry (assoc from jabber-socks5-proxies-data))) + (when existing-entry + (setq jabber-socks5-proxies-data + (delq existing-entry jabber-socks5-proxies-data)))) + + (when successp + (setq jabber-socks5-proxies-data + (cons (cons from streamhosts) + jabber-socks5-proxies-data))) + (message "%s from %s. %d of %d proxies have answered." + (if successp "Response" "Error") from + (length jabber-socks5-proxies-data) (length jabber-socks5-proxies)))) + +(defun jabber-socks5-accept (jid sid profile-data-function) + "Remember that we are waiting for connection from JID, with stream id SID" + ;; asking the user for permission is done in the profile + (add-to-list 'jabber-socks5-pending-sessions + (list sid jid profile-data-function))) + +(add-to-list 'jabber-iq-set-xmlns-alist + (cons "http://jabber.org/protocol/bytestreams" 'jabber-socks5-process)) +(defun jabber-socks5-process (xml-data) + "Accept IQ get for SOCKS5 bytestream" + (let* ((jid (jabber-xml-get-attribute xml-data 'from)) + (id (jabber-xml-get-attribute xml-data 'id)) + (query (jabber-iq-query xml-data)) + (sid (jabber-xml-get-attribute query 'sid)) + (session (dolist (pending-session jabber-socks5-pending-sessions) + (when (and (equal sid (nth 0 pending-session)) + (equal jid (nth 1 pending-session))) + (return pending-session)))) + (profile-data-function (nth 2 session))) + ;; check that we really are expecting this session + (unless session + (jabber-signal-error "auth" 'not-acceptable)) + + (setq jabber-socks5-pending-sessions (delq session jabber-socks5-pending-sessions)) + ;; find streamhost to connect to + (let* ((streamhosts (jabber-xml-get-children query 'streamhost)) + (streamhost (dolist (streamhost streamhosts) + (let ((connection (jabber-socks5-connect streamhost sid jid (concat jabber-username "@" jabber-server "/" jabber-resource)))) + (when connection + ;; We select the first streamhost that we are able to connect to. + (push (list connection sid jid profile-data-function) + jabber-socks5-active-sessions) + ;; Now set the filter, for the rest of the output + (set-process-filter connection #'jabber-socks5-filter) + (set-process-sentinel connection #'jabber-socks5-sentinel) + (return streamhost)))))) + (unless streamhost + (jabber-signal-error "cancel" 'item-not-found)) + + ;; tell initiator which streamhost we use + (jabber-send-iq jid "result" + `(query ((xmlns . "http://jabber.org/protocol/bytestreams")) + (streamhost-used ((jid . ,(jabber-xml-get-attribute streamhost 'jid))))) + nil nil nil nil id) + ;; now, as data is sent, it will be passed to the profile. + ))) + +(defun jabber-socks5-connect (streamhost sid initiator target) + "Attempt to connect to STREAMHOST, authenticating with SID, INITIATOR and TARGET. +Return nil on error. Return connection object on success. + +STREAMHOST has the form +\(streamhost ((host . HOST) + (port . PORT))) + +Zeroconf is not supported." + (message "Attempting SOCKS5 connection to %s (%s->%s, %s)" streamhost initiator target sid) + (condition-case e + (let ((coding-system-for-read 'binary) + (coding-system-for-write 'binary) + (host (jabber-xml-get-attribute streamhost 'host)) + (port (string-to-number (jabber-xml-get-attribute streamhost 'port)))) + ;; is this the best way to send binary network output? + (let ((socks5-connection (open-network-stream "socks5" (generate-new-buffer-name "socks5") host port))) + (with-current-buffer (process-buffer socks5-connection) + ;; version: 5. number of auth methods supported: 1. + ;; which one: no authentication. + (process-send-string socks5-connection (string 5 1 0)) + ;; wait for response + (accept-process-output socks5-connection 15) + ;; should return: + ;; version: 5. auth method to use: none + (unless (string= (buffer-substring 1 3) (string 5 0)) + (error "SOCKS5 authentication required")) + + ;; send connect command + (let ((hash (sha1-string (concat sid initiator target)))) + (process-send-string + socks5-connection + (concat (string 5 1 0 3 (length hash)) + hash + (string 0 0)))) + + (accept-process-output socks5-connection 15) + (unless (string= (buffer-substring 3 5) (string 5 0)) + (error "SOCKS5 failure")) + + (message "SOCKS5 connection established") + + ;; The information returned here is exactly the same that we sent... + ;; Not very exciting. Anyway, this part is done, we have a connection. + (let* ((address-type (aref (buffer-substring 6 7) 0)) + (address-length (aref (buffer-substring 7 8) 0)) + (address (buffer-substring 8 (+ 8 address-length))) + (address-port-string (buffer-substring (+ 8 address-length) (+ 8 address-length 2))) + (address-port (+ + (* 256 (aref address-port-string 0)) + (* 1 (aref address-port-string 1))))) + ;;(message "Address type: %d\nAddress: %s\nPort: %d" address-type address address-port) + + ;; Delete all SOCKS5 data, leave room for the stream. + (delete-region 1 (+ 8 address-length 2))) + + socks5-connection))) + (error + (message "SOCKS5 connection failed: %s" e) + nil))) + +(defun jabber-socks5-filter (connection data) + "Pass data from connection to profile data function" + (let* ((session (assq connection jabber-socks5-active-sessions)) + (sid (nth 1 session)) + (jid (nth 2 session)) + (profile-data-function (nth 3 session))) + ;; If the data function requests it, tear down the connection. + (unless (funcall profile-data-function jid sid data) + (jabber-socks5-sentinel connection nil)))) + +(defun jabber-socks5-sentinel (process event-string) + ;; Connection terminated. Shuffle together the remaining data, + ;; and kill the buffer. + (let* ((session (assq process jabber-socks5-active-sessions)) + (buffer (process-buffer process)) + (sid (nth 1 session)) + (jid (nth 2 session)) + (profile-data-function (nth 3 session))) + (kill-buffer buffer) + (delete-process process) + (funcall profile-data-function jid sid nil) + (setq jabber-socks5-active-sessions (delq session jabber-socks5-pending-sessions)))) + +(defun jabber-socks5-client-1 (jid sid profile-function) + "Negotiate a SOCKS5 connection with JID. +This function simply sends a request; the response is handled elsewhere." + ;; TODO: start our own server if we can. + (unless jabber-socks5-proxies + (error "No proxies defined. Set `jabber-socks5-proxies'.")) + (unless jabber-socks5-proxies-data + (error "No proxy data available. Run `jabber-socks5-query-all-proxies'.")) + + ;; Sort the alist jabber-socks5-proxies-data such that the + ;; keys are in the same order as in jabber-socks5-proxies. + (setq jabber-socks5-proxies-data + (sort jabber-socks5-proxies-data + #'(lambda (a b) + (> (length (member (car a) jabber-socks5-proxies)) + (length (member (car b) jabber-socks5-proxies)))))) + + (jabber-send-iq jid "set" + `(query ((xmlns . "http://jabber.org/protocol/bytestreams") + (sid . ,sid)) + ,@(mapcar + #'(lambda (proxy) + (mapcar + #'(lambda (streamhost) + (list 'streamhost + (list (cons 'jid (jabber-xml-get-attribute streamhost 'jid)) + (cons 'host (jabber-xml-get-attribute streamhost 'host)) + (cons 'port (jabber-xml-get-attribute streamhost 'port))))) + (cdr proxy))) + jabber-socks5-proxies-data)) + `(lambda (xml-data closure-data) + (jabber-socks5-client-2 xml-data ,jid ,sid ,profile-function)) nil + ;; TODO: error handling + #'jabber-report-success "SOCKS5 negotiation")) + +(defun jabber-socks5-client-2 (xml-data jid sid profile-function) + "Contact has selected a streamhost to use. Connect to the proxy." + (let* ((query (jabber-iq-query xml-data)) + (streamhost-used (car (jabber-xml-get-children query 'streamhost-used))) + (proxy-used (jabber-xml-get-attribute streamhost-used 'jid)) + connection) + (let ((streamhosts-left (cdr (assoc proxy-used jabber-socks5-proxies-data)))) + (while (and streamhosts-left (not connection)) + (setq connection + (jabber-socks5-connect (car streamhosts-left) + sid + (concat jabber-username "@" jabber-server "/" jabber-resource) + jid)) + (setq streamhosts-left (cdr streamhosts-left)))) + (unless connection + (error "Couldn't connect to proxy %s" proxy-used)) + + ;; Activation is only needed for proxies. + (jabber-send-iq proxy-used "set" + `(query ((xmlns . "http://jabber.org/protocol/bytestreams") + (sid . ,sid)) + (activate () ,jid)) + `(lambda (xml-data closure-data) + (jabber-socks5-client-3 xml-data ,jid ,sid ,profile-function ,connection)) nil + ;; TODO: report error to contact? + #'jabber-report-success "Proxy activation"))) + +(defun jabber-socks5-client-3 (xml-data jid sid profile-function proxy-connection) + "Proxy is activated. Start the transfer." + ;; The response from the proxy does not contain any interesting + ;; information, beyond success confirmation. + + (funcall profile-function jid sid `(lambda (data) + (process-send-string ,proxy-connection data)))) + +(provide 'jabber-socks5) + +;;; arch-tag: 9e70dfea-2522-40c6-a79f-302c8fb82ac5 diff --git a/jabber-util.el b/jabber-util.el new file mode 100644 index 0000000..112d799 --- /dev/null +++ b/jabber-util.el @@ -0,0 +1,444 @@ +;; jabber-util.el - various utility functions + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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 + +(defvar jabber-jid-history nil + "History of entered JIDs") + +(defvar *jabber-sound-playing* nil + "is a sound playing right now?") + +(cond + ((fboundp 'replace-in-string) + (defsubst jabber-replace-in-string (str regexp newtext) + (replace-in-string str regexp newtext t))) + ((fboundp 'replace-regexp-in-string) + (defsubst jabber-replace-in-string (str regexp newtext) + (replace-regexp-in-string regexp newtext str t t)))) + +;;; XEmacs compatibility. Stolen from ibuffer.el +(if (fboundp 'propertize) + (defalias 'jabber-propertize 'propertize) + (defun jabber-propertize (string &rest properties) + "Return a copy of STRING with text properties added. + + [Note: this docstring has been copied from the Emacs 21 version] + +First argument is the string to copy. +Remaining arguments form a sequence of PROPERTY VALUE pairs for text +properties to add to the result." + (let ((str (copy-sequence string))) + (add-text-properties 0 (length str) + properties + str) + str))) + +(unless (fboundp 'bound-and-true-p) + (defmacro bound-and-true-p (var) + "Return the value of symbol VAR if it is bound, else nil." + `(and (boundp (quote ,var)) ,var))) + +;;; more XEmacs compatibility +;;; Preserve input method when entering a minibuffer +(if (featurep 'xemacs) + ;; I don't know how to do this + (defsubst jabber-read-with-input-method (prompt &optional initial-contents history default-value) + (read-string prompt initial-contents history default-value)) + (defsubst jabber-read-with-input-method (prompt &optional initial-contents history default-value) + (read-string prompt initial-contents history default-value t))) + +(unless (fboundp 'delete-and-extract-region) + (defsubst delete-and-extract-region (start end) + (prog1 + (buffer-substring start end) + (delete-region start end)))) + +(unless (fboundp 'access-file) + (defsubst access-file (filename error-message) + (unless (file-readable-p filename) + (error error-message)))) + +(if (fboundp 'float-time) + (defalias 'jabber-float-time 'float-time) + (defun jabber-float-time (&optional specified-time) + (unless specified-time + (setq specified-time (current-time))) + ;; second precision is good enough for us + (+ (* 65536.0 (car specified-time)) + (cadr specified-time)))) + +(cond + ((fboundp 'cancel-timer) + (defalias 'jabber-cancel-timer 'cancel-timer)) + ((fboundp 'delete-itimer) + (defalias 'jabber-cancel-timer 'delete-itimer)) + (t + (error "No `cancel-timer' function found"))) + +(defun jabber-jid-username (string) + "return the username portion of a JID, or nil if no username" + (when (string-match "\\(.*\\)@.*\\(/.*\\)?" string) + (match-string 1 string))) + +(defun jabber-jid-user (string) + "return the user (username@server) portion of a JID" + ;;transports don't have @, so don't require it + ;;(string-match ".*@[^/]*" string) + (string-match "[^/]*" string) + (match-string 0 string)) + +(defun jabber-jid-server (string) + "Return the server portion of a JID." + (string-match "^\\(.*@\\)?\\([^@/]+\\)\\(/.*\\)?$" string) + (match-string 2 string)) + +(defun jabber-jid-rostername (string) + "return the name of the user, if given in roster, else nil" + (let ((user (jabber-jid-symbol string))) + (if (> (length (get user 'name)) 0) + (get user 'name)))) + +(defun jabber-jid-displayname (string) + "return the name of the user, if given in roster, else username@server" + (or (jabber-jid-rostername string) + (jabber-jid-user (if (symbolp string) + (symbol-name string) + string)))) + +(defun jabber-jid-resource (string) + "return the resource portion of a JID, or nil if there is none." + (when (string-match "^\\(\\([^/]*@\\)?[^/]*\\)/\\(.*\\)" string) + (match-string 3 string))) + +(defun jabber-jid-symbol (string) + "return the symbol for the given JID" + ;; If it's already a symbol, just return it. + (if (symbolp string) + string + ;; XXX: "downcase" is poor man's nodeprep. See XMPP CORE. + (intern (downcase (jabber-jid-user string)) jabber-jid-obarray))) + +(defun jabber-my-jid-p (jid) + "Return non-nil if the specified JID is equal to the user's JID, modulo resource." + (equal (jabber-jid-user jid) + (concat jabber-username "@" jabber-server))) + +(defun jabber-read-jid-completing (prompt &optional subset require-match default) + "read a jid out of the current roster from the minibuffer. +If SUBSET is non-nil, it should be a list of symbols from which +the JID is to be selected, instead of using the entire roster. +If REQUIRE-MATCH is non-nil, the JID must be in the list used. +If DEFAULT is non-nil, it's used as the default value, otherwise +the default is inferred from context." + (let ((jid-at-point (or + (and default + ;; default can be either a symbol or a string + (if (symbolp default) + (symbol-name default) + default)) + (get-text-property (point) 'jabber-jid) + (bound-and-true-p jabber-chatting-with) + (bound-and-true-p jabber-group))) + (completion-ignore-case t) + (jid-completion-table (mapcar #'(lambda (item) + (cons (symbol-name item) item)) + (or subset *jabber-roster*)))) + (dolist (item (or subset *jabber-roster*)) + (if (get item 'name) + (push (cons (get item 'name) item) jid-completion-table))) + ;; if the default is not in the allowed subset, it's not a good default + (if (and subset (not (assoc jid-at-point jid-completion-table))) + (setq jid-at-point nil)) + (let ((input + (completing-read (concat prompt + (if jid-at-point + (format "(default %s) " jid-at-point))) + jid-completion-table + nil require-match nil 'jabber-jid-history jid-at-point))) + (if (and input (assoc-ignore-case input jid-completion-table)) + (symbol-name (cdr (assoc-ignore-case input jid-completion-table))) + (and (not (zerop (length input))) + input))))) + +(defun jabber-read-node (prompt) + "Read node name, taking default from disco item at point." + (let ((node-at-point (get-text-property (point) 'jabber-node))) + (read-string (concat prompt + (if node-at-point + (format "(default %s) " node-at-point))) + node-at-point))) + +(defun jabber-read-passwd (&optional prompt) + "Read Jabber password, either from customized variable or from minibuffer. +See `jabber-password'." + (or jabber-password (read-passwd (or prompt "Jabber password: ")))) + +(defun jabber-iq-query (xml-data) + "Return the query part of an IQ stanza. +An IQ stanza may have zero or one query child, and zero or one <error/> child. +The query child is often but not always <query/>." + (let (query) + (dolist (x (jabber-xml-node-children xml-data)) + (if (and + (listp x) + (not (eq (jabber-xml-node-name x) 'error))) + (setq query x))) + query)) + +(defun jabber-iq-error (xml-data) + "Return the <error/> part of an IQ stanza, if any." + (car (jabber-xml-get-children xml-data 'error))) + +(defun jabber-iq-xmlns (xml-data) + "Return the namespace of an IQ stanza, i.e. the namespace of its query part." + (jabber-xml-get-attribute (jabber-iq-query xml-data) 'xmlns)) + +(defun jabber-x-delay (xml-data) + "Return timestamp given a <x/> tag in namespace jabber:x:delay. +Return nil if no such data available." + (when (and (eq (jabber-xml-node-name xml-data) 'x) + (string= (jabber-xml-get-attribute xml-data 'xmlns) "jabber:x:delay")) + (let ((stamp (jabber-xml-get-attribute xml-data 'stamp))) + (if (and (stringp stamp) + (= (length stamp) 17)) + (jabber-parse-legacy-time stamp))))) + +(defun jabber-parse-legacy-time (timestamp) + "Parse timestamp in ccyymmddThh:mm:ss format (UTC) and return as internal time value." + (let ((year (string-to-number (substring timestamp 0 4))) + (month (string-to-number (substring timestamp 4 6))) + (day (string-to-number (substring timestamp 6 8))) + (hour (string-to-number (substring timestamp 9 11))) + (minute (string-to-number (substring timestamp 12 14))) + (second (string-to-number (substring timestamp 15 17)))) + (encode-time second minute hour day month year 0))) + +(defun jabber-encode-legacy-time (timestamp) + "Parse TIMESTAMP as internal time value and encode as ccyymmddThh:mm:ss (UTC)." + (if (featurep 'xemacs) + ;; XEmacs doesn't have `universal' argument to format-time-string, + ;; so we have to do it ourselves. + (format-time-string "%Y%m%dT%H:%M:%S" + (time-subtract timestamp + (list 0 (car (current-time-zone))))) + (format-time-string "%Y%m%dT%H:%M:%S" timestamp t))) + +(defun jabber-encode-time (time) + "Convert TIME to a string by JEP-0082. +TIME is in a format accepted by `format-time-string'." + (let ((time-zone-offset (nth 0 (current-time-zone)))) + (if (null time-zone-offset) + ;; no time zone information available; pretend it's UTC + (format-time-string "%Y-%m-%dT%H:%M:%SZ" time) + (let* ((positivep (>= time-zone-offset 0)) + (hours (/ (abs time-zone-offset) 3600)) + (minutes (/ (% (abs time-zone-offset) 3600) 60))) + (format "%s%s%02d:%02d" (format-time-string "%Y-%m-%dT%H:%M:%S" time) + (if positivep "+" "-") hours minutes))))) + +(defun jabber-parse-time (time) + "Parse the DateTime encoded in TIME according to JEP-0082." + (let* ((year (string-to-number (substring time 0 4))) + (month (string-to-number (substring time 5 7))) + (day (string-to-number (substring time 8 10))) + (hour (string-to-number (substring time 11 13))) + (minute (string-to-number (substring time 14 16))) + (second (string-to-number (substring time 17 19))) + ;; fractions are optional + (fraction (if (eq (aref time 19) ?.) + (string-to-number (substring time 20 23)))) + (timezone (substring time (if fraction 23 19)))) + ;; timezone is either Z (UTC) or [+-]HH:MM + (let ((timezone-seconds + (if (string= timezone "Z") + 0 + (* (if (eq (aref timezone 0) ?+) 1 -1) + (* 60 (+ (* 60 (string-to-number (substring timezone 1 3))) + (string-to-number (substring timezone 4 6)))))))) + (encode-time second minute hour day month year timezone-seconds)))) + +(defun jabber-report-success (xml-data context) + "IQ callback reporting success or failure of the operation. +CONTEXT is a string describing the action." + (let ((type (jabber-xml-get-attribute xml-data 'type))) + (message (concat context + (if (string= type "result") + " succeeded" + (concat + " failed: " + (let ((the-error (jabber-iq-error xml-data))) + (if the-error + (jabber-parse-error the-error) + "No error message given")))))))) + +(defconst jabber-error-messages + (list + (cons 'bad-request "Bad request") + (cons 'conflict "Conflict") + (cons 'feature-not-implemented "Feature not implemented") + (cons 'forbidden "Forbidden") + (cons 'gone "Gone") + (cons 'internal-server-error "Internal server error") + (cons 'item-not-found "Item not found") + (cons 'jid-malformed "JID malformed") + (cons 'not-acceptable "Not acceptable") + (cons 'not-allowed "Not allowed") + (cons 'not-authorized "Not authorized") + (cons 'payment-required "Payment required") + (cons 'recipient-unavailable "Recipient unavailable") + (cons 'redirect "Redirect") + (cons 'registration-required "Registration required") + (cons 'remote-server-not-found "Remote server not found") + (cons 'remote-server-timeout "Remote server timeout") + (cons 'resource-constraint "Resource constraint") + (cons 'service-unavailable "Service unavailable") + (cons 'subscription-required "Subscription required") + (cons 'undefined-condition "Undefined condition") + (cons 'unexpected-request "Unexpected request")) + "String descriptions of XMPP stanza errors") + +(defconst jabber-legacy-error-messages + (list + (cons 302 "Redirect") + (cons 400 "Bad request") + (cons 401 "Unauthorized") + (cons 402 "Payment required") + (cons 403 "Forbidden") + (cons 404 "Not found") + (cons 405 "Not allowed") + (cons 406 "Not acceptable") + (cons 407 "Registration required") + (cons 408 "Request timeout") + (cons 409 "Conflict") + (cons 500 "Internal server error") + (cons 501 "Not implemented") + (cons 502 "Remote server error") + (cons 503 "Service unavailable") + (cons 504 "Remote server timeout") + (cons 510 "Disconnected")) + "String descriptions of legacy errors (JEP-0086)") + +(defun jabber-parse-error (error-xml) + "Parse the given <error/> tag and return a string fit for human consumption. +See secton 9.3, Stanza Errors, of XMPP Core, and JEP-0086, Legacy Errors." + (let ((error-type (jabber-xml-get-attribute error-xml 'type)) + (error-code (jabber-xml-get-attribute error-xml 'code)) + condition text) + (if error-type + ;; If the <error/> tag has a type element, it is new-school. + (dolist (child (jabber-xml-node-children error-xml)) + (when (string= + (jabber-xml-get-attribute child 'xmlns) + "urn:ietf:params:xml:ns:xmpp-stanzas") + (if (eq (jabber-xml-node-name child) 'text) + (setq text (car (jabber-xml-node-children child))) + (setq condition + (or (cdr (assq (jabber-xml-node-name child) jabber-error-messages)) + (symbol-name (jabber-xml-node-name child))))))) + (setq condition (or (cdr (assq (string-to-number error-code) jabber-legacy-error-messages)) + error-code)) + (setq text (car (jabber-xml-node-children error-xml)))) + (concat condition + (if text (format ": %s" text))))) + +(defvar jabber-stream-error-messages + (list + (cons 'bad-format "Bad XML format") + (cons 'bad-namespace-prefix "Bad namespace prefix") + (cons 'conflict "Conflict") + (cons 'connection-timeout "Connection timeout") + (cons 'host-gone "Host gone") + (cons 'host-unknown "Host unknown") + (cons 'improper-addressing "Improper addressing") ; actually only s2s + (cons 'internal-server-error "Internal server error") + (cons 'invalid-from "Invalid from") + (cons 'invalid-id "Invalid id") + (cons 'invalid-namespace "Invalid namespace") + (cons 'invalid-xml "Invalid XML") + (cons 'not-authorized "Not authorized") + (cons 'policy-violation "Policy violation") + (cons 'remote-connection-failed "Remote connection failed") + (cons 'resource-constraint "Resource constraint") + (cons 'restricted-xml "Restricted XML") + (cons 'see-other-host "See other host") + (cons 'system-shutdown "System shutdown") + (cons 'undefined-condition "Undefined condition") + (cons 'unsupported-encoding "Unsupported encoding") + (cons 'unsupported-stanza-type "Unsupported stanza type") + (cons 'unsupported-version "Unsupported version") + (cons 'xml-not-well-formed "XML not well formed")) + "String descriptions of XMPP stream errors") + +(defun jabber-parse-stream-error (error-xml) + "Parse the given <stream:error/> tag and return a sting fit for human consumption." + (let ((text-node (car (jabber-xml-get-children error-xml 'text))) + condition) + ;; as we don't know the node name of the condition, we have to + ;; search for it. + (dolist (node (jabber-xml-node-children error-xml)) + (when (and (string= (jabber-xml-get-attribute node 'xmlns) + "urn:ietf:params:xml:ns:xmpp-streams") + (assq (jabber-xml-node-name node) + jabber-stream-error-messages)) + (setq condition (jabber-xml-node-name node)) + (return))) + (concat (if condition (cdr (assq condition jabber-stream-error-messages)) + "Unknown stream error") + (if (and text-node (stringp (car (jabber-xml-node-children text-node)))) + (concat ": " (car (jabber-xml-node-children text-node))))))) + +(put 'jabber-error + 'error-conditions + '(error jabber-error)) +(put 'jabber-error + 'error-message + "Jabber error") + +(defun jabber-signal-error (error-type condition &optional text app-specific) + "Signal an error to be sent by Jabber. +ERROR-TYPE is one of \"cancel\", \"continue\", \"modify\", \"auth\" +and \"wait\". +CONDITION is a symbol denoting a defined XMPP condition. +TEXT is a string to be sent in the error message, or nil for no text. +APP-SPECIFIC is a list of extra XML tags. + +See section 9.3 of XMPP Core." + (signal 'jabber-error + (list error-type condition text app-specific))) + +(defun jabber-play-sound-file (soundfile) + (if (not *jabber-sound-playing*) + (progn + (setq *jabber-sound-playing* t) + (run-with-idle-timer 0.01 nil + #'(lambda (sf) + (condition-case nil + ;; play-sound-file might display "Could not set sample rate" in + ;; echo area. Don't let this erase the previous message. + (let ((old-message (current-message))) + (play-sound-file sf) + (setq *jabber-sound-playing* nil) + (message "%s" old-message)) + (error (setq *jabber-sound-playing* nil)))) + soundfile)))) + +(provide 'jabber-util) + +;;; arch-tag: cfbb73ac-e2d7-4652-a08d-dc789bcded8a diff --git a/jabber-vcard.el b/jabber-vcard.el new file mode 100644 index 0000000..42cd1d7 --- /dev/null +++ b/jabber-vcard.el @@ -0,0 +1,468 @@ +;;; jabber-vcard.el --- vcards according to JEP-0054 + +;; Copyright (C) 2005 Magnus Henoch + +;; Author: Magnus Henoch <mange@freemail.hu> + +;; 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, 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 GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +;; Boston, MA 02111-1307, USA. + +;;; Commentary: + +;; There are great variations in Jabber vcard implementations. This +;; one adds some spice to the mix, while trying to follow the JEP +;; closely. + +;; Fields not implemented: GEO, LOGO, AGENT, ORG, CATEGORIES, SOUND, +;; CLASS, KEY, PHOTO. + +;; The internal data structure used for vCards is an alist. All +;; keys are uppercase symbols. +;; +;; FN, NICKNAME, BDAY, JABBERID, MAILER, TZ, TITLE, ROLE, NOTE, +;; PRODID, REV, SORT-STRING, UID, URL, DESC: +;; Value is a string. +;; +;; N: +;; Value is an alist, with keys FAMILY, GIVEN, MIDDLE, PREFIX and SUFFIX. +;; +;; ADR: +;; Value is a list, each element representing a separate address. +;; The car of each address is a list of types; possible values are +;; HOME, WORK, POSTAL, PARCEL, DOM, INTL, PREF. +;; The cdr of each address is an alist, with keys POBOX, EXTADD, +;; STREET, LOCALITY, REGION, PCODE, CTRY, and values being strings. +;; +;; TEL: +;; Value is a list, each element representing a separate phone number. +;; The car of each number is a list of types; possible values are +;; HOME, WORK, VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, +;; PCS, PREF +;; The cdr is the phone number as a string. +;; +;; EMAIL: +;; Value is a list, each element representing a separate e-mail address. +;; The car of each address is a list of types; possible values are +;; HOME, WORK, INTERNET, PREF, X400. At least one of INTERNET and +;; X400 is always present. +;; The cdr is the address as a string. + +;;; Code: + +(require 'jabber-core) +(require 'jabber-widget) +(require 'jabber-iq) + +(defun jabber-vcard-parse (vcard) + "Parse the vCard XML structure given in VCARD. +The top node should be the `vCard' node." + ;; Hm... stpeter has a <query/> as top node... + ;;(unless (eq (jabber-xml-node-name vcard) 'vCard) + ;; (error "Invalid vCard")) + (let (result) + (dolist (verbatim-node '(FN NICKNAME BDAY JABBERID MAILER TZ + TITLE ROLE NOTE PRODID REV SORT-STRING + UID URL DESC)) + ;; There should only be one of each of these. They are + ;; used verbatim. + (let ((node (car (jabber-xml-get-children vcard + verbatim-node)))) + ;; Some clients include the node, but without data + (when (car (jabber-xml-node-children node)) + (push (cons (jabber-xml-node-name node) + (car (jabber-xml-node-children node))) + result)))) + + ;; Name components + (let ((node (car (jabber-xml-get-children vcard 'N)))) + ;; Subnodes are FAMILY, GIVEN, MIDDLE, PREFIX, SUFFIX + (push (cons 'N + (let (name) + (dolist (subnode (jabber-xml-node-children node)) + (when (and (memq (jabber-xml-node-name subnode) + '(FAMILY GIVEN MIDDLE PREFIX SUFFIX)) + (not (zerop (length + (car (jabber-xml-node-children + subnode)))))) + (push (cons (jabber-xml-node-name subnode) + (car (jabber-xml-node-children + subnode))) + name))) + name)) + result)) + + ;; There can be several addresses + (let (addresses) + (dolist (adr (jabber-xml-get-children vcard 'ADR)) + ;; Find address type(s) + (let (types) + (dolist (possible-type '(HOME WORK POSTAL PARCEL DOM INTL PREF)) + (when (jabber-xml-get-children adr possible-type) + (push possible-type types))) + + (let (components) + (dolist (component (jabber-xml-node-children adr)) + (when (and (memq (jabber-xml-node-name component) + '(POBOX EXTADD STREET LOCALITY REGION + PCODE CTRY)) + (not (zerop (length + (car (jabber-xml-node-children + component)))))) + (push (cons (jabber-xml-node-name component) + (car (jabber-xml-node-children component))) + components))) + + (push (cons types components) addresses)))) + + (when addresses + (push (cons 'ADR addresses) result))) + + ;; Likewise for phone numbers + (let (phone-numbers) + (dolist (tel (jabber-xml-get-children vcard 'TEL)) + ;; Find phone type(s) + (let ((number (car (jabber-xml-node-children + (car (jabber-xml-get-children tel 'NUMBER))))) + types) + ;; Some clients put no NUMBER node. Avoid that. + (when number + (dolist (possible-type '(HOME WORK VOICE FAX PAGER MSG CELL + VIDEO BBS MODEM ISDN PCS PREF)) + (when (jabber-xml-get-children tel possible-type) + (push possible-type types))) + + (push (cons types number) phone-numbers)))) + + (when phone-numbers + (push (cons 'TEL phone-numbers) result))) + + ;; And for e-mail addresses + (let (e-mails) + (dolist (email (jabber-xml-get-children vcard 'EMAIL)) + (let ((userid (car (jabber-xml-node-children + (car (jabber-xml-get-children email 'USERID))))) + types) + ;; Some clients put no USERID node. Avoid that. + (when userid + (dolist (possible-type '(HOME WORK INTERNET PREF X400)) + (when (jabber-xml-get-children email possible-type) + (push possible-type types))) + (unless (or (memq 'INTERNET types) + (memq 'X400 types)) + (push 'INTERNET types)) + + (push (cons types userid) e-mails)))) + + (when e-mails + (push (cons 'EMAIL e-mails) result))) + + result)) + +(defun jabber-vcard-reassemble (parsed) + "Create a vCard XML structure from PARSED." + `(vCard ((xmlns . "vcard-temp")) + ;; Put in simple fields + ,@(mapcar + (lambda (field) + (when (and (assq (car field) jabber-vcard-fields) + (not (zerop (length (cdr field))))) + (list (car field) nil (cdr field)))) + parsed) + ;; Put in decomposited name + (N nil + ,@(mapcar + (lambda (name-part) + (when (not (zerop (length (cdr name-part)))) + (list (car name-part) nil (cdr name-part)))) + (cdr (assq 'N parsed)))) + ;; Put in addresses + ,@(mapcar + (lambda (address) + (append '(ADR) '(()) + (mapcar 'list (nth 0 address)) + (mapcar (lambda (field) + (list (car field) nil (cdr field))) + (cdr address)))) + (cdr (assq 'ADR parsed))) + ;; Put in phone numbers + ,@(mapcar + (lambda (phone) + (append '(TEL) '(()) + (mapcar 'list (car phone)) + (list (list 'NUMBER nil (cdr phone))))) + (cdr (assq 'TEL parsed))) + ;; Put in e-mail addresses + ,@(mapcar + (lambda (email) + (append '(EMAIL) '(()) + (mapcar 'list (car email)) + (list (list 'USERID nil (cdr email))))) + (cdr (assq 'EMAIL parsed))))) + +(add-to-list 'jabber-jid-info-menu + (cons "Request vcard" 'jabber-vcard-get)) + +(defun jabber-vcard-get (jid) + "Request vcard from JID." + (interactive (list (jabber-read-jid-completing "Request vcard from: "))) + (jabber-send-iq jid + "get" + '(vCard ((xmlns . "vcard-temp"))) + #'jabber-process-data #'jabber-vcard-display + #'jabber-process-data "Vcard request failed")) + +(defun jabber-vcard-edit () + "Edit your own vcard." + (interactive) + (jabber-send-iq nil + "get" + '(vCard ((xmlns . "vcard-temp"))) + #'jabber-vcard-do-edit nil + #'jabber-report-success "Vcard request failed")) + +(defconst jabber-vcard-fields '((FN . "Full name") + (NICKNAME . "Nickname") + (BDAY . "Birthday") + (URL . "URL") + (JABBERID . "JID") + (MAILER . "User agent") + (TZ . "Time zone") + (TITLE . "Title") + (ROLE . "Role") + (REV . "Last changed") + (DESC . "Description") + (NOTE . "Note"))) + +(defconst jabber-vcard-name-fields '((PREFIX . "Prefix") + (GIVEN . "Given name") + (MIDDLE . "Middle name") + (FAMILY . "Family name") + (SUFFIX . "Suffix"))) + +(defconst jabber-vcard-phone-types '((HOME . "Home") + (WORK . "Work") + (VOICE . "Voice") + (FAX . "Fax") + (PAGER . "Pager") + (MSG . "Message") + (CELL . "Cell phone") + (VIDEO . "Video") + (BBS . "BBS") + (MODEM . "Modem") + (ISDN . "ISDN") + (PCS . "PCS"))) + +(defconst jabber-vcard-email-types '((HOME . "Home") + (WORK . "Work") + (INTERNET . "Internet") + (X400 . "X400") + (PREF . "Preferred"))) + +(defconst jabber-vcard-address-types '((HOME . "Home") + (WORK . "Work") + (POSTAL . "Postal") + (PARCEL . "Parcel") + (DOM . "Domestic") + (INTL . "International") + (PREF . "Preferred"))) + +(defconst jabber-vcard-address-fields '((POBOX . "Post box") + (EXTADD . "Ext. address") + (STREET . "Street") + (LOCALITY . "Locality") + (REGION . "Region") + (PCODE . "Post code") + (CTRY . "Country"))) + +(defun jabber-vcard-display (xml-data) + "Display received vcard." + (let ((parsed (jabber-vcard-parse (jabber-iq-query xml-data)))) + (dolist (simple-field jabber-vcard-fields) + (let ((field (assq (car simple-field) parsed))) + (when field + (insert (cdr simple-field)) + (indent-to 20) + (insert (cdr field) "\n")))) + + (let ((names (cdr (assq 'N parsed)))) + (when names + (insert "\n") + (dolist (name-field jabber-vcard-name-fields) + (let ((field (assq (car name-field) names))) + (when field + (insert (cdr name-field)) + (indent-to 20) + (insert (cdr field) "\n")))))) + + (let ((email-addresses (cdr (assq 'EMAIL parsed)))) + (when email-addresses + (insert "\n") + (insert (jabber-propertize "E-mail addresses:\n" + 'face 'jabber-title-medium)) + (dolist (email email-addresses) + (insert (mapconcat (lambda (type) + (cdr (assq type jabber-vcard-email-types))) + (car email) + " ")) + (insert ": " (cdr email) "\n")))) + + (let ((phone-numbers (cdr (assq 'TEL parsed)))) + (when phone-numbers + (insert "\n") + (insert (jabber-propertize "Phone numbers:\n" + 'face 'jabber-title-medium)) + (dolist (number phone-numbers) + (insert (mapconcat (lambda (type) + (cdr (assq type jabber-vcard-phone-types))) + (car number) + " ")) + (insert ": " (cdr number) "\n")))) + + (let ((addresses (cdr (assq 'ADR parsed)))) + (when addresses + (insert "\n") + (insert (jabber-propertize "Addresses:\n" + 'face 'jabber-title-medium)) + (dolist (address addresses) + (insert (jabber-propertize + (mapconcat (lambda (type) + (cdr (assq type jabber-vcard-address-types))) + (car address) + " ") + 'face 'jabber-title-small)) + (insert "\n") + (dolist (address-field jabber-vcard-address-fields) + (let ((field (assq (car address-field) address))) + (when field + (insert (cdr address-field)) + (indent-to 20) + (insert (cdr field) "\n"))))))))) + +(defun jabber-vcard-do-edit (xml-data closure-data) + (let ((parsed (jabber-vcard-parse (jabber-iq-query xml-data)))) + (with-current-buffer (get-buffer-create "Edit vcard") + (jabber-init-widget-buffer nil) + + (dolist (simple-field jabber-vcard-fields) + (widget-insert (cdr simple-field)) + (indent-to 15) + (let ((default-value (cdr (assq (car simple-field) parsed)))) + (push (cons (car simple-field) + (widget-create 'editable-field (or default-value ""))) + jabber-widget-alist))) + + (widget-insert "\n") + (push (cons 'N + (widget-create + '(set :tag "Decomposited name" + (cons :tag "Prefix" :format "%t: %v" (const :format "" PREFIX) (string :format "%v")) + (cons :tag "Given name" :format "%t: %v" (const :format "" GIVEN) (string :format "%v")) + (cons :tag "Middle name" :format "%t: %v" (const :format "" MIDDLE) (string :format "%v")) + (cons :tag "Family name" :format "%t: %v" (const :format "" FAMILY) (string :format "%v")) + (cons :tag "Suffix" :format "%t: %v" (const :format "" SUFFIX) (string :format "%v"))) + :value (cdr (assq 'N parsed)))) + jabber-widget-alist) + + (widget-insert "\n") + (push (cons 'ADR + (widget-create + '(repeat :tag "Postal addresses" + (cons + :tag "Address" + (set :tag "Type" + (const :tag "Home" HOME) + (const :tag "Work" WORK) + (const :tag "Postal" POSTAL) + (const :tag "Parcel" PARCEL) + (const :tag "Domestic" DOM) + (const :tag "International" INTL) + (const :tag "Preferred" PREF)) + (set + :tag "Address" + (cons :tag "Post box" :format "%t: %v" + (const :format "" POBOX) (string :format "%v")) + (cons :tag "Ext. address" :format "%t: %v" + (const :format "" EXTADD) (string :format "%v")) + (cons :tag "Street" :format "%t: %v" + (const :format "" STREET) (string :format "%v")) + (cons :tag "Locality" :format "%t: %v" + (const :format "" LOCALITY) (string :format "%v")) + (cons :tag "Region" :format "%t: %v" + (const :format "" REGION) (string :format "%v")) + (cons :tag "Post code" :format "%t: %v" + (const :format "" PCODE) (string :format "%v")) + (cons :tag "Country" :format "%t: %v" + (const :format "" CTRY) (string :format "%v"))))) + :value (cdr (assq 'ADR parsed)))) + jabber-widget-alist) + + (widget-insert "\n") + (push (cons 'TEL + (widget-create + '(repeat :tag "Phone numbers" + (cons :tag "Number" + (set :tag "Type" + (const :tag "Home" HOME) + (const :tag "Work" WORK) + (const :tag "Voice" VOICE) + (const :tag "Fax" FAX) + (const :tag "Pager" PAGER) + (const :tag "Message" MSG) + (const :tag "Cell phone" CELL) + (const :tag "Video" VIDEO) + (const :tag "BBS" BBS) + (const :tag "Modem" MODEM) + (const :tag "ISDN" ISDN) + (const :tag "PCS" PCS)) + (string :tag "Number"))) + :value (cdr (assq 'TEL parsed)))) + jabber-widget-alist) + + (widget-insert "\n") + (push (cons 'EMAIL + (widget-create + '(repeat :tag "E-mail addresses" + (cons :tag "Address" + (set :tag "Type" + (const :tag "Home" HOME) + (const :tag "Work" WORK) + (const :tag "Internet" INTERNET) + (const :tag "X400" X400) + (const :tag "Preferred" PREF)) + (string :tag "Address"))) + :value (cdr (assq 'EMAIL parsed)))) + jabber-widget-alist) + + (widget-insert "\n") + (widget-create 'push-button :notify #'jabber-vcard-submit "Submit") + + (widget-setup) + (widget-minor-mode 1) + (switch-to-buffer (current-buffer))))) + +(defun jabber-vcard-submit (&rest ignore) + (jabber-send-iq nil + "set" + (jabber-vcard-reassemble + (mapcar (lambda (entry) + (cons (car entry) (widget-value (cdr entry)))) + jabber-widget-alist)) + #'jabber-report-success "Changing vCard" + #'jabber-report-success "Changing vCard")) + +(provide 'jabber-vcard) +;; arch-tag: 65B95E9C-63BD-11D9-94A9-000A95C2FCD0 diff --git a/jabber-version.el b/jabber-version.el new file mode 100644 index 0000000..c59ca9f --- /dev/null +++ b/jabber-version.el @@ -0,0 +1,72 @@ +;; jabber-version.el - version reporting by JEP-0092 + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-iq) +(require 'jabber-util) + +(defconst jabber-version "0.6.1pre" + "version returned to those who query us") + +(add-to-list 'jabber-jid-info-menu + (cons "Request software version" 'jabber-get-version)) +(defun jabber-get-version (to) + "Request software version" + (interactive (list (jabber-read-jid-completing "Request version of: "))) + ;; XXX: you will not get any result unless you add the resource to the JID. + (jabber-send-iq to + "get" + '(query ((xmlns . "jabber:iq:version"))) + #'jabber-process-data #'jabber-process-version + #'jabber-process-data "Version request failed")) + +;; called by jabber-process-data +(defun jabber-process-version (xml-data) + "Handle results from jabber:iq:version requests." + + (let ((query (jabber-iq-query xml-data))) + (dolist (x '((name . "Name:\t\t") (version . "Version:\t") (os . "OS:\t\t"))) + (let ((data (car (jabber-xml-node-children (car (jabber-xml-get-children query (car x))))))) + (when data + (insert (cdr x) data "\n")))))) + +(add-to-list 'jabber-iq-get-xmlns-alist (cons "jabber:iq:version" 'jabber-return-version)) +(add-to-list 'jabber-advertised-features "jabber:iq:version") +(defun jabber-return-version (xml-data) + "Return client version as defined in JEP-0092. Sender and ID are +determined from the incoming packet passed in XML-DATA." + ;; Things we might check: does this iq message really have type='get' and + ;; exactly one child, namely query with xmlns='jabber:iq:version'? + ;; Then again, jabber-process-iq should take care of that. + (let ((to (jabber-xml-get-attribute xml-data 'from)) + (id (jabber-xml-get-attribute xml-data 'id))) + (jabber-send-iq to "result" + `(query ((xmlns . "jabber:iq:version")) + (name () "jabber.el") + (version () ,jabber-version) + ;; Booting... /vmemacs.el + ;; Shamelessly stolen from someone's sig. + (os () ,(jabber-escape-xml (emacs-version)))) + nil nil nil nil + id))) + +(provide 'jabber-version) + +;;; arch-tag: 2051dbe7-01b5-401e-bd8a-fe24afb88e1e diff --git a/jabber-watch.el b/jabber-watch.el new file mode 100644 index 0000000..ccf14d0 --- /dev/null +++ b/jabber-watch.el @@ -0,0 +1,72 @@ +;; jabber-watch.el - get notified when certain persons go online + +;; Copyright (C) 2004 - Mathias Dahl +;; Copyright (C) 2004 - Magnus Henoch - mange@freemail.hu + +;; 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-util) + +(defvar jabber-watch-alist nil + "Alist of buddies for which an extra notification should be sent +when they come online, with comment strings as values.") + +(defun jabber-presence-watch (who oldstatus newstatus + statustext proposed-alert) + "Checks if one of your extra-important buddies comes online and +sends a message if that happens. The buddies are stored in +`jabber-watch-alist' and are added and removed by calling +`jabber-watch-add' and `jabber-watch-remove.'" + ;; check that buddy was previously offline and now online + (if (and (null oldstatus) + (not (null newstatus))) + (let ((entry (assq who jabber-watch-alist))) + (when entry + ;; Give an intrusive message. With a window system, + ;; that's easy. + (if window-system + (message-box "%s%s" proposed-alert + (if (cdr entry) (format ": %s" (cdr entry)) "")) + ;; Without a window system, yes-or-no-p should be + ;; sufficient. + (while (not + (yes-or-no-p (format "%s%s Got that? " proposed-alert + (if (cdr entry) (format ": %s" (cdr entry)) "")))))))))) + +(defun jabber-watch-add (buddy &optional comment) + (interactive (list (jabber-read-jid-completing "Add buddy to watch list: ") + (read-string "Comment: "))) + (unless (memq 'jabber-presence-watch jabber-presence-hooks) + (error "jabber-presence-watch is not in jabber-presence-hooks")) + (add-to-list 'jabber-watch-alist (cons + (jabber-jid-symbol buddy) + (and (not (zerop (length comment))) + comment)))) + +(defun jabber-watch-remove (buddy) + (interactive + (list (jabber-read-jid-completing "Remove buddy from watch list: " + (or (mapcar 'car jabber-watch-alist) + (error "Watch list is empty")) + t))) + (setq jabber-watch-alist + (delq (assq (jabber-jid-symbol buddy) jabber-watch-alist) + jabber-watch-alist))) + +(provide 'jabber-watch) + +;; arch-tag: c27299d8-019e-44b5-9529-d67b8682be23 diff --git a/jabber-widget.el b/jabber-widget.el new file mode 100644 index 0000000..8a58c26 --- /dev/null +++ b/jabber-widget.el @@ -0,0 +1,322 @@ +;; jabber-widget.el - display various kinds of forms + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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 'widget) +(require 'wid-edit) +(require 'jabber-util) +(require 'jabber-disco) + +(defvar jabber-widget-alist nil + "Alist of widgets currently used") + +(defvar jabber-form-type nil + "Type of form. One of: +'x-data, jabber:x:data +'register, as used in jabber:iq:register and jabber:iq:search") + +(defvar jabber-submit-to nil + "JID of the entity to which form data is to be sent") + +(add-to-list 'jabber-advertised-features "jabber:x:data") + +(defun jabber-init-widget-buffer (submit-to) + "Setup buffer-local variables for widgets." + (make-local-variable 'jabber-widget-alist) + (make-local-variable 'jabber-submit-to) + (setq jabber-widget-alist nil) + (setq jabber-submit-to submit-to) + (setq buffer-read-only nil) + ;; XXX: This is because data from other queries would otherwise be + ;; appended to this buffer, which would fail since widget buffers + ;; are read-only... or something like that. Maybe there's a + ;; better way. + (rename-uniquely)) + +(defun jabber-render-register-form (query) + "Display widgets from <query/> element in jabber:iq:{register,search} namespace." + (make-local-variable 'jabber-widget-alist) + (setq jabber-widget-alist nil) + (make-local-variable 'jabber-form-type) + (setq jabber-form-type 'register) + + (if (jabber-xml-get-children query 'instructions) + (widget-insert "Instructions: " (car (jabber-xml-node-children (car (jabber-xml-get-children query 'instructions)))) "\n")) + (if (jabber-xml-get-children query 'registered) + (widget-insert "You are already registered. You can change your details here.\n")) + (widget-insert "\n") + + (let ((possible-fields + ;; taken from JEP-0077 + '((username . "Username") + (nick . "Nickname") + (password . "Password") + (name . "Full name") + (first . "First name") + (last . "Last name") + (email . "E-mail") + (address . "Address") + (city . "City") + (state . "State") + (zip . "Zip") + (phone . "Telephone") + (url . "Web page") + (date . "Birth date")))) + (dolist (field (jabber-xml-node-children query)) + (let ((entry (assq (jabber-xml-node-name field) possible-fields))) + (when entry + (widget-insert (cdr entry) "\t") + ;; Special case: when registering a new account, the default + ;; username is the one specified in jabber-username. Things + ;; will break if the user changes that name, though... + (let ((default-value (if (and jabber-register-p + (eq (jabber-xml-node-name field) 'username)) + jabber-username + ""))) + (setq jabber-widget-alist + (cons + (cons (car entry) + (widget-create 'editable-field + :secret (if (eq (car entry) 'password) + ?* nil) + (or (car (jabber-xml-node-children + field)) default-value))) + jabber-widget-alist))) + (widget-insert "\n")))))) + +(defun jabber-parse-register-form () + "Return children of a <query/> tag containing information entered in the widgets of the current buffer." + (mapcar + (lambda (widget-cons) + (list (car widget-cons) + nil + (widget-value (cdr widget-cons)))) + jabber-widget-alist)) + +(defun jabber-render-xdata-form (x &optional defaults) + "Display widgets from <x/> element in jabber:x:data namespace. +DEFAULTS is an alist associating variable names with default values. +DEFAULTS takes precedence over values specified in the form." + (make-local-variable 'jabber-widget-alist) + (setq jabber-widget-alist nil) + (make-local-variable 'jabber-form-type) + (setq jabber-form-type 'xdata) + + (let ((title (car (jabber-xml-node-children (car (jabber-xml-get-children x 'title)))))) + (if (stringp title) + (widget-insert (jabber-propertize title 'face 'jabber-title-medium) "\n\n"))) + (let ((instructions (car (jabber-xml-node-children (car (jabber-xml-get-children x 'instructions)))))) + (if (stringp instructions) + (widget-insert "Instructions: " instructions "\n\n"))) + + (dolist (field (jabber-xml-get-children x 'field)) + (let* ((var (jabber-xml-get-attribute field 'var)) + (label (jabber-xml-get-attribute field 'label)) + (type (jabber-xml-get-attribute field 'type)) + (required (jabber-xml-get-children field 'required)) + (values (jabber-xml-get-children field 'value)) + (options (jabber-xml-get-children field 'option)) + (desc (car (jabber-xml-get-children field 'desc))) + (default-value (assoc var defaults))) + ;; "required" not implemented yet + + (cond + ((string= type "fixed") + (widget-insert (car (jabber-xml-node-children (car values))))) + + ((string= type "text-multi") + (if (or label var) + (widget-insert (or label var) ":\n")) + (push (cons (cons var type) + (widget-create 'text (or (cdr default-value) + (mapconcat #'(lambda (val) + (car (jabber-xml-node-children val))) + values "\n") + ""))) + jabber-widget-alist)) + + ((string= type "list-single") + (if (or label var) + (widget-insert (or label var) ":\n")) + (push (cons (cons var type) + (apply 'widget-create + 'radio-button-choice + :value (or (cdr default-value) + (car (xml-node-children (car values)))) + (mapcar (lambda (option) + `(item :tag ,(jabber-xml-get-attribute option 'label) + :value ,(car (jabber-xml-node-children (car (jabber-xml-get-children option 'value)))))) + options))) + jabber-widget-alist)) + + ((string= type "boolean") + (push (cons (cons var type) + (widget-create 'checkbox + :tag (or label var) + :value (if default-value + (cdr default-value) + (not (null + (member (car (xml-node-children (car values))) '("1" "true"))))))) + jabber-widget-alist) + (if (or label var) + (widget-insert " " (or label var) "\n"))) + + (t ; in particular including text-single and text-private + (if (or label var) + (widget-insert (or label var) ": ")) + (setq jabber-widget-alist + (cons + (cons (cons var type) + (widget-create 'editable-field + :secret (if (string= type "text-private") ?* nil) + (or (cdr default-value) + (car (jabber-xml-node-children (car values))) + ""))) + jabber-widget-alist)))) + (when (and desc (car (jabber-xml-node-children desc))) + (widget-insert "\n" (car (jabber-xml-node-children desc)))) + (widget-insert "\n")))) + +(defun jabber-parse-xdata-form () + "Return an <x/> tag containing information entered in the widgets of the current buffer." + `(x ((xmlns . "jabber:x:data") + (type . "submit")) + ,@(mapcar + (lambda (widget-cons) + (let ((values (jabber-xdata-value-convert (widget-value (cdr widget-cons)) (cdar widget-cons)))) + ;; empty fields are not included + (when values + `(field ((var . ,(caar widget-cons))) + ,@(mapcar + (lambda (value) + (list 'value nil value)) + values))))) + jabber-widget-alist))) + +(defun jabber-xdata-value-convert (value type) + "Convert VALUE from form used by widget library to form required by JEP-0004. +Return a list of strings, each of which to be included as cdata in a <value/> tag." + (cond + ((string= type "boolean") + (if value (list "1") (list "0"))) + ((string= type "text-multi") + (split-string value "[\n\r]")) + (t ; in particular including text-single, text-private and list-single + (if (zerop (length value)) + nil + (list value))))) + +(defun jabber-render-xdata-search-results (xdata) + "Render search results in x:data form." + + (let ((title (car (jabber-xml-get-children xdata 'title)))) + (when title + (insert (jabber-propertize (car (jabber-xml-node-children title)) 'face 'jabber-title-medium) "\n"))) + + (if (jabber-xml-get-children xdata 'reported) + (jabber-render-xdata-search-results-multi xdata) + (jabber-render-xdata-search-results-single xdata))) + +(defun jabber-render-xdata-search-results-multi (xdata) + "Render multi-record search results." + (let (fields + (jid-fields 0)) + (let ((reported (car (jabber-xml-get-children xdata 'reported))) + (column 0)) + (dolist (field (jabber-xml-get-children reported 'field)) + (let (width) + ;; Clever algorithm for estimating width based on field type goes here. + (setq width 20) + + (setq fields + (append + fields + (list (cons (jabber-xml-get-attribute field 'var) + (list 'label (jabber-xml-get-attribute field 'label) + 'type (jabber-xml-get-attribute field 'type) + 'column column))))) + (setq column (+ column width)) + (if (string= (jabber-xml-get-attribute field 'type) "jid-single") + (setq jid-fields (1+ jid-fields)))))) + + (dolist (field-cons fields) + (indent-to (plist-get (cdr field-cons) 'column) 1) + (insert (jabber-propertize (plist-get (cdr field-cons) 'label) 'face 'bold))) + (insert "\n\n") + + ;; Now, the items + (dolist (item (jabber-xml-get-children xdata 'item)) + + (let ((start-of-line (point)) + jid) + + ;; The following code assumes that the order of the <field/>s in each + ;; <item/> is the same as in the <reported/> tag. + (dolist (field (jabber-xml-get-children item 'field)) + (let ((field-plist (cdr (assoc (jabber-xml-get-attribute field 'var) fields))) + (value (car (jabber-xml-node-children (car (jabber-xml-get-children field 'value)))))) + + (indent-to (plist-get field-plist 'column) 1) + + ;; Absent values are sometimes "", sometimes nil. insert + ;; doesn't like nil. + (when value + ;; If there is only one JID field, let the whole row + ;; have the jabber-jid property. If there are many JID + ;; fields, the string belonging to each field has that + ;; property. + (if (string= (plist-get field-plist 'type) "jid-single") + (if (not (eq jid-fields 1)) + (insert (jabber-propertize value 'jabber-jid value)) + (setq jid value) + (insert value)) + (insert value))))) + + (if jid + (put-text-property start-of-line (point) + 'jabber-jid jid)) + (insert "\n"))))) + +(defun jabber-render-xdata-search-results-single (xdata) + "Render single-record search results." + (dolist (field (jabber-xml-get-children xdata 'field)) + (let ((label (jabber-xml-get-attribute field 'label)) + (type (jabber-xml-get-attribute field 'type)) + (values (mapcar #'(lambda (val) + (car (jabber-xml-node-children val))) + (jabber-xml-get-children field 'value)))) + ;; XXX: consider type + (insert (jabber-propertize (concat label ": ") 'face 'bold)) + (indent-to 30) + (insert (apply #'concat values) "\n")))) + +(defun jabber-xdata-formtype (x) + "Return the form type of the xdata form in X, by JEP-0068. +Return nil if no form type is specified." + (catch 'found-formtype + (dolist (field (jabber-xml-get-children x 'field)) + (when (and (string= (jabber-xml-get-attribute field 'var) "FORM_TYPE") + (string= (jabber-xml-get-attribute field 'type) "hidden")) + (throw 'found-formtype (car (jabber-xml-node-children + (car (jabber-xml-get-children field 'value))))))))) + +(provide 'jabber-widget) + +;;; arch-tag: da3312f3-1970-41d5-a974-14b8d76156b8 diff --git a/jabber-xmessage.el b/jabber-xmessage.el new file mode 100644 index 0000000..f15334d --- /dev/null +++ b/jabber-xmessage.el @@ -0,0 +1,30 @@ +;; jabber-xmessage.el - emacs-jabber interface to xmessage + +;; Copyright (C) 2005 - Mario Domenech Goulart + +;; 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 + +(defun jabber-xmessage-display-message (message) + "Displays MESSAGE using the xmessage program." + (let ((process-connection-type nil)) + (start-process "xmessage" nil "xmessage" message))) + +(define-jabber-alert xmessage "Display a message using the xmessage program." + 'jabber-xmessage-display-message) + +(provide 'jabber-xmessage) +;; arch-tag: 10A74D00-5D2C-11D9-A294-000A95C2FCD0 diff --git a/jabber-xml.el b/jabber-xml.el new file mode 100644 index 0000000..f782133 --- /dev/null +++ b/jabber-xml.el @@ -0,0 +1,206 @@ +;; jabber-xml.el - XML functions + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; 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 'xml) +(require 'jabber-util) + +(defun jabber-escape-xml (str) + "escape strings for xml" + (if (stringp str) + (let ((newstr (concat str))) + ;; Form feeds might appear in code you copy, etc. Nevertheless, + ;; it's invalid XML. + (setq newstr (jabber-replace-in-string newstr "\f" "\n")) + ;; Other control characters are also illegal, except for + ;; tab, CR, and LF. + (setq newstr (jabber-replace-in-string newstr "[\000-\010\013\014\016-\037]" " ")) + (setq newstr (jabber-replace-in-string newstr "&" "&")) + (setq newstr (jabber-replace-in-string newstr "<" "<")) + (setq newstr (jabber-replace-in-string newstr ">" ">")) + (setq newstr (jabber-replace-in-string newstr "'" "'")) + (setq newstr (jabber-replace-in-string newstr "\"" """)) + newstr) + str)) + +(defun jabber-unescape-xml (str) + "unescape xml strings" + ;; Eventually this can be done with `xml-substitute-special', but the + ;; version in xml.el of GNU Emacs 21.3 is buggy. + (if (stringp str) + (let ((newstr str)) + (setq newstr (jabber-replace-in-string newstr """ "\"")) + (setq newstr (jabber-replace-in-string newstr "'" "'")) + (setq newstr (jabber-replace-in-string newstr ">" ">")) + (setq newstr (jabber-replace-in-string newstr "<" "<")) + (setq newstr (jabber-replace-in-string newstr "&" "&")) + newstr) + str)) + +(defun jabber-sexp2xml (sexp) + "converts an SEXP in the format (tagname ((attribute-name . attribute-value)...) children...) and converts it to well-formatted xml." + (cond + ((stringp sexp) + sexp) + ((listp (car sexp)) + (let ((xml "")) + (dolist (tag sexp) + (setq xml (concat xml (jabber-sexp2xml tag)))) + xml)) + ;; work around bug in old versions of xml.el, where ("") can appear + ;; as children of a node + ((and (consp sexp) + (stringp (car sexp)) + (zerop (length (car sexp)))) + "") + (t + (let ((xml "")) + (setq xml (concat "<" + (symbol-name (car sexp)))) + (dolist (attr (cadr sexp)) + (if (consp attr) + (setq xml (concat xml + (format " %s='%s'" + (symbol-name (car attr)) + (cdr attr)))))) + (if (cddr sexp) + (progn + (setq xml (concat xml ">")) + (dolist (child (cddr sexp)) + (setq xml (concat xml + (jabber-sexp2xml child)))) + (setq xml (concat xml + "</" + (symbol-name (car sexp)) + ">"))) + (setq xml (concat xml + "/>"))) + xml)))) + +(defun jabber-xml-skip-tag-forward () + "Skip to end of tag or matching closing tag if present. +Return t iff after a closing tag, otherwise throws an 'unfinished +tag with value nil. + +The version of `sgml-skip-tag-forward' in Emacs 21 isn't good +enough for us." + (skip-chars-forward "^<") + (if (not (looking-at "<\\([^ \t\n/>]+\\)\\([ \t\n]+[^=]+='[^']*'\\|[ \t\n]+[^=]+=\"[^\"]*\"\\)*")) + (throw 'unfinished nil) + (let ((node-name (match-string 1))) + (goto-char (match-end 0)) + (cond + ((looking-at "/>") + (goto-char (match-end 0)) + t) + ((looking-at ">") + (forward-char 1) + (loop + do (skip-chars-forward "^<") + until (looking-at (regexp-quote (concat "</" node-name ">"))) + do (jabber-xml-skip-tag-forward)) + (goto-char (match-end 0)) + t) + (t + (throw 'unfinished nil)))))) + +(defsubst jabber-xml-node-name (node) + "Return the tag associated with NODE. +The tag is a lower-case symbol." + (if (listp node) (car node))) + +(defsubst jabber-xml-node-attributes (node) + "Return the list of attributes of NODE. +The list can be nil." + (if (listp node) (nth 1 node))) + +(defsubst jabber-xml-node-children (node) + "Return the list of children of NODE. +This is a list of nodes, and it can be nil." + (let ((children (cddr node))) + ;; Work around a bug in early versions of xml.el + (if (equal children '((""))) + nil + children))) + +(defun jabber-xml-get-children (node child-name) + "Return the children of NODE whose tag is CHILD-NAME. +CHILD-NAME should be a lower case symbol." + (let ((match ())) + (dolist (child (jabber-xml-node-children node)) + (if child + (if (equal (jabber-xml-node-name child) child-name) + (push child match)))) + (nreverse match))) + +;; `xml-get-attribute' returns "" if the attribute is not found, which +;; is not very useful. Therefore, we use `xml-get-attribute-or-nil' +;; if present, or emulate its behavior. +(if (fboundp 'xml-get-attribute-or-nil) + (defalias 'jabber-xml-get-attribute 'xml-get-attribute-or-nil) + (defsubst jabber-xml-get-attribute (node attribute) + "Get from NODE the value of ATTRIBUTE. +Return nil if the attribute was not found." + (let ((result (xml-get-attribute node attribute))) + (and (> (length result) 0) result)))) + +(defun jabber-xml-path (xml-data path) + "Find sub-node of XML-DATA according to PATH. +PATH is a vaguely XPath-inspired list. Each element can be: + +a symbol go to first child node with this node name +cons cell car is string containing namespace URI, + cdr is string containing node name. Find + first matching child node. +any string character data of this node" + (let ((node xml-data)) + (while (and path node) + (let ((step (car path))) + (cond + ((symbolp step) + (setq node (car (jabber-xml-get-children node step)))) + ((consp step) + ;; This will be easier with namespace-aware use + ;; of xml.el. It will also be more correct. + ;; Now, it only matches explicit namespace declarations. + (setq node + (dolist (x (jabber-xml-get-children node (intern (cdr step)))) + (when (string= (jabber-xml-get-attribute x 'xmlns) + (car step)) + (return x))))) + ((stringp step) + (setq node (car (jabber-xml-node-children node))) + (unless (stringp node) + (setq node nil))) + (t + (error "Unknown path step: %s" step)))) + (setq path (cdr path))) + node)) + +(defmacro jabber-xml-let-attributes (attributes xml-data &rest body) + "Bind variables to the same-name attribute values in XML-DATA." + `(let ,(mapcar #'(lambda (attr) + (list attr `(jabber-xml-get-attribute ,xml-data ',attr))) + attributes) + ,@body)) + +(provide 'jabber-xml) + +;;; arch-tag: ca206e65-7026-4ee8-9af2-ff6a9c5af98a diff --git a/jabber.el b/jabber.el new file mode 100644 index 0000000..9dc1066 --- /dev/null +++ b/jabber.el @@ -0,0 +1,183 @@ +;; jabber.el - a minimal jabber client + +;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net +;; Copyright (C) 2003, 2004 - Magnus Henoch - mange@freemail.hu + +;; SSL - Support, mostly inspired by Gnus +;; Copyright (C) 2005 - Georg Lehner - jorge@magma.com.ni + +;; 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 + +;;; load Unicode tables if this needed +(when (and (featurep 'xemacs) (not (emacs-version>= 21 5 5))) + (require 'un-define)) + +;;; these customize fields should come first +(defgroup jabber nil "Jabber instant messaging" + :group 'applications) + +(defcustom jabber-username "emacs" + "jabber username (user part of JID)" + :type 'string + :group 'jabber) + +(defcustom jabber-server "magaf.org" + "jabber server (domain part of JID)" + :type 'string + :group 'jabber) + +(defcustom jabber-password nil + "jabber password" + :type '(radio (const :tag "Prompt for password" nil) + (string :tag "Save password in .emacs")) + :group 'jabber) + +(defcustom jabber-resource "emacs" + "jabber resource" + :type 'string + :group 'jabber) + +(defcustom jabber-default-show "" + "default show state" + :type '(choice (const :tag "Online" "") + (const :tag "Chatty" "chat") + (const :tag "Away" "away") + (const :tag "Extended away" "xa") + (const :tag "Do not disturb" "dnd")) + :group 'jabber) + +(defcustom jabber-default-status "" + "default status string" + :type 'string + :group 'jabber) + +(defcustom jabber-default-priority 10 + "default priority" + :type 'integer + :group 'jabber) + +(defcustom jabber-nickname jabber-username + "jabber nickname, used in chat buffer prompts and as default groupchat nickname." + :type 'string + :group 'jabber) + +;;; guess internal dependencies! +(require 'jabber-util) +(require 'jabber-menu) +(require 'jabber-xml) +(require 'jabber-conn) +(require 'jabber-core) +(require 'jabber-logon) +(require 'jabber-roster) +(require 'jabber-presence) +(require 'jabber-alert) +(require 'jabber-chat) +(require 'jabber-disco) +(require 'jabber-iq) +(require 'jabber-widget) +(require 'jabber-register) +(require 'jabber-search) +(require 'jabber-browse) +(require 'jabber-muc) +(require 'jabber-version) +(require 'jabber-ahc-presence) +(require 'jabber-modeline) +(require 'jabber-keepalive) +(require 'jabber-watch) +(require 'jabber-activity) +(require 'jabber-vcard) +(require 'jabber-events) + +;; XXX: automate this some time +(autoload 'jabber-export-roster "jabber-export" + "Create buffer from which roster can be exported to a file." + t) +(autoload 'jabber-import-roster "jabber-export" + "Create buffer for roster import from FILE." + t) + +(defvar *jabber-current-status* "" + "the users current presence staus") + +(defvar *jabber-current-show* "" + "the users current presence show") + +(defvar *jabber-current-priority* 10 + "the user's current priority") + +(defvar *jabber-status-history* nil + "history of status messages") + +(defgroup jabber-faces nil "faces for displaying jabber instant messaging" + :group 'jabber) + +(defface jabber-title-small + '((t (:weight bold :width semi-expanded :height 1.0 :inherit variable-pitch))) + "face for small titles" + :group 'jabber-faces) + +(defface jabber-title-medium + '((t (:weight bold :width expanded :height 2.0 :inherit variable-pitch))) + "face for medium titles" + :group 'jabber-faces) + +(defface jabber-title-large + '((t (:weight bold :width ultra-expanded :height 3.0 :inherit variable-pitch))) + "face for large titles" + :group 'jabber-faces) + +(defgroup jabber-debug nil "debugging options" + :group 'jabber) + +(defcustom jabber-debug-log-xml nil + "log all XML i/o in *-jabber-xml-log-*" + :type 'boolean + :group 'jabber-debug) + +(defconst jabber-presence-faces + '(("" . jabber-roster-user-online) + ("away" . jabber-roster-user-away) + ("xa" . jabber-roster-user-xa) + ("dnd" . jabber-roster-user-dnd) + ("chat" . jabber-roster-user-chatty) + ("error" . jabber-roster-user-error) + (nil . jabber-roster-user-offline)) + "Mapping from presence types to faces") + +(defconst jabber-presence-strings + '(("" . "Online") + ("away" . "Away") + ("xa" . "Extended Away") + ("dnd" . "Do not Disturb") + ("chat" . "Chatty") + ("error" . "Error") + (nil . "Offline")) + "Mapping from presence types to readable strings") + +(defun jabber-customize () + "customize jabber options" + (interactive) + (customize-group 'jabber)) + +(defun jabber-info () + "open jabber.el manual" + (interactive) + (info "jabber")) + +(provide 'jabber) + +;;; arch-tag: 5145153e-4d19-4dc2-800c-b1282feb155d diff --git a/jabber.texi b/jabber.texi new file mode 100644 index 0000000..77dde78 --- /dev/null +++ b/jabber.texi @@ -0,0 +1,1863 @@ +\input texinfo @c -*-texinfo-*- +@c %**start of header +@setfilename jabber.info +@settitle jabber.el manual 0.7 +@c %**end of header + +@dircategory Emacs +@direntry +* jabber.el: (jabber). Emacs Jabber client +@end direntry + +@copying +This manual is for jabber.el, version 0.7. + +Copyright @copyright{} 2004, 2005 Magnus Henoch, Tom Berger. + +@quotation +Permission is granted to make and distribute verbatim copies or +modified versions of this manual, provided the copyright notice and +this permission notice are preserved on all copies. +@end quotation +@end copying + +@titlepage +@title jabber.el +@subtitle where Emacs and Jabber meet +@author by Magnus Henoch and Tom Berger + +@page +@vskip 0pt plus 1filll +@insertcopying +@end titlepage + +@contents + +@ifnottex +@node Top, Introduction, (dir), (dir) +@top jabber.el manual + +@insertcopying + +@end ifnottex + +@menu +* Introduction:: +* Basic operation:: +* Groupchat:: +* Services:: +* Personal information:: +* Useful features:: +* Message history:: +* Message events:: +* Roster import and export:: +* Customization:: +* Hacking and extending:: +* Protocol support:: +* Concept index:: +* Function index:: +* Variable index:: +@end menu + + +@node Introduction, Basic operation, Top, Top +@chapter Introduction + +jabber.el is a Jabber client running under Emacs. For more +information on the open-protocol instant messaging network Jabber, +please visit @uref{http://www.jabber.org}. + +As a Jabber client, jabber.el is mostly just a face in the crowd, +except that it uses buffers where GUI clients have windows. There is +a roster buffer, and to chat with someone you open a chat buffer, and +there are browse buffers (increasingly inexactly named) for +interaction with servers and services. Then again, jabber.el delivers +excellent console performance and customizable hooks (if you have +speech synthesizer software, hook it up to your presence alerts). + +jabber.el does not yet support STARTTLS, GPG, sending and receiving +roster items, and various other things. + +@menu +* Contact:: +@end menu + +@node Contact, , Introduction, Introduction +@section Contact + +jabber.el is developed by Tom Berger (e-mail +@email{object@@intellectronica.net}, JID @code{object@@jabber.org.uk}) and +Magnus Henoch (e-mail @email{mange@@freemail.hu}, JID +@code{legoscia@@jabber.cd.chalmers.se}). There is a web page at +@uref{http://intellectronica.net/emacs-jabber/}, and a Sourceforge +project page at @uref{http://sourceforge.net/projects/emacs-jabber}. + +@node Basic operation, Groupchat, Introduction, Top +@chapter Basic operation + +This chapter is intended as an introduction to basic usage of +jabber.el. If you have used Jabber before and are familiar with the +terminology, you might find it a bit too basic --- in that case, just +skim it, making sure to pick up the commands mentioned. + +There are a handful of global keybindings for common commands. They +start with @kbd{C-x C-j}, and you can get a list of them by typing +@kbd{C-x C-j C-h}. + +@menu +* Connecting:: +* Chatting:: +* Presence:: +* Presence subscription:: +* Roster buffer:: +@end menu + +@node Connecting, Chatting, Basic operation, Basic operation +@section Connecting + +@findex jabber-connect +@findex jabber-disconnect + +@cindex Connecting +@cindex Registering an account + +I'll assume that you have already successfully installed jabber.el; if +not, consult the @file{README} file. Also, make sure you have +@code{(require 'jabber)} in your @file{.emacs}. + +Now, type @kbd{M-x jabber-customize}. This brings up a customize +buffer for jabber.el. The most important variables to customize are +@code{jabber-username} and +@code{jabber-server}.@footnote{@xref{Connection settings}, for other +things you might have to change.} Save your changes, and type +@kbd{M-x jabber-connect} to connect. + +If you do not yet have a Jabber account, you can register one. Enter +your desired username for @code{jabber-username} and the server you wish +to use for @code{jabber-server}, save, and type @kbd{C-u M-x +jabber-connect} or @kbd{C-u C-x C-j C-c}. If the server supports +in-band registration, you will be presented with a registration form to +fill out and send. There the username you chose will be prefilled. +Don't change it, otherwise jabber.el will be confused. + +If you successfully connect, jabber.el will download your roster and +display it in a buffer called @code{*-jabber-*}. + +By default, you will appear as ``online'' to your contacts. To change +this to e.g. ``away'', type @kbd{M-x jabber-send-presence}. +@xref{Presence}, for more information. + +To disconnect, type @kbd{M-x jabber-disconnect} or @kbd{C-x C-j C-d}. + +@node Chatting, Presence, Connecting, Basic operation +@section Chatting + +@cindex Chatting +@findex jabber-chat-with + +There are several ways to open a chat buffer. The shortest way is to +put point over the person you want to chat with in the roster display +and hit RET. + +You can also use menus to access commands. In the roster display, you +can access several menus through keystrokes or mouse clicks. You can +bring one big menu up by pressing the second mouse button, or you can +bring up the ``chat menu'' by typing @kbd{C-c C-c}. If you do the +latter while point is on a roster entry, that entry will be the +default value when you are asked for whom to chat with. + +You can also use the function @code{jabber-chat-with}, which is what the +menu item is bound to. This function is bound to @kbd{C-x C-j C-j} in +the global keymap. + +Now, try opening a chat with someone. A buffer named +@code{*-jabber-chat-:-@var{person}-*} will be created and selected. +Type your message at the end of the buffer, and hit @kbd{RET} to send +it. To include a newline in your message, use @kbd{C-j}. + +@node Presence, Presence subscription, Chatting, Basic operation +@section Presence + +@cindex Presence +@cindex Sending presence +@findex jabber-send-presence +@findex jabber-send-default-presence +@vindex jabber-default-show +@vindex jabber-default-status +@vindex jabber-default-priority + +``Presence'' is the Jabber term for letting other people know that you +are online, and additionally how ``available'' you are. There are +three elements to presence: availability status (called ``show''), +status message, and priority. + +Your show status may either be empty (meaning simply ``online''), or +one of @code{away}, @code{xa}, @code{dnd} and @code{chat}, meaning +``away'', ``extended away'' (i.e. away for an extended period), ``do +not disturb'', and ``free for chat'', respectively. This information +is available to everyone subscribing to your presence, but technically +it does not restrict anyone's actions. You can chat with people even +if you claim to be away. + +The status message is a short text complementing your show status, +such as ``at home'', ``working'', ``phone'', ``playing games'' or +whatever you want. It is sent to everyone subscribing to your +presence, but not all clients prominently display it to the user. + +The priority is only interesting if you are running more than one +Jabber client at a time accessing the same account. In that case, +messages sent to you without an indication of which client to send to +are sent to the client with the highest priority. + +To set your presence, use the function @code{jabber-send-presence}. +It can be called both interactively and in Lisp code. For the latter +case, use something like @code{(jabber-send-presence "away" "idle for +10 minutes" 10)}. + +By default, jabber.el sets your presence when you connect. If you +want it not to do that, remove @code{jabber-send-default-presence} +from @code{jabber-post-connect-hook}. If you want to change the +presence that is sent, change the variables +@code{jabber-default-show}, @code{jabber-default-status} and +@code{jabber-default-priority}. + +With jabber.el, you can set your presence remotely. @xref{Ad-Hoc Commands}. + +@node Presence subscription, Roster buffer, Presence, Basic operation +@section Presence subscription + +@cindex Presence subscription +@findex jabber-send-subscription-request + +Having permission to view the presence status of a person is called +@dfn{subscribing to his presence}. Presence subscription between two +persons can be asymmetric. + +When jabber.el receives a presence subscription request, it will +present it to you in an alert requiring immediate response, and offer +you to send a subscription request back to that person. + +To request subscription to someone, type @kbd{M-x +jabber-send-subscription-request}. You will be prompted for the JID +to send it to. This command can also be accessed through the Roster +menu, by typing @kbd{C-c C-r} in the roster buffer. After that, you +will probably want to give the contact a more readable name. The +command for that is @code{jabber-roster-change}, which is also available +in the Roster menu. + +@node Roster buffer, , Presence subscription, Basic operation +@section The roster buffer + +@cindex Roster buffer +@cindex Menus +@cindex Key bindings +@findex jabber-display-roster + +The roster buffer is called @code{*-jabber-*}. It simply contains a +list of the contacts on your roster. + +In the roster buffer, any command which requires a JID will default to +the JID under point when called. These commands can be called through +either keyboard menus or mouse menus. To open a menu with the mouse, +simply press the second mouse button over the JID in +question.@footnote{For some reason, mouse menus don't work in XEmacs. +Patches welcome.} This will bring up a menu with all available +actions. The keyboard menus are split into categories: Chat, Roster, +Information, MUC (Multi-User Chat, or groupchat) and Services, opened +by @kbd{C-c C-c}, @kbd{C-c C-r}, @kbd{C-c C-i}, @kbd{C-c C-m} and +@kbd{C-c C-s}, respectively. + +A list of keybindings is displayed at the top of the roster buffer. +You can turn it off by setting @code{jabber-roster-show-bindings} to +nil. + +You can call @code{jabber-display-roster} to redisplay your roster +according to changed preferences (@pxref{Customizing the roster +buffer}). This will not refetch your roster from the server. +Refetching the roster is usually not needed, since updates are pushed +to clients automatically. + +You can choose not to have the roster updated automatically on +presence changes (@pxref{Presence alerts}). In that case, you need to +call @code{jabber-display-roster} manually. + +@node Groupchat, Services, Basic operation, Top +@chapter Groupchat + +@cindex Groupchat +@cindex MUC +@findex jabber-groupchat-join +@findex jabber-groupchat-leave +@findex jabber-groupchat-get-config +@findex jabber-muc-names +@findex jabber-muc-set-role +@findex jabber-muc-set-topic + +The groupchat menu can be accessed by typing @kbd{C-c C-m} in the +roster buffer. You can also type the commands directly, as will be +shown here. + +To join a groupchat, type @kbd{M-x jabber-groupchat-join}. You will +be prompted for the groupchat to join, and your nickname in the +groupchat. This nickname doesn't need to have any correlation to your +JID; in fact, groupchats are usually (but not always) configured such +that only moderators can see your JID. You can change your nickname +with @kbd{M-x jabber-muc-nick}. + +Groupchat messages will be displayed in a buffer called +@code{*-jabber-groupchat-:-@var{groupchat}-*}. It works much like the +chat buffer. + +To change the topic of a groupchat, type @kbd{M-x +jabber-muc-set-topic}. + +To leave a groupchat, type @kbd{M-x jabber-groupchat-leave}. + +If you are the owner of a groupchat, you can change its configuration +by typing @kbd{M-x jabber-groupchat-get-config}. A configuration form +will be rendered in new buffer. + +To see which people are in a groupchat, type @kbd{M-x +jabber-muc-names}. This gives a list of nicknames, ``roles'', +``affiliations'', and possibly JIDs. @xref{MUC Administration}, for +the meaning of roles and affiliations. + + +@menu +* Automation:: +* Invitations:: +* Private messages:: +* MUC Administration:: +@end menu + +@node Automation, Invitations, Groupchat, Groupchat +@section Automation + +@vindex jabber-muc-default-nicknames +@vindex jabber-muc-autojoin +@findex jabber-muc-autojoin +@cindex Default MUC nickname +@cindex Autojoin chat rooms + +You can select a default nickname by setting @code{jabber-nickname}. +Additionally, you can set different nicknames for different groups, by +customizing @code{jabber-muc-default-nicknames}. There you specify +the JID of the group, and your preferred nickname. + +If you want to automatically join certain rooms when connecting, you +can set @code{jabber-muc-autojoin} to a list containing the JIDs of +the rooms you want to enter. To disable this feature, remove +@code{jabber-muc-autojoin} from @code{jabber-post-connect-hook}. + +@node Invitations, Private messages, Automation, Groupchat +@section Invitations + +@cindex Invitations +@findex jabber-muc-invite + +You can invite someone to a groupchat with @kbd{M-x jabber-muc-invite} +(also available in the MUC menu). Pay attention to the order of the +arguments --- as both users and rooms are just JIDs, it is technically +possible to invite a room to a user, but that's probably not what you +want. + +When you receive an invitation, it appears in the chat buffer along +with two buttons, ``Accept'' and ``Decline''. Pressing ``Accept'' +enters the room, as you would expect. Pressing ``Decline'' gives you +an opportunity to state the reason why you're not joining. + +@node Private messages, MUC Administration, Invitations, Groupchat +@section Private messages + +@cindex Private MUC messages +@findex jabber-muc-private + +You can open a private chat with a participant in a chat room with +@kbd{M-x jabber-muc-private} (or by using the MUC menu). This creates +a buffer with the name +@code{*-jabber-muc-priv-@var{group}-@var{nickname}-*} (customizable by +@code{jabber-muc-private-buffer-format}), which behaves mostly like an +ordinary chat buffer. This buffer will also be created if someone +sends a private message to you. + +Private MUC messages use the same alerts as normal chat messages. +@xref{Message alerts}. + +@node MUC Administration, , Private messages, Groupchat +@section Administration + +The possible roles are: + +@table @samp +@item moderator +Has voice, can change other people's roles. + +@item participant +Has voice. + +@item visitor +Doesn't have voice (can't send messages to everyone, but can send +private messages) + +@item none +Not in room. +@end table + +Affiliations are: + +@table @samp +@item owner +Can destroy room, appoint admins, make people members, ban people. + +@item admin +Can make people members or ban people. + +@item member +Different privileges depending on room configuration (room may be +members-only, or grant voice only to members) + +@item none +Rights depend on room configuration. + +@item outcast +Banned from the room +@end table + +If you have moderator privileges, you can change the role of a +participant with @kbd{M-x jabber-muc-set-role}. Kicking means setting +the role to ``none''. Granting and revoking voice are ``participant'' +and ``visitor'', respectively. ``moderator'' gives moderator +privileges, obviously. The role of a participant is ephemeral, and +disappears when leaving the room. + +If you have admin or owner privileges, you can change the affiliation of +a user. Affiliation is persistent, and based on JIDs. Depending of +your affiliation and the MUC implementation, you might not be allowed to +perform all kinds of changes, and maybe not in one step. + + +@node Services, Personal information, Groupchat, Top +@chapter Services + +@cindex Browse buffers + +Not every Jabber entity is a physical person. There are many +automatic entities, called servers, services, components, agents, +transports and other names. The use of these is described here. + +The functions described in this chapter use @dfn{browse buffers}. +Browse buffers are named @code{*-jabber-browse-:-@var{service}-*}, +sometimes with a numerical suffix. The different menus have the same +keybindings as in the roster buffer, and if you call a function +operating on a JID while point is over a JID, that JID will be the +default value, so you don't have to type it or copy it yourself. + +@menu +* Service discovery and browsing:: +* Registering:: +* Searching:: +* Ad-Hoc Commands:: +@end menu + +@node Service discovery and browsing, Registering, Services, Services +@section Service discovery and browsing + +@cindex Service discovery +@cindex Browsing +@findex jabber-get-browse +@findex jabber-get-disco-items +@findex jabber-get-disco-info + +To find services you want to use, you need to discover them first. +This can be done with either service discovery or browsing. Service +discovery is the newer and preferred protocol, while browsing is still +used by much software. The use of both is very similar. + +The most common use of service discovery is to browse your home +server, to see what services are provided locally. Note, however, +that this is no restriction; you can use services from all over the +network. + +For service discovery there are two commands, +@code{jabber-get-disco-items} and @code{jabber-get-disco-info}, +depending on whether you want information about that specific JID or +about services related to it, respectively. To start browsing, type +@kbd{M-x jabber-get-browse} and enter the JID you want to browse. + +These commands can be accessed from the Info menu, which is opened by +typing @kbd{C-c C-i}. + +If you think that the interface to service discovery is awkward and +should be replaced with something better, you are completely right. + +@node Registering, Searching, Service discovery and browsing, Services +@section Registering + +@cindex Registration +@cindex Cancelling registration +@cindex Changing password +@cindex Gateway registration +@cindex Password change +@findex jabber-get-register + +Some services, in particular user directories and gateways to legacy +IM systems, require registration. To register with such a service, +either type @kbd{M-x jabber-get-register} or select it from the +Service menu, which is opened by typing @kbd{C-c C-s}. You have to +know the service's JID, possibly from service discovery. +(@pxref{Service discovery and browsing}) + +This is also the way to change your registration details, e.g. your +password --- just ask to register with that service again. To change +the password of your Jabber account, ask to register with your Jabber +server. + +Please note that any passwords sent in this way will be sent in +cleartext to your Jabber server, unless you have enabled SSL encryption +(@pxref{Connection settings}), and possibly sent in cleartext from your +server to the server hosting the service. + +jabber.el will then request a registration form from that service. If +for some reason the service does not answer (maybe network problems, +or some services neither support registration nor report errors about +that) that will be the last thing you saw about it. jabber.el will +not report timeout errors, but rather simply wait until you shut it +down. + +Once the response arrives, the form will be rendered in a browse +buffer. Just fill out the fields, and hit Submit. You will receive +confirmation of your registration in the echo area. + +To cancel an existing registration (and also for cancelling your +Jabber account, if you sent a registration request to your server), +hit Cancel. The unregistration will be confirmed in the echo area. + +@node Searching, Ad-Hoc Commands, Registering, Services +@section Searching + +@cindex Searching +@findex jabber-get-search + +Some services, notably user directories and gateways to legacy IM +systems, allow searching. Searching in Jabber generally means +searching for someone's JID, but the protocol is general enough to +support most databases. + +To search a service, either type @kbd{M-x jabber-get-search} or select +it from the Service menu, which is opened by typing @kbd{C-c C-s}. + +Just like with registration, this command sends a request for a search +form, and displays it if and when the response arrives. Enter your +search and submit it. Search results will be displayed in a different +browse buffer. + +@node Ad-Hoc Commands, , Searching, Services +@section Ad-Hoc Commands + +@cindex Ad-Hoc Commands +@findex jabber-ahc-get-list +@findex jabber-ahc-execute-command + +jabber.el supports a subset of JEP-0050, the standard for Ad-Hoc +Commands. As the name implies, this can be used for just about +anything. It has been used for remote-controlling clients (e.g. Psi), +and administering services (e.g. PyMSNt). + +Currently, jabber.el uses ad-hoc commands for setting presence remotely. +If you realize that you forgot to set your client to ``away'' with a low +priority, you can do it remotely.@footnote{Most Jabber servers also +support kicking a client off the net by logging in with another client +with exactly the same resource.} + +The commands for executing ad-hoc commands are available under the +Service menu, which is opened by typing @kbd{C-c C-s}. + +To find which commands are available, run ``Request command-list'' +(@code{jabber-ahc-get-list}).@footnote{This is the same thing as a +disco items request to the node +@code{http://jabber.org/protocol/commands}.} + +To run a command from the list, put point over it and run ``Execute +command'' (@code{jabber-ahc-execute-command}), accepting the defaults +for JID and node. (If you already know those, you could of course +enter them yourself) The form you get should hopefully be +self-explanatory. + +@node Personal information, Useful features, Services, Top +@chapter Personal information + +@cindex vCard +@findex jabber-vcard-get +@findex jabber-vcard-edit + +The Jabber way of handling personal information (name, addresses, +phone numbers, etc) is ``vCards'' encoded in XML. You can get +information about a user by running @kbd{M-x jabber-vcard-get}, and +you can edit your own information by running @kbd{M-x +jabber-vcard-edit}. + +The form for editing your information can be slightly confusing --- +you are allowed to enter any number of addresses, phone numbers and +e-mail addresses, each of which has a set of orthogonal properties. +You can add and remove items with the [INS] and [DEL] buttons, +respectively. + +@node Useful features, Message history, Personal information, Top +@chapter Useful features + +jabber.el includes a number of features meant to improve the user +interface and do other useful things. + +@menu +* Modeline status:: +* Keepalive:: +* Tracking activity:: +* Watch buddies:: +* Spell checking:: +@end menu + +@node Modeline status, Keepalive, Useful features, Useful features +@section Modeline status + +@cindex modeline +@findex jabber-mode-line-mode +@vindex jabber-mode-line-mode +@vindex jabber-mode-line-compact + +By typing @kbd{M-x jabber-mode-line-mode} you toggle display of some +status in mode lines. The information is your own presence status, +and some numbers showing the status of your roster contacts. By +default, there are three numbers, for ``online'' (chatty and online), +``away'' (away, extended away and do not disturb) and offline +contacts. + +If you set @code{jabber-mode-line-compact} to nil, you get a complete +breakdown of presence status. That gives you six numbers indicating +the number of chatty, online, away, extended away, dnd, and offline +contacts, respectively. + +@node Keepalive, Tracking activity, Modeline status, Useful features +@section Keepalive + +@cindex keepalive +@findex jabber-keepalive-start +@findex jabber-keepalive-stop +@vindex jabber-keepalive-interval +@vindex jabber-keepalive-timeout + +Sometimes network connections are lost without you noticing. This is +especially true with Jabber, as it is quite reasonable to keep the +connection open for a long time without either sending or receiving +any data. + +If you want to detect a lost connection earlier, you can use the +keepalive functions. Type @kbd{M-x jabber-keepalive-start} to start +it, and @kbd{M-x jabber-keepalive-stop} to stop it. + +These functions work by asking your server for the time once in a +while (by default every ten minutes), and considering the connection +lost if the server doesn't answer within reasonable time (by default +20 seconds). + +You can customize the interval and the timeout with the variables +@code{jabber-keepalive-interval} and @code{jabber-keepalive-timeout}, +respectively. + +@node Tracking activity, Watch buddies, Keepalive, Useful features +@section Tracking activity + +@cindex Activity +@findex jabber-activity-mode +@vindex jabber-activity-make-strings +@vindex jabber-activity-query-unread +@vindex jabber-activity-count-in-title +@vindex jabber-activity-count-in-title-format + +When you're working on something important you might want to delay +responding to incoming messages. However, when you're done working, +will you remember them? If you're anything like me, you'll have a lot +of buffers in your Emacs session, and a jabber chat buffer can easily +get lost. + +When you type @kbd{M-x jabber-activity-mode} Emacs starts keeping +track of the buddies which have messaged you since last you visited +their buffer, and will display them in mode line. As soon as you +visit their buffer they disappear from the mode line, indicating that +you've read their message. + +If your mode line fills over because of these notifications, you can +customize @code{jabber-activity-make-strings} to shorten them to the +shortest possibly unambiguous form. + +If you try to exit Emacs while you still have unread messages, you +will be notified and asked about this. If you don't like that, set +@code{jabber-activity-query-unread} to nil. + +If you want to display the number of unread buffers in the frame title, +set @code{jabber-activity-count-in-title} to t. The format of the +number can be changed through +@code{jabber-activity-count-in-title-format}. + +For complete customizability, write a hook function for +@code{jabber-activity-update-hook}. From that function, you can take +action based on @code{jabber-activity-jids}, +@code{jabber-activity-mode-string}, and +@code{jabber-activity-count-string}. + +@node Watch buddies, Spell checking, Tracking activity, Useful features +@section Watch buddies + +@cindex watch +@findex jabber-watch-add +@findex jabber-watch-remove + +Sometimes you might be waiting for a certain person to come online, +and you don't want that occasion to get lost in the noise. To get an +obtrusive message when that happens, type @kbd{M-x jabber-watch-add} +and select the person in question. You can enter a comment, to +remember why you added the watch. + +You will get a message whenever that person goes from offline to +online. jabber.el will remember this for the rest of your Emacs +session (it's not saved to disk, though), but if you want to get rid +of it, type @kbd{M-x jabber-watch-remove}. + +@node Spell checking, , Watch buddies, Useful features +@section Spell checking + +You can activate spell checking in a chat buffer with @kbd{M-x +flyspell-mode}. It will check only what you are currently writing, not +what you receive or what you have already sent. You may want to add +@code{flyspell-mode} to @code{jabber-chat-mode-hook}. + +For more information about Emacs spell checking, @xref{Spelling, , +Checking and Correcting Spelling, emacs, GNU Emacs Manual}. + +@node Message history, Message events, Useful features, Top +@chapter Message history + +@cindex history +@cindex backlog +@cindex rotation +@vindex jabber-history-enabled +@vindex jabber-global-history-filename +@vindex jabber-use-global-history +@vindex jabber-history-dir +@vindex jabber-history-enable-rotation +@vindex jabber-history-size-limit +@vindex jabber-backlog-number +@vindex jabber-backlog-days + +If you want a record of messages sent and received, set +@code{jabber-history-enabled} to t. By default all messages to will +be saved to a global history file specified by +@code{jabber-global-history-filename} +(@file{~/.jabber_global_message_log} by default). If you prefer to +store your chats' history in per-contact files, you can set the +@code{jabber-use-global-history} variable to @code{nil}. When using +per-contact history, files are named by the contact JID and saved +under the directory specified by the variable +@code{jabber-history-dir} (default is @file{~/.emacs-jabber}). + +There is no facility for reading old messages yet, but just reading +the file as text should be enough for many purposes. + +When you open a new chat buffer and have entries in your history file, +the last few messages you recently exchanged with the contact in +question will be inserted. You can control how many messages with +@code{jabber-backlog-number} (by default 10), and how old messages +with @code{jabber-backlog-days} (by default 3 days). + +If you worry about your history file(s) size, you can enable history +rotation feature by setting the variable +@code{jabber-history-enable-rotation} to @code{t} (default is +@code{nil}). This feature ``rotates'' your history files according to +the following rule: When @code{jabber-history-size-limit} (in +kilobytes) is reached, the history file is renamed to +<history-file>-<number>, where <number> is 1 or the smallest number +after the last rotation. For example, suppose you set the +@code{jabber-history-size-limit} variable to 512 and you chat with +your buddy foo@@jabber.server using the per-contact strategy to store +history files. So, when the history file (@file{foo@@jabber-server}) +reaches 512K bytes, it will be renamed to @file{foo@@jabber-server-1} +and @file{foo@@jabber-server} will be set empty. Next time +@file{foo@@jabber-server} grows to 512K bytes, it will be saved as +@file{foo@@jabber-server-2} and so on. Although the example was +presented with the per-contact history file strategy, history rotation +works for both per-contact and global history logging strategies. + + +@node Message events, Roster import and export, Message history, Top +@chapter Message events + +@cindex composing +@cindex delivered +@cindex displayed +@vindex jabber-events-request-these +@vindex jabber-events-confirm-delivered +@vindex jabber-events-confirm-displayed +@vindex jabber-events-confirm-composing + +In the status line of the chat buffer, you can sometimes see +notifications about the progress of the message you just sent. These +states are possible: + +@itemize @bullet +@item +Delivered to offline storage (the user will receive it on next logon) + +@item +Delivered to user's client (but not necessarily displayed) + +@item +Displayed to user + +@item +User is composing a reply + +@end itemize + +The first state is only reported by servers; the other three are +reported by clients. jabber.el can report all three of them, and can +display all four; not all clients support all states, though. + +If you don't want jabber.el to send out this information about you, set +the variables @code{jabber-events-confirm-delivered}, +@code{jabber-events-confirm-displayed}, and/or +@code{jabber-events-confirm-composing} to nil. You can make jabber.el +not to request such information by customizing +@code{jabber-events-request-these}. + +@node Roster import and export, Customization, Message events, Top +@chapter Roster import and export + +@findex jabber-export-roster +@findex jabber-import-roster +@cindex export roster +@cindex import roster + +Your roster is saved on the Jabber server, and usually not in the +client. However, you might want to save the roster to a file anyway. +The most common reason for this is probably to copy it to another +account. + +To export your roster to a file, type @kbd{M-x jabber-export-roster}. +A buffer will appear in which you can edit the data to be exported. +Changes done in that buffer will not affect your real roster. + +To import your roster from a file, type @kbd{M-x jabber-import-roster}. +You will be able to edit the data before importing it. Items not in the +roster will be added; items in the roster will be modified to match +imported data. Subscriptions will be updated. + +The format of the roster files is the XML used by roster pushes in the +XMPP protocol, in UTF-8 encoding. + +@node Customization, Hacking and extending, Roster import and export, Top +@chapter Customization + +@findex jabber-customize +@cindex Customization + +jabber.el is intended to be customizable for many tastes. After all, +this is Emacs. To open a customization buffer for jabber.el, type +@kbd{M-x jabber-customize}. + +@menu +* Account settings:: +* Connection settings:: +* Miscellaneous settings:: +* Customizing the roster buffer:: +* Customizing the chat buffer:: +* Customizing alerts:: +* Hooks:: +* Debug options:: +@end menu + +@node Account settings, Connection settings, Customization, Customization +@section Account settings + +@vindex jabber-username +@vindex jabber-server +@vindex jabber-password +@vindex jabber-resource +@vindex jabber-default-priority +@vindex jabber-nickname + +@code{jabber-username} is the username part of your JID. + +@code{jabber-server} is the JID of your server, i.e. the hostname part +of your JID. This is usually, but not necessarily, the same as the +hostname of the server. + +@code{jabber-password} is your password. You have the option to set +it here, in which case it will be stored in cleartext in your +@file{.emacs} file. If this is set to @code{nil}, you will be prompted for +your password every time you connect. + +@code{jabber-resource} is the resource you want to log in under. This +only matters if you are connected to the same account from different +clients or different computers, since each connection must have a +unique resource. You might want to set this to your hostname. + +@code{jabber-default-priority} is the default priority sent with your +presence. Regardless of what you have here, you can change your +priority during a session with @code{jabber-send-presence}. +@xref{Presence}, for more information on priority. + +@code{jabber-nickname} is your default nickname for groupchats. + +@node Connection settings, Miscellaneous settings, Account settings, Customization +@section Connection settings + +@vindex jabber-network-server +@vindex jabber-port +@vindex jabber-connection-type +@vindex jabber-connection-ssl-program +@cindex SSL +@cindex TLS + +@code{jabber-network-server} is the hostname or IP address of your +server. If it is set to @code{nil}, jabber.el will use the name in +@code{jabber-server}. + +@code{jabber-port} is the TCP port of the server to connect to. If +@code{nil}, the default port is selected based on the chosen +connection method. + +@code{jabber-connection-type} specifies what kind of connection to +use. @code{network} means normal unencrypted network connection +(usually on port 5222), and @code{ssl} means encrypted connection +through GnuTLS or OpenSSL (port 5223). You can change the settings of +the encryption program through @kbd{M-x customize-group RET tls} or +@kbd{M-x customize-group RET ssl}, respectively. + +By default, GnuTLS will be used if the @code{tls} library is available, +and if that fails, OpenSSL will be used if the @code{ssl} library is +available. You can force the use of either program by setting +@code{jabber-connection-ssl-program} to @code{gnutls} or @code{openssl}, +respectively. + +@node Miscellaneous settings, Customizing the roster buffer, Connection settings, Customization +@section Miscellaneous settings + +@findex jabber-menu +@cindex Menus + +If you want a Jabber menu on the menu bar with some common commands, +type @kbd{M-x jabber-menu}. You can remove it again with @kbd{C-u M-x +jabber-menu}. Unfortunately, this cannot be changed through Customize +settings, so you need to add @code{(jabber-menu)} to your @file{.emacs} +to enable it permanently. + +@node Customizing the roster buffer, Customizing the chat buffer, Miscellaneous settings, Customization +@section Customizing the roster buffer + +@vindex jabber-roster-sort-functions +@vindex jabber-sort-order +@vindex jabber-show-resources +@vindex jabber-roster-line-format +@vindex jabber-resource-line-format +@vindex jabber-roster-buffer +@vindex jabber-roster-show-bindings + +@code{jabber-roster-sort-functions} controls how roster items are +sorted. By default, contacts are sorted first by presence, and then +alphabetically by displayed name. + +@code{jabber-sort-order} controls how roster items are sorted by +presence. It is a list containing strings corresponding to show +status (@pxref{Presence}) or @code{nil}, which represents offline. + +@code{jabber-show-resources} controls when your contacts' resources +are shown in the roster buffer. The default is to show resources when +a contact has more than one connected resource. + +@code{jabber-roster-line-format} specifies how the entry for each +contact looks. It is a string where some characters are special if +preceded by a percent sign: + +@table @code +@item %c +"*" if the contact is connected, or " " if not +@item %n +Nickname of contact, or JID if no nickname +@item %j +Bare JID of contact (without resource) +@item %r +Highest-priority resource of contact +@item %s +Availability of contact as string ("Online", "Away" etc) +@item %S +Status string specified by contact +@end table + +@code{jabber-resource-line-format} is nearly identical, except that +the values correspond to the values of the resource in question, and +that the @code{%p} escape is available, which inserts the priority of +the resource. + +@code{jabber-roster-buffer} specifies the name of the roster buffer. +If you change this, the new name will be used the next time you +connect. + +@code{jabber-roster-show-bindings} controls whether to show a list of +keybindings at the top of the roster buffer. You need to run @kbd{M-x +jabber-display-roster} after changing this variable to update the display. + +@node Customizing the chat buffer, Customizing alerts, Customizing the roster buffer, Customization +@section Customizing the chat buffer + +@vindex jabber-chat-time-format +@vindex jabber-chat-delayed-time-format +@vindex jabber-print-rare-time +@vindex jabber-rare-time-format +@vindex jabber-chat-local-prompt-format +@vindex jabber-chat-foreign-prompt-format +@vindex jabber-chat-prompt-local +@vindex jabber-chat-prompt-foreign +@vindex jabber-chat-header-line-format +@vindex jabber-muc-header-line-format +@vindex jabber-chat-fill-long-lines +@cindex Chat buffer +@cindex Timestamps + +You can customize the look of the prompts in the chat buffer. There +are separate settings for local (i.e. your own messages) and foreign +prompts. + +@code{jabber-chat-prompt-local} and @code{jabber-chat-prompt-foreign} +determine the faces used for the prompts. + +@code{jabber-chat-local-prompt-format} and +@code{jabber-chat-foreign-prompt-format} determine what text is +displayed in the prompts. They are format strings, with the following +special sequences defined: + +@table @code +@item %t +The time when the message was sent or received +@item %n +The nickname of the user. For the foreign prompt, this is the name of +the contact in the roster, or the JID if no name set. For the local +prompt, this is the username part of your JID. +@item %j +The bare JID of the user +@end table + +@code{jabber-chat-time-format} defines how @code{%t} shows time. Its +format is identical to that passed to @code{format-time-string}. +@xref{Time Conversion, , Time Conversion, elisp, GNU Emacs Lisp +Reference Manual}. + +@code{jabber-chat-delayed-time-format} is used instead of +@code{jabber-chat-time-format} for delayed messages (messages sent while +you were offline, or fetched from history). This way you can have short +timestamps everywhere except where you need long ones. You can always +see the complete timestamp in a tooltip by hovering over the prompt with +the mouse. + +By default, timestamps are printed in the chat buffer every hour (at +``rare'' times). This can be toggled with +@code{jabber-print-rare-time}. You can customize the displayed time by +setting @code{jabber-rare-time-format}. Rare timestamps will be printed +whenever time formatted by that format string would change. + +You can also customize the header line of chat buffers, by modifying +the variable @code{jabber-chat-header-line-format}. The format of +that variable is the same as that of @code{mode-line-format} and +@code{header-line-format}. @xref{Mode Line Format, , Mode-Line +Format, elisp, GNU Emacs Lisp Reference Manual}. For MUC buffers, +@code{jabber-muc-header-line-format} is used instead. + +The variable @code{jabber-chat-fill-long-lines} controls whether long +lines in the chat buffer are filled. + +@node Customizing alerts, Hooks, Customizing the chat buffer, Customization +@section Customizing alerts + +@cindex Alert hooks +@findex define-jabber-alert + +When an event happens (currently including presence changes, incoming +messages, and completed queries) you will usually want to be +notified. Since tastes in this area vary wildly, these alerts are +implemented as hooks, so you can choose which ones you want, or write +your own if none fit. + +Actually, if you don't want to write your own, stop reading this +section and just read @ref{Standard alerts}. + +Many kinds of alerts consist in displaying a text message through a +certain mechanism. This text message is provided by a function which +you can rewrite or replace. If this function returns @code{nil}, no +message is displayed, and non-textual alerts refrain from action. + +If you want to write alert hooks that do nothing except displaying the +supplied message in some way, use the macro +@code{define-jabber-alert}. For example, if @var{foo} is a function +that takes a string as an argument, write +@example +(define-jabber-alert foo + "Display a message in a fooish way" + 'foo) +@end example +and all details will be taken care of for you. + +The hooks take different arguments depending on category. However, +they all have in common that the last argument is the result of the +message function. The message function for each category takes the +same arguments as the corresponding hooks, except for that last +argument. + +Alert hook contributions are very welcome. You can send them to the +mailing list, or to the Sourceforge patch tracker. + +Alert hooks are meant for optional UI things, that are subject to +varying user tastes, and that can be toggled by simply adding or +removing the function from/to the hook. For other things, there are +corresponding general hooks, that are defvars instead of defcustoms, and +that are to be managed by Lisp code. They have the same name as the +alert hooks minus the @code{-alert} part, +i.e. @code{jabber-message-hooks} vs @code{jabber-alert-message-hooks}, +etc. + +@menu +* Standard alerts:: +* Presence alerts:: +* Message alerts:: +* MUC alerts:: +* Info alerts:: +@end menu + +@node Standard alerts, Presence alerts, Customizing alerts, Customizing alerts +@subsection Standard alerts + +@cindex alerts +@cindex scroll + +Eight alerts are already written for all four alert categories. These +all obey the result from the corresponding message function. + +The @code{beep} alerts simply sound the terminal bell by calling +@code{ding}. They are disabled by default. + +The @code{echo} alerts display a message in the echo area by calling +@code{message}. They are enabled by default. + +The @code{switch} alerts switch to the buffer where the event occurred +(chat buffer for incoming messages, roster buffer for presence +changes, browse buffer for completed queries). They are disabled by +default. Take care when using them, as they may interrupt your +editing. + +The @code{display} alerts display but do not select the buffer in +question, using the function @code{display-buffer}. @xref{Choosing +Window, , Choosing a Window for Display, elisp, GNU Emacs Lisp +Reference Manual}, for information about customizing its behaviour. +This is enabled by default for info requests. + +The @code{wave} alerts play a sound file by calling +@code{play-sound-file}. No sound files are provided. To use this, +enter the names of the sound files in +@code{jabber-alert-message-wave}, @code{jabber-alert-presence-wave} +and @code{jabber-alert-info-wave}, respectively. + +The @code{screen} alerts send a message through the Screen terminal +manager (see @uref{http://www.gnu.org/software/screen/}). They do no +harm if called when you don't use Screen. + +The @code{ratpoison} alerts send a message through the Ratpoison +window manager (see @uref{http://ratpoison.sourceforge.net/}). They +do no harm if used when you're not running X, but if you are running X +with another window manager, the ratpoison processes will never exit. +You can look at them with @code{list-processes}. + +The @code{sawfish} alerts send a message through the Sawfish window +manager. + +The @code{festival} alerts speak the message using the Emacs interface +of the Festival speech synthesis system (see +@uref{http://www.cstr.ed.ac.uk/projects/festival/}). + +Additionally, for one-to-one and MUC messages, there are @code{scroll} +alerts (enabled by default), that aim to do the right thing with chat +buffers that are visible but not active. Sometimes you want point to +scroll down, and sometimes not. These functions should do what you +mean; if they don't, it's a bug. + +Some of these functions are in the @file{jabber-alert.el} file, and the +others are in their own files. You can use them as templates or +inspiration for your own alerts. + +@node Presence alerts, Message alerts, Standard alerts, Customizing alerts +@subsection Presence alerts + +@vindex jabber-alert-presence-message-function +@findex jabber-presence-default-message + +Set @code{jabber-alert-presence-message-function} to your desired +function. This function should look like: + +@example +(defun @var{function} (@var{who} @var{oldstatus} @var{newstatus} @var{statustext}) + ... + ) +@end example + +@var{who} is the JID symbol (@pxref{Roster structure}), +@var{oldstatus} and @var{newstatus} are the previous and current +stati, respectively, and @var{statustext} is the status message if +provided, otherwise nil. + +@var{newstatus} can also be one of @code{"subscribe"}, +@code{"subscribed"}, @code{"unsubscribe"} and @code{"unsubscribed"}. + +The default function, @code{jabber-presence-default-message}, returns +@code{nil} if @var{oldstatus} and @var{newstatus} are the same, and in +other cases constructs a message from the given data. + +All presence alert hooks take the same arguments plus the additional +@var{proposed-alert}, which is the result of the specified message +function. This last argument is usually the only one they use. + +@node Message alerts, MUC alerts, Presence alerts, Customizing alerts +@subsection Message alerts + +@vindex jabber-alert-message-function +@findex jabber-message-default-message +@vindex jabber-message-alert-same-buffer + +If you don't want message alerts when the chat buffer in question is +already the current buffer, set @code{jabber-message-alert-same-buffer} +to nil. This affects the behaviour of the default message function, so +you'll have to reimplement this functionality if you write your own +message function. + +Set @code{jabber-alert-message-function} to your desired +function.@footnote{Logically it should be +@code{jabber-alert-message-message-function}, but that would be +really ugly.} This function should look like: + +@example +(defun @var{function} (@var{from} @var{buffer} @var{text}) + ... + ) +@end example + +@var{from} is the JID symbol (@pxref{Roster structure}), @var{buffer} +is the buffer where the message is displayed, and @var{text} is the +text of the message. + +The default function, @code{jabber-message-default-message}, returns +``Message from @var{person}'', where @var{person} is the name of the +person if specified in the roster, otherwise the JID. + +All message alert hooks take the same arguments plus the additional +@var{proposed-alert}, which is the result of the specified message +function. + +@node MUC alerts, Info alerts, Message alerts, Customizing alerts +@subsection MUC alerts + +@vindex jabber-alert-muc-function +@findex jabber-muc-default-message + +Set @code{jabber-alert-muc-function} to your desired +function. This function should look like: + +@example +(defun @var{function} (@var{nick} @var{group} @var{buffer} @var{text}) + ... + ) +@end example + +@var{nick} is the nickname, @var{group} is the JID of the group, +@var{buffer} is the buffer where the message is displayed, and +@var{text} is the text of the message. + +The default function, @code{jabber-muc-default-message}, returns +``Message from @var{nick} in @var{group}'' or ``Message in +@var{group}'', the latter for messages from the room itself. + +All MUC alert hooks take the same arguments plus the additional +@var{proposed-alert}, which is the result of the specified message +function. + +@node Info alerts, , MUC alerts, Customizing alerts +@subsection Info alerts + +@vindex jabber-alert-info-message-function +@findex jabber-info-default-message + +Info alerts are sadly underdeveloped. The message function, +@code{jabber-alert-info-message-function}, takes two arguments, +@var{infotype} and @var{buffer}. @var{buffer} is the buffer where +something happened, and @var{infotype} is either @code{'roster} for +roster updates, or @code{'browse} for anything that uses the browse +buffer (basically anything except chatting). + +The info alert hooks take an extra argument, as could be expected. + +@node Hooks, Debug options, Customizing alerts, Customization +@section Hooks + +@vindex jabber-post-connect-hook +@vindex jabber-pre-disconnect-hook +@vindex jabber-post-disconnect-hook +@vindex jabber-lost-connection-hook + +jabber.el provides various hooks that you can use for whatever +purpose. + +@table @code +@item jabber-post-connect-hook +This hook is called after successful connection and authentication. +By default it contains @code{jabber-send-default-presence} +(@pxref{Presence}). + +@item jabber-lost-connection-hook +This hook is called when you have been disconnected for unknown +reasons. Usually this isn't noticed for quite a long time. + +@item jabber-pre-disconnect-hook +This hook is called just before voluntary disconnection. This might +be due to failed authentication, so check +@code{*jabber-authenticated*} if you want to send a stanza. + +@item jabber-post-disconnect-hook +This hook is called after disconnection of any kind, possibly just +after @code{jabber-lost-connection-hook}. + +@end table + +@node Debug options, , Hooks, Customization +@section Debug options + +@vindex jabber-debug-log-xml + +These settings provide a lot of information which is usually not very +interesting, but can be useful for debugging various things. + +@code{jabber-debug-log-xml} activates XML logging. All XML stanzas +sent and received are logged in the buffer @code{*-jabber-xml-log-*} +in list format. @xref{XML representation}. + +@node Hacking and extending, Protocol support, Customization, Top +@chapter Hacking and extending + +This part of the manual is an attempt to explain parts of the source +code. It is not meant to discourage you from reading the code +yourself and trying to figure it out, but as a guide on where to +look. Knowledge of Jabber protocols is assumed. + +@menu +* XML representation:: +* Roster structure:: +* Listening for new requests:: +* Sending new requests:: +* Extending service discovery:: +* Chat printers:: +* Stanza chains:: +@end menu + +@node XML representation, Roster structure, Hacking and extending, Hacking and extending +@section XML representation + +@cindex XML representation + +The XML representation is the one generated by @file{xml.el} in Emacs, +namely the following. Each tag is a list. The first element of the +list is a symbol, the name of which is the name of the tag. The +second element is an alist of attributes, where the keys are the +attribute names in symbol form, and the values are strings. The +remaining elements are the tags and data contained within the tag. + +For example, +@example +<foo bar='baz'> +<frobozz/>Fnord +</foo> +@end example +is represented as +@example +(foo ((bar . "baz")) (frobozz nil "") "Fnord +") +@end example + +Note the empty string as the third element of the @code{frobozz} +list. It is not present in newer (post-21.3) versions of +@file{xml.el}, but it's probably best to assume it might be there. + +If you want to see what an XML tag would look like, use +@code{jabber-sexp2xml}, which takes a tag and returns a string. You +will usually not need it in your code, as you can use +@code{jabber-send-sexp} to send away your tags to the server. + +@node Roster structure, Listening for new requests, XML representation, Hacking and extending +@section Roster structure + +@vindex *jabber-roster* +@vindex jabber-jid-obarray + +Roster entries are contained in the list @code{*jabber-roster*}. + +A roster entry is a symbol. Its name is the JID, and it is interned +in @code{jabber-jid-obarray}. A roster entry can have the following +properties: + +@table @code +@item xml +The XML tag received from the server on roster update + +@item name +The name of the roster item (just like the XML attribute) + +@item subscription +The subscription state (also copied) + +@item ask +The ask state (copied) + +@item groups +A list of strings (possibly empty) containing all the groups the +contact is in + +@item connected +Boolean, true if any resource is connected + +@item show +Presence show status for highest-priority connected resource + +@item status +Presence status message for highest-priority connected resource + +@item resources +Alist. Keys are strings (resource names), values are plists with +properties @code{connected}, @code{show}, @code{status} and +@code{priority}. + +@end table + +Incoming presence information is inserted in @code{resources}, and the +information from the resource with the highest priority is inserted in +@code{show} and @code{status} by the function +@code{jabber-prioritize-resources}. + +@node Listening for new requests, Sending new requests, Roster structure, Hacking and extending +@section Listening for new requests + +@findex jabber-send-iq +@findex jabber-process-iq +@findex jabber-signal-error +@vindex jabber-iq-get-xmlns-alist +@vindex jabber-iq-set-xmlns-alist + +To listen for new IQ requests, add the appropriate entry in +@code{jabber-iq-get-xmlns-alist} or @code{jabber-iq-set-xmlns-alist}. +The key is the namespace of the request, and the value is a function +that takes one argument, the entire IQ stanza in list format. +@code{jabber-process-iq} reads these alists to determine which +function to call on incoming packets. + +For example, the Ad-Hoc Commands module contains the following: + +@example +(add-to-list 'jabber-iq-set-xmlns-alist + (cons "http://jabber.org/protocol/commands" 'jabber-ahc-process)) +@end example + +To send a response to an IQ request, use @samp{(jabber-send-iq +@var{sender} "result" @var{query} nil nil nil nil @var{id})}, where +@var{query} is the query in list format. @code{jabber-send-iq} will +encapsulate the query in an IQ packet with the specified id. + +To return an error to the Jabber entity that sent the query, use +@code{jabber-signal-error}. The signal is caught by +@code{jabber-process-iq}, which takes care of sending the error. + +@node Sending new requests, Extending service discovery, Listening for new requests, Hacking and extending +@section Sending new requests + +@findex jabber-send-iq +@findex jabber-process-iq +@findex jabber-report-success +@findex jabber-process-data + +To send an IQ request, use @code{jabber-send-iq}. It will generate an +id, and create a mapping for it for use when the response comes. The +syntax is: + +@example +(jabber-send-iq @var{to} @var{type} @var{query} + @var{success-callback} @var{success-closure} + @var{failure-callback} @var{failure-closure}) +@end example + +Both callbacks take two arguments, the IQ stanza returned and the +closure item mentioned here. + +Two standard callbacks are provided. @code{jabber-report-success} +takes a string as closure item, and reports success or failure in the +echo area. @code{jabber-process-data} prepares a browse buffer. If +its closure argument is a function, it calls that function with point +in this browse buffer. If it's a string, it prints that string along +with the error message in the IQ response. If it's anything else +(e.g. @code{nil}), it just dumps the XML in the browse buffer. + +Examples follow. This is the hypothetical Jabber protocol ``frob'', +for which only success report is needed: +@example +(jabber-send-iq "someone@@somewhere.org" "set" + '(query ((xmlns . "frob"))) + 'jabber-report-success "Frobbing" + 'jabber-report-success "Frobbing") +@end example +This will print ``Frobbing succeeded'' or ``Frobbing failed: reason'', +respectively, in the echo area. + +The protocol ``investigate'' needs to parse results and show them in a +browse buffer: +@example +(jabber-send-iq "someone@@somewhere.org" "get" + '(query ((xmlns . "investigate"))) + 'jabber-process-data 'jabber-process-investigate + 'jabber-process-data "Investigation failed") +@end example +Of course, the previous example could have used +@code{jabber-report-success} for the error message. It's a matter of +UI taste. + +@node Extending service discovery, Chat printers, Sending new requests, Hacking and extending +@section Service discovery + +@vindex jabber-advertised-features +@vindex jabber-disco-items-nodes +@vindex jabber-disco-info-nodes +@findex jabber-my-jid-p + +Your new handlers will likely want to advertise their existence +through service discovery. + +To have an additional feature reported in response to disco info +requests, add a string to @code{jabber-advertised-features}. + +By default, the service discovery functions reject all requests +containing a node identifier with an ``Item not found'' error. To +make them respond, add the appropriate entries to +@code{jabber-disco-items-nodes} and @code{jabber-disco-info-nodes}. +Both variables work in the same way. They are alists, where the keys +are the node names, and the values are lists of two items. + +The first item is the data to return --- either a list or a function +taking the entire IQ stanza and returning a list, this list containing +the XML nodes to include in the @code{<query/>} node in the response. + +The second item is the access control function. An access control +function receives the JID as its only argument, and returns non-nil if +access is to be granted. If nil is specified instead of a function, +access is always granted. One such function is provided, +@code{jabber-my-jid-p}, which grants access for JIDs where the +username and server (not necessarily resource) are equal to those of +the user. + +@node Chat printers, Stanza chains, Extending service discovery, Hacking and extending +@section Chat printers + +@vindex jabber-chat-printers +@vindex jabber-muc-printers +@vindex jabber-body-printers +@cindex Chat printers +@cindex Body printers + +Chat printers are functions that print a certain aspect of an incoming +message in a chat buffer. Included are functions for printing subjects +(@code{jabber-chat-print-subject}), bodies +(@code{jabber-chat-print-body}, and @code{jabber:x:oob}-style URLs +(@code{jabber-chat-print-url}). The functions in +@code{jabber-chat-printers} are called in order, with the entire +@code{<message/>} stanza as argument, and are expected to call +@code{insert} if they have anything to add. + +For MUC, the functions in @code{jabber-muc-printers} are prepended to +those in @code{jabber-chat-printers}. + +Body printers are a subgroup of chat printers. They are exclusive; only +one of them applies to any given message. The idea is that +``higher-quality'' parts of the message override pieces included for +backwards compatibility. Included are @code{jabber-muc-print-invite} +and @code{jabber-chat-normal-body}; functions for XHTML-IM and PGP +encrypted messages may be written in the future. The functions in +@code{jabber-body-printers} are called in order until one of them +returns non-nil. + +@node Stanza chains, , Chat printers, Hacking and extending +@section Stanza chains + +@vindex jabber-message-chain +@vindex jabber-iq-chain +@vindex jabber-presence-chain + +If you really need to get under the skin of jabber.el, you can add +functions to the lists @code{jabber-message-chain}, +@code{jabber-iq-chain} and @code{jabber-presence-chain}. The functions +in these lists will be called in order when an XML stanza of the +corresponding type arrives, with the entire XML stanza passed as the +only argument. Earlier functions can modify the stanza to change the +behaviour of downstream functions. + +@node Protocol support, Concept index, Hacking and extending, Top +@appendix Protocol support + +These are the protocols currently supported (in full or partially) by +jabber.el. + +@menu +* RFC 3920 (XMPP-CORE):: +* RFC 3921 (XMPP-IM):: +* JEP-0004 (Data Forms):: +* JEP-0020 (Feature Negotiation):: +* JEP-0022 (Message Events):: +* JEP-0030 (Service Discovery):: +* JEP-0045 (Multi-User Chat):: +* JEP-0050 (Ad-Hoc Commands):: +* JEP-0054 (vcard-temp):: +* JEP-0055 (Jabber Search):: +* JEP-0065 (SOCKS5 Bytestreams):: +* JEP-0066 (Out of Band Data):: +* JEP-0068 (Field Standardization for Data Forms):: +* JEP-0077 (In-Band Registration):: +* JEP-0078 (Non-SASL Authentication):: +* JEP-0082 (Jabber Date and Time Profiles):: +* JEP-0086 (Error Condition Mappings):: +* JEP-0091 (Delayed Delivery):: +* JEP-0092 (Software Version):: +* JEP-0095 (Stream Initiation):: +* JEP-0096 (File Transfer):: +@end menu + +@node RFC 3920 (XMPP-CORE), RFC 3921 (XMPP-IM), Protocol support, Protocol support +@section RFC 3920 (XMPP-CORE) + +Most of RFC 3920 is supported, with the following exceptions. + +The STARTTLS feature is not supported. + +SASL is supported only when an external SASL library from FLIM or Gnus +is present. As SASL is an essential part to XMPP, jabber.el will send +pre-XMPP stream headers if it is not available. + +None of the stringprep profiles are implemented. jabber.el changes +JIDs to lowercase internally; that's all. + +jabber.el doesn't interpret namespace prefixes. + +The @code{xml:lang} attribute is neither interpreted nor generated. + +SRV records are not used. + +@node RFC 3921 (XMPP-IM), JEP-0004 (Data Forms), RFC 3920 (XMPP-CORE), Protocol support +@section RFC 3921 (XMPP-IM) + +Most of RFC 3921 is supported, with the following exceptions. + +Messages of type ``headline'' are not treated in any special way. + +The @code{<thread/>} element is not used or generated. + +Neither sending nor receiving ``directed presence'' is supported. + +Privacy lists are not supported at all. + +jabber.el doesn't support XMPP-E2E or ``im:'' CPIM URIs. + +@node JEP-0004 (Data Forms), JEP-0020 (Feature Negotiation), RFC 3921 (XMPP-IM), Protocol support +@section JEP-0004 (Data Forms) + +JEP-0004 support is good enough for many purposes. Limitations are +the following. + +Forms in incoming messages are not interpreted. See each specific +protocol for whether forms are accepted in that context. + +``Cancel'' messages are probably not consistently generated when they +should be. This is partly a paradigm clash, as jabber.el doesn't use +modal dialog boxes but buffers which can easily be buried. + +@code{<required/>} elements are not enforced. + +The field types ``jid-single'', ``jid-multi'' and ``list-multi'' are +not implemented, due to programmer laziness. Let us know if you need +them. + +@node JEP-0020 (Feature Negotiation), JEP-0022 (Message Events), JEP-0004 (Data Forms), Protocol support +@section JEP-0020 (Feature Negotiation) + +There are no known limitations or bugs in JEP-0020 support. + +@node JEP-0022 (Message Events), JEP-0030 (Service Discovery), JEP-0020 (Feature Negotiation), Protocol support +@section JEP-0022 (Message Events) + +jabber.el understands all four specified kinds of message events +(offline, delivered, displayed, and composing) and by default requests +all of them. It also reports those three events that make sense for +clients. + +@node JEP-0030 (Service Discovery), JEP-0045 (Multi-User Chat), JEP-0022 (Message Events), Protocol support +@section JEP-0030 (Service Discovery) + +Service discovery is supported, both as client and server, with the +following limitations. Currently, the client part is not used by any +code, but is provided only as a user tool. The user interface is not +appealing. + +Publishing items is not supported. + +@node JEP-0045 (Multi-User Chat), JEP-0050 (Ad-Hoc Commands), JEP-0030 (Service Discovery), Protocol support +@section JEP-0045 (Multi-User Chat) + +jabber.el supports parts of JEP-0045. Entering, leaving and chatting +work. So do invitations and private messages. Room configuration is +supported. Changing roles of participants (basic moderation) is +implemented, as is changing affiliations, but requesting affiliation +lists is not yet supported. + +@node JEP-0050 (Ad-Hoc Commands), JEP-0054 (vcard-temp), JEP-0045 (Multi-User Chat), Protocol support +@section JEP-0050 (Ad-Hoc Commands) + +jabber.el is probably the first implementation of JEP-0050 (see +@uref{http://article.gmane.org/gmane.network.jabber.devel/21413, post +on jdev from 2004-03-10}). Both the client and server parts are +supported. + +@node JEP-0054 (vcard-temp), JEP-0055 (Jabber Search), JEP-0050 (Ad-Hoc Commands), Protocol support +@section JEP-0054 (vcard-temp) + +Both displaying other users' vCards and editing your own vCard are +supported. The implementation tries to follow the schema in the JEP +accurately. + +@node JEP-0055 (Jabber Search), JEP-0065 (SOCKS5 Bytestreams), JEP-0054 (vcard-temp), Protocol support +@section JEP-0055 (Jabber Search) + +JEP-0055 is supported, both with traditional fields and with Data +Forms (@pxref{JEP-0004 (Data Forms)}). As the traditional fields +specified by the JEP is a subset of those allowed in JEP-0077, +handling of those two form types are merged. @ref{JEP-0077 (In-Band +Registration)}. + +@node JEP-0065 (SOCKS5 Bytestreams), JEP-0066 (Out of Band Data), JEP-0055 (Jabber Search), Protocol support +@section JEP-0065 (SOCKS5 Bytestreams) + +JEP-0065 support seems to work, but it is not yet enabled by default. + +Currently jabber.el cannot act as a server, not even on on Emacsen +that support server sockets (GNU Emacs 22 and up). Therefore it +relies on proxies. Proxies have to be entered and queried manually. + +Psi's ``fast mode'' +(@uref{http://delta.affinix.com/specs/stream.html}), which gives +greater flexibility with regards to NAT, is not implemented. + +@node JEP-0066 (Out of Band Data), JEP-0068 (Field Standardization for Data Forms), JEP-0065 (SOCKS5 Bytestreams), Protocol support +@section JEP-0066 (Out of Band Data) + +jabber.el will display URLs sent in message stanzas qualified by +the @code{jabber:x:oob} namespace, as described in this JEP. Sending +such URLs or doing anything with iq stanzas (using the +@code{jabber:iq:oob} namespace) is not supported. + +@node JEP-0068 (Field Standardization for Data Forms), JEP-0077 (In-Band Registration), JEP-0066 (Out of Band Data), Protocol support +@section JEP-0068 (Field Standardization for Data Forms) + +JEP-0068 is only used in the context of creating a new Jabber account, +to prefill the username field of the registration form. + +@node JEP-0077 (In-Band Registration), JEP-0078 (Non-SASL Authentication), JEP-0068 (Field Standardization for Data Forms), Protocol support +@section JEP-0077 (In-Band Registration) + +In-band registration is supported for all purposes. That means +registering a new Jabber account, changing Jabber password, removing a +Jabber account, registering with a service, and cancelling +registration to a service. Data forms are supported as well. URL +redirections are not. + +jabber.el will not prevent or alert a user trying to change a password +over an unencrypted connection. + +@node JEP-0078 (Non-SASL Authentication), JEP-0082 (Jabber Date and Time Profiles), JEP-0077 (In-Band Registration), Protocol support +@section JEP-0078 (Non-SASL Authentication) + +Non-SASL authentication is supported, both plaintext and digest. +Digest is preferred, and a warning is displayed to the user if only +plaintext is available. + +@node JEP-0082 (Jabber Date and Time Profiles), JEP-0086 (Error Condition Mappings), JEP-0078 (Non-SASL Authentication), Protocol support +@section JEP-0082 (Jabber Date and Time Profiles) + +The DateTime profile of JEP-0082 is supported. Currently this is only +used for file transfer. + +@node JEP-0086 (Error Condition Mappings), JEP-0091 (Delayed Delivery), JEP-0082 (Jabber Date and Time Profiles), Protocol support +@section JEP-0086 (Error Condition Mappings) + +Legacy errors are interpreted, but never generated. XMPP style error +messages take precedence when errors are reported to the user. + +@node JEP-0091 (Delayed Delivery), JEP-0092 (Software Version), JEP-0086 (Error Condition Mappings), Protocol support +@section JEP-0091 (Delayed Delivery) + +The time specified on delayed incoming messages is interpreted, and +displayed in chat buffers instead of the current time. + +@node JEP-0092 (Software Version), JEP-0095 (Stream Initiation), JEP-0091 (Delayed Delivery), Protocol support +@section JEP-0092 (Software Version) + +The user can request the version of any entity. jabber.el answers +version requests to anyone, giving ``jabber.el'' as name, and the +Emacs version as OS. + +@node JEP-0095 (Stream Initiation), JEP-0096 (File Transfer), JEP-0092 (Software Version), Protocol support +@section JEP-0095 (Stream Initiation) + +JEP-0095 is supported, both incoming and outgoing, with the following +exceptions. It is not enabled by default, as it hasn't received much +testing. + +jabber.el doesn't check service discovery results before sending a +stream initiation request. + +@node JEP-0096 (File Transfer), , JEP-0095 (Stream Initiation), Protocol support +@section JEP-0096 (File Transfer) + +Both sending and receiving files is supported, though not enabled by +default. The following limitations apply. + +The hash of a file being sent is not calculated and sent in the +request. + +Ranged transfers are not supported. + +In-band bytestreams are not yet supported, even though JEP-0096 +requires them. + +@node Concept index, Function index, Protocol support, Top +@unnumbered Concept index + +@printindex cp + +@node Function index, Variable index, Concept index, Top +@unnumbered Function index + +@printindex fn + +@node Variable index, , Function index, Top +@unnumbered Variable index + +@printindex vr + +@bye + +@ignore + arch-tag: 995bf3da-0e87-4b15-895a-1e85fac139a2 +@end ignore @@ -0,0 +1,443 @@ +;;; sha1.el --- SHA1 Secure Hash Algorithm in Emacs-Lisp + +;; Copyright (C) 1999, 2001, 2002, 2003, 2004, +;; 2005 Free Software Foundation, Inc. + +;; Author: Shuhei KOBAYASHI <shuhei@aqua.ocn.ne.jp> +;; Keywords: SHA1, FIPS 180-1 + +;; This file is part of FLIM (Faithful Library about Internet Message). + +;; 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, 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; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: + +;; This program is implemented from the definition of SHA-1 in FIPS PUB +;; 180-1 (Federal Information Processing Standards Publication 180-1), +;; "Announcing the Standard for SECURE HASH STANDARD". +;; <URL:http://www.itl.nist.gov/div897/pubs/fip180-1.htm> +;; (EXCEPTION; two optimizations taken from GnuPG/cipher/sha1.c) +;; +;; Test cases from FIPS PUB 180-1. +;; +;; (sha1 "abc") +;; => a9993e364706816aba3e25717850c26c9cd0d89d +;; +;; (sha1 "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") +;; => 84983e441c3bd26ebaae4aa1f95129e5e54670f1 +;; +;; (sha1 (make-string 1000000 ?a)) +;; => 34aa973cd4c4daa4f61eeb2bdbad27316534016f +;; +;; BUGS: +;; * It is assumed that length of input string is less than 2^29 bytes. +;; * It is caller's responsibility to make string (or region) unibyte. +;; +;; TODO: +;; * Rewrite from scratch! +;; This version is much faster than Keiichi Suzuki's another sha1.el, +;; but it is too dirty. + +;;; Code: + +(require 'hex-util) + +;;; +;;; external SHA1 function. +;;; + +(defgroup sha1 nil + "Elisp interface for SHA1 hash computation." + :version "22.1" + :group 'extensions) + +(defcustom sha1-maximum-internal-length 500 + "*Maximum length of message to use Lisp version of SHA1 function. +If message is longer than this, `sha1-program' is used instead. + +If this variable is set to 0, use external program only. +If this variable is set to nil, use internal function only." + :type 'integer + :group 'sha1) + +(defcustom sha1-program '("sha1sum") + "*Name of program to compute SHA1. +It must be a string \(program name\) or list of strings \(name and its args\)." + :type '(repeat string) + :group 'sha1) + +(defcustom sha1-use-external (condition-case () + (executable-find (car sha1-program)) + (error)) + "*Use external SHA1 program. +If this variable is set to nil, use internal function only." + :type 'boolean + :group 'sha1) + +(defun sha1-string-external (string &optional binary) + (let (prog args digest default-enable-multibyte-characters) + (if (consp sha1-program) + (setq prog (car sha1-program) + args (cdr sha1-program)) + (setq prog sha1-program + args nil)) + (with-temp-buffer + (insert string) + (apply (function call-process-region) + (point-min)(point-max) + prog t t nil args) + ;; SHA1 is 40 bytes long in hexadecimal form. + (setq digest (buffer-substring (point-min)(+ (point-min) 40)))) + (if binary + (decode-hex-string digest) + digest))) + +(defun sha1-region-external (beg end &optional binary) + (sha1-string-external (buffer-substring-no-properties beg end) binary)) + +;;; +;;; internal SHA1 function. +;;; + +(eval-when-compile + ;; optional second arg of string-to-number is new in v20. + (defconst sha1-K0-high 23170) ; (string-to-number "5A82" 16) + (defconst sha1-K0-low 31129) ; (string-to-number "7999" 16) + (defconst sha1-K1-high 28377) ; (string-to-number "6ED9" 16) + (defconst sha1-K1-low 60321) ; (string-to-number "EBA1" 16) + (defconst sha1-K2-high 36635) ; (string-to-number "8F1B" 16) + (defconst sha1-K2-low 48348) ; (string-to-number "BCDC" 16) + (defconst sha1-K3-high 51810) ; (string-to-number "CA62" 16) + (defconst sha1-K3-low 49622) ; (string-to-number "C1D6" 16) + +;;; original definition of sha1-F0. +;;; (defmacro sha1-F0 (B C D) +;;; (` (logior (logand (, B) (, C)) +;;; (logand (lognot (, B)) (, D))))) +;;; a little optimization from GnuPG/cipher/sha1.c. + (defmacro sha1-F0 (B C D) + (` (logxor (, D) (logand (, B) (logxor (, C) (, D)))))) + (defmacro sha1-F1 (B C D) + (` (logxor (, B) (, C) (, D)))) +;;; original definition of sha1-F2. +;;; (defmacro sha1-F2 (B C D) +;;; (` (logior (logand (, B) (, C)) +;;; (logand (, B) (, D)) +;;; (logand (, C) (, D))))) +;;; a little optimization from GnuPG/cipher/sha1.c. + (defmacro sha1-F2 (B C D) + (` (logior (logand (, B) (, C)) + (logand (, D) (logior (, B) (, C)))))) + (defmacro sha1-F3 (B C D) + (` (logxor (, B) (, C) (, D)))) + + (defmacro sha1-S1 (W-high W-low) + (` (let ((W-high (, W-high)) + (W-low (, W-low))) + (setq S1W-high (+ (% (* W-high 2) 65536) + (/ W-low (, (/ 65536 2))))) + (setq S1W-low (+ (/ W-high (, (/ 65536 2))) + (% (* W-low 2) 65536)))))) + (defmacro sha1-S5 (A-high A-low) + (` (progn + (setq S5A-high (+ (% (* (, A-high) 32) 65536) + (/ (, A-low) (, (/ 65536 32))))) + (setq S5A-low (+ (/ (, A-high) (, (/ 65536 32))) + (% (* (, A-low) 32) 65536)))))) + (defmacro sha1-S30 (B-high B-low) + (` (progn + (setq S30B-high (+ (/ (, B-high) 4) + (* (% (, B-low) 4) (, (/ 65536 4))))) + (setq S30B-low (+ (/ (, B-low) 4) + (* (% (, B-high) 4) (, (/ 65536 4)))))))) + + (defmacro sha1-OP (round) + (` (progn + (sha1-S5 sha1-A-high sha1-A-low) + (sha1-S30 sha1-B-high sha1-B-low) + (setq sha1-A-low (+ ((, (intern (format "sha1-F%d" round))) + sha1-B-low sha1-C-low sha1-D-low) + sha1-E-low + (, (symbol-value + (intern (format "sha1-K%d-low" round)))) + (aref block-low idx) + (progn + (setq sha1-E-low sha1-D-low) + (setq sha1-D-low sha1-C-low) + (setq sha1-C-low S30B-low) + (setq sha1-B-low sha1-A-low) + S5A-low))) + (setq carry (/ sha1-A-low 65536)) + (setq sha1-A-low (% sha1-A-low 65536)) + (setq sha1-A-high (% (+ ((, (intern (format "sha1-F%d" round))) + sha1-B-high sha1-C-high sha1-D-high) + sha1-E-high + (, (symbol-value + (intern (format "sha1-K%d-high" round)))) + (aref block-high idx) + (progn + (setq sha1-E-high sha1-D-high) + (setq sha1-D-high sha1-C-high) + (setq sha1-C-high S30B-high) + (setq sha1-B-high sha1-A-high) + S5A-high) + carry) + 65536))))) + + (defmacro sha1-add-to-H (H X) + (` (progn + (setq (, (intern (format "sha1-%s-low" H))) + (+ (, (intern (format "sha1-%s-low" H))) + (, (intern (format "sha1-%s-low" X))))) + (setq carry (/ (, (intern (format "sha1-%s-low" H))) 65536)) + (setq (, (intern (format "sha1-%s-low" H))) + (% (, (intern (format "sha1-%s-low" H))) 65536)) + (setq (, (intern (format "sha1-%s-high" H))) + (% (+ (, (intern (format "sha1-%s-high" H))) + (, (intern (format "sha1-%s-high" X))) + carry) + 65536))))) + ) + +;;; buffers (H0 H1 H2 H3 H4). +(defvar sha1-H0-high) +(defvar sha1-H0-low) +(defvar sha1-H1-high) +(defvar sha1-H1-low) +(defvar sha1-H2-high) +(defvar sha1-H2-low) +(defvar sha1-H3-high) +(defvar sha1-H3-low) +(defvar sha1-H4-high) +(defvar sha1-H4-low) + +(defun sha1-block (block-high block-low) + (let (;; step (c) --- initialize buffers (A B C D E). + (sha1-A-high sha1-H0-high) (sha1-A-low sha1-H0-low) + (sha1-B-high sha1-H1-high) (sha1-B-low sha1-H1-low) + (sha1-C-high sha1-H2-high) (sha1-C-low sha1-H2-low) + (sha1-D-high sha1-H3-high) (sha1-D-low sha1-H3-low) + (sha1-E-high sha1-H4-high) (sha1-E-low sha1-H4-low) + (idx 16)) + ;; step (b). + (let (;; temporary variables used in sha1-S1 macro. + S1W-high S1W-low) + (while (< idx 80) + (sha1-S1 (logxor (aref block-high (- idx 3)) + (aref block-high (- idx 8)) + (aref block-high (- idx 14)) + (aref block-high (- idx 16))) + (logxor (aref block-low (- idx 3)) + (aref block-low (- idx 8)) + (aref block-low (- idx 14)) + (aref block-low (- idx 16)))) + (aset block-high idx S1W-high) + (aset block-low idx S1W-low) + (setq idx (1+ idx)))) + ;; step (d). + (setq idx 0) + (let (;; temporary variables used in sha1-OP macro. + S5A-high S5A-low S30B-high S30B-low carry) + (while (< idx 20) (sha1-OP 0) (setq idx (1+ idx))) + (while (< idx 40) (sha1-OP 1) (setq idx (1+ idx))) + (while (< idx 60) (sha1-OP 2) (setq idx (1+ idx))) + (while (< idx 80) (sha1-OP 3) (setq idx (1+ idx)))) + ;; step (e). + (let (;; temporary variables used in sha1-add-to-H macro. + carry) + (sha1-add-to-H H0 A) + (sha1-add-to-H H1 B) + (sha1-add-to-H H2 C) + (sha1-add-to-H H3 D) + (sha1-add-to-H H4 E)))) + +(defun sha1-binary (string) + "Return the SHA1 of STRING in binary form." + (let (;; prepare buffers for a block. byte-length of block is 64. + ;; input block is split into two vectors. + ;; + ;; input block: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ... + ;; block-high: +-0-+ +-1-+ +-2-+ +-3-+ + ;; block-low: +-0-+ +-1-+ +-2-+ +-3-+ + ;; + ;; length of each vector is 80, and elements of each vector are + ;; 16bit integers. elements 0x10-0x4F of each vector are + ;; assigned later in `sha1-block'. + (block-high (eval-when-compile (make-vector 80 nil))) + (block-low (eval-when-compile (make-vector 80 nil)))) + (unwind-protect + (let* (;; byte-length of input string. + (len (length string)) + (lim (* (/ len 64) 64)) + (rem (% len 4)) + (idx 0)(pos 0)) + ;; initialize buffers (H0 H1 H2 H3 H4). + (setq sha1-H0-high 26437 ; (string-to-number "6745" 16) + sha1-H0-low 8961 ; (string-to-number "2301" 16) + sha1-H1-high 61389 ; (string-to-number "EFCD" 16) + sha1-H1-low 43913 ; (string-to-number "AB89" 16) + sha1-H2-high 39098 ; (string-to-number "98BA" 16) + sha1-H2-low 56574 ; (string-to-number "DCFE" 16) + sha1-H3-high 4146 ; (string-to-number "1032" 16) + sha1-H3-low 21622 ; (string-to-number "5476" 16) + sha1-H4-high 50130 ; (string-to-number "C3D2" 16) + sha1-H4-low 57840) ; (string-to-number "E1F0" 16) + ;; loop for each 64 bytes block. + (while (< pos lim) + ;; step (a). + (setq idx 0) + (while (< idx 16) + (aset block-high idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (aset block-low idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (setq idx (1+ idx))) + (sha1-block block-high block-low)) + ;; last block. + (if (prog1 + (< (- len lim) 56) + (setq lim (- len rem)) + (setq idx 0) + (while (< pos lim) + (aset block-high idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (aset block-low idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (setq idx (1+ idx))) + ;; this is the last (at most) 32bit word. + (cond + ((= rem 3) + (aset block-high idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (setq pos (+ pos 2)) + (aset block-low idx (+ (* (aref string pos) 256) + 128))) + ((= rem 2) + (aset block-high idx (+ (* (aref string pos) 256) + (aref string (1+ pos)))) + (aset block-low idx 32768)) + ((= rem 1) + (aset block-high idx (+ (* (aref string pos) 256) + 128)) + (aset block-low idx 0)) + (t ;; (= rem 0) + (aset block-high idx 32768) + (aset block-low idx 0))) + (setq idx (1+ idx)) + (while (< idx 16) + (aset block-high idx 0) + (aset block-low idx 0) + (setq idx (1+ idx)))) + ;; last block has enough room to write the length of string. + (progn + ;; write bit length of string to last 4 bytes of the block. + (aset block-low 15 (* (% len 8192) 8)) + (setq len (/ len 8192)) + (aset block-high 15 (% len 65536)) + ;; XXX: It is not practical to compute SHA1 of + ;; such a huge message on emacs. + ;; (setq len (/ len 65536)) ; for 64bit emacs. + ;; (aset block-low 14 (% len 65536)) + ;; (aset block-high 14 (/ len 65536)) + (sha1-block block-high block-low)) + ;; need one more block. + (sha1-block block-high block-low) + (fillarray block-high 0) + (fillarray block-low 0) + ;; write bit length of string to last 4 bytes of the block. + (aset block-low 15 (* (% len 8192) 8)) + (setq len (/ len 8192)) + (aset block-high 15 (% len 65536)) + ;; XXX: It is not practical to compute SHA1 of + ;; such a huge message on emacs. + ;; (setq len (/ len 65536)) ; for 64bit emacs. + ;; (aset block-low 14 (% len 65536)) + ;; (aset block-high 14 (/ len 65536)) + (sha1-block block-high block-low)) + ;; make output string (in binary form). + (let ((result (make-string 20 0))) + (aset result 0 (/ sha1-H0-high 256)) + (aset result 1 (% sha1-H0-high 256)) + (aset result 2 (/ sha1-H0-low 256)) + (aset result 3 (% sha1-H0-low 256)) + (aset result 4 (/ sha1-H1-high 256)) + (aset result 5 (% sha1-H1-high 256)) + (aset result 6 (/ sha1-H1-low 256)) + (aset result 7 (% sha1-H1-low 256)) + (aset result 8 (/ sha1-H2-high 256)) + (aset result 9 (% sha1-H2-high 256)) + (aset result 10 (/ sha1-H2-low 256)) + (aset result 11 (% sha1-H2-low 256)) + (aset result 12 (/ sha1-H3-high 256)) + (aset result 13 (% sha1-H3-high 256)) + (aset result 14 (/ sha1-H3-low 256)) + (aset result 15 (% sha1-H3-low 256)) + (aset result 16 (/ sha1-H4-high 256)) + (aset result 17 (% sha1-H4-high 256)) + (aset result 18 (/ sha1-H4-low 256)) + (aset result 19 (% sha1-H4-low 256)) + result)) + ;; do not leave a copy of input string. + (fillarray block-high nil) + (fillarray block-low nil)))) + +(defun sha1-string-internal (string &optional binary) + (if binary + (sha1-binary string) + (encode-hex-string (sha1-binary string)))) + +(defun sha1-region-internal (beg end &optional binary) + (sha1-string-internal (buffer-substring-no-properties beg end) binary)) + +;;; +;;; application interface. +;;; + +(defun sha1-region (beg end &optional binary) + (if (and sha1-use-external + sha1-maximum-internal-length + (> (abs (- end beg)) sha1-maximum-internal-length)) + (sha1-region-external beg end binary) + (sha1-region-internal beg end binary))) + +(defun sha1-string (string &optional binary) + (if (and sha1-use-external + sha1-maximum-internal-length + (> (length string) sha1-maximum-internal-length)) + (sha1-string-external string binary) + (sha1-string-internal string binary))) + +;;;###autoload +(defun sha1 (object &optional beg end binary) + "Return the SHA1 (Secure Hash Algorithm) of an object. +OBJECT is either a string or a buffer. +Optional arguments BEG and END denote buffer positions for computing the +hash of a portion of OBJECT. +If BINARY is non-nil, return a string in binary form." + (if (stringp object) + (sha1-string object binary) + (save-excursion + (set-buffer object) + (sha1-region (or beg (point-min)) (or end (point-max)) binary)))) + +(provide 'sha1) + +;;; arch-tag: c0f9abd0-ffc1-4557-aac6-ece7f2d4c901 +;;; sha1.el ends here |