summaryrefslogtreecommitdiff
path: root/jabber-history.el
diff options
context:
space:
mode:
Diffstat (limited to 'jabber-history.el')
-rw-r--r--jabber-history.el337
1 files changed, 337 insertions, 0 deletions
diff --git a/jabber-history.el b/jabber-history.el
new file mode 100644
index 0000000..a1e8250
--- /dev/null
+++ b/jabber-history.el
@@ -0,0 +1,337 @@
+;; jabber-history.el - recording message history
+
+;; Copyright (C) 2004, 2007, 2008 - Magnus Henoch - mange@freemail.hu
+;; Copyright (C) 2004 - Mathias Dahl
+
+;; 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)
+(require 'jabber-util)
+
+(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-history-muc-enabled nil
+ "Non-nil means MUC logging is enabled.
+Default is nil, cause MUC logging may be i/o-intensive."
+ :type 'boolean
+ :group 'jabber-history)
+
+(defcustom jabber-history-dir
+ (locate-user-emacs-file "jabber-history" ".emacs-jabber")
+ "Base directory where per-contact history files are stored.
+Used only when `jabber-use-global-history' is nil."
+ :type 'directory
+ :group 'jabber-history)
+
+(defcustom jabber-global-history-filename
+ (locate-user-emacs-file "jabber-global-message-log" ".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-use-global-history
+ ;; Using a global history file by default was a bad idea. Let's
+ ;; default to per-user files unless the global history file already
+ ;; exists, to avoid breaking existing installations.
+ (file-exists-p jabber-global-history-filename)
+ "Whether to use a global file for message history.
+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-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)
+
+(defvar jabber-history-inhibit-received-message-functions nil
+ "Functions determining whether to log an incoming message stanza.
+The functions in this list are called with two arguments,
+the connection and the full message stanza.
+If any of the functions returns non-nil, the stanza is not logged
+in the message 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 (jc 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))
+ (let ((is-muc (jabber-muc-message-p xml-data)))
+ (when (and jabber-history-enabled
+ (or
+ (not is-muc) ;chat message or private MUC message
+ (and jabber-history-muc-enabled is-muc))) ;muc message and muc logging active
+ (unless (run-hook-with-args-until-success
+ 'jabber-history-inhibit-received-message-functions
+ jc 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 (jabber-message-timestamp xml-data)))
+ (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
+ ;; jabber-jid-symbol is the best canonicalization we have.
+ (concat jabber-history-dir
+ "/" (symbol-name (jabber-jid-symbol 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 (when from
+ (prin1-to-string from))
+ "\"me\"")
+ (or (when to
+ (prin1-to-string 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))
+ (condition-case e
+ (write-region (point-min) (point-max) history-file t 'quiet)
+ (error
+ (message "Unable to write history: %s" (error-message-string e)))))))
+
+(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))
+ (if jabber-use-global-history
+ (insert-file-contents history-file)
+ (let* ((lines-collected nil)
+ (matched-files
+ (directory-files jabber-history-dir t
+ (concat "^"
+ (regexp-quote (file-name-nondirectory
+ history-file)))))
+ (matched-files
+ (cons (car matched-files)
+ (sort (cdr matched-files) 'string>-numerical))))
+ (while (not lines-collected)
+ (if (null matched-files)
+ (setq lines-collected t)
+ (let ((file (pop matched-files)))
+ (progn
+ (insert-file-contents file)
+ (when (numberp number)
+ (if (>= (count-lines (point-min) (point-max)) number)
+ (setq lines-collected t))))))))))
+ (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."
+ (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)))
+
+(defun jabber-history-move-to-per-user ()
+ "Migrate global history to per-user files."
+ (interactive)
+ (when (file-directory-p jabber-history-dir)
+ (error "Per-user history directory already exists"))
+ (make-directory jabber-history-dir)
+ (let ((jabber-use-global-history nil))
+ (with-temp-buffer
+ (let ((coding-system-for-read 'utf-8))
+ (insert-file-contents jabber-global-history-filename))
+ (let ((progress-reporter
+ (when (fboundp 'make-progress-reporter)
+ (make-progress-reporter "Migrating history..."
+ (point-min) (point-max))))
+ ;;(file-table (make-hash-table :test 'equal))
+ ;; Keep track of blocks of entries pertaining to the same JID.
+ current-jid jid-start)
+ (while (not (eobp))
+ (let* ((start (point))
+ (end (progn (forward-line) (point)))
+ (line (buffer-substring start end))
+ (parsed (car (read-from-string line)))
+ (jid (if (string= (aref parsed 2) "me")
+ (aref parsed 3)
+ (aref parsed 2))))
+ ;; Whenever there is a change in JID...
+ (when (not (equal jid current-jid))
+ (when current-jid
+ ;; ...save data for previous JID...
+ (let ((history-file (jabber-history-filename current-jid)))
+ (write-region jid-start start history-file t 'quiet)))
+ ;; ...and switch to new JID.
+ (setq current-jid jid)
+ (setq jid-start start))
+ (when (fboundp 'progress-reporter-update)
+ (progress-reporter-update progress-reporter (point)))))
+ ;; Finally, save the last block, if any.
+ (when current-jid
+ (let ((history-file (jabber-history-filename current-jid)))
+ (write-region jid-start (point-max) history-file t 'quiet))))))
+ (message "Done. Please change `jabber-use-global-history' now."))
+
+(provide 'jabber-history)
+
+;; arch-tag: 0AA0C235-3FC0-11D9-9FE7-000A95C2FCD0