summaryrefslogtreecommitdiff
path: root/jabber-rtt.el
diff options
context:
space:
mode:
Diffstat (limited to 'jabber-rtt.el')
-rw-r--r--jabber-rtt.el321
1 files changed, 321 insertions, 0 deletions
diff --git a/jabber-rtt.el b/jabber-rtt.el
new file mode 100644
index 0000000..8d34850
--- /dev/null
+++ b/jabber-rtt.el
@@ -0,0 +1,321 @@
+;;; jabber-rtt.el --- XEP-0301: In-Band Real Time Text
+
+;; Copyright (C) 2013 Magnus Henoch
+
+;; Author: Magnus Henoch <magnus.henoch@gmail.com>
+
+;; 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 3 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, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+
+(eval-when-compile (require 'cl))
+
+;;;; Handling incoming events
+
+;;;###autoload
+(eval-after-load "jabber-disco"
+ '(jabber-disco-advertise-feature "urn:xmpp:rtt:0"))
+
+(defvar jabber-rtt-ewoc-node nil)
+(make-variable-buffer-local 'jabber-rtt-ewoc-node)
+
+(defvar jabber-rtt-last-seq nil)
+(make-variable-buffer-local 'jabber-rtt-last-seq)
+
+(defvar jabber-rtt-message nil)
+(make-variable-buffer-local 'jabber-rtt-message)
+
+(defvar jabber-rtt-pending-events nil)
+(make-variable-buffer-local 'jabber-rtt-pending-events)
+
+(defvar jabber-rtt-timer nil)
+(make-variable-buffer-local 'jabber-rtt-timer)
+
+;; Add function last in chain, so a chat buffer is already created.
+;;;###autoload
+(eval-after-load "jabber-core"
+ '(add-to-list 'jabber-message-chain #'jabber-rtt-handle-message t))
+
+;;;###autoload
+(defun jabber-rtt-handle-message (jc xml-data)
+ ;; We could support this for MUC as well, if useful.
+ (when (and (not (jabber-muc-message-p xml-data))
+ (get-buffer (jabber-chat-get-buffer (jabber-xml-get-attribute xml-data 'from))))
+ (with-current-buffer (jabber-chat-get-buffer (jabber-xml-get-attribute xml-data 'from))
+ (let* ((rtt (jabber-xml-path xml-data '(("urn:xmpp:rtt:0" . "rtt"))))
+ (body (jabber-xml-path xml-data '(body)))
+ (seq (when rtt (jabber-xml-get-attribute rtt 'seq)))
+ (event (when rtt (or (jabber-xml-get-attribute rtt 'event) "edit")))
+ (actions (when rtt (jabber-xml-node-children rtt)))
+ (inhibit-read-only t))
+ (cond
+ ((or body (string= event "cancel"))
+ ;; A <body/> element supersedes real time text.
+ (jabber-rtt--reset))
+ ((member event '("new" "reset"))
+ (jabber-rtt--reset)
+ (setq jabber-rtt-ewoc-node
+ (ewoc-enter-last jabber-chat-ewoc (list :notice "[typing...]"))
+ jabber-rtt-last-seq (string-to-number seq)
+ jabber-rtt-message ""
+ jabber-rtt-pending-events nil)
+ (jabber-rtt--enqueue-actions actions))
+ ((string= event "edit")
+ ;; TODO: check whether this works properly in 32-bit Emacs
+ (cond
+ ((and jabber-rtt-last-seq
+ (equal (1+ jabber-rtt-last-seq)
+ (string-to-number seq)))
+ ;; We are in sync.
+ (setq jabber-rtt-last-seq (string-to-number seq))
+ (jabber-rtt--enqueue-actions actions))
+ (t
+ ;; TODO: show warning when not in sync
+ (message "out of sync! %s vs %s"
+ seq jabber-rtt-last-seq))
+ ))
+ ;; TODO: handle event="init"
+ )))))
+
+(defun jabber-rtt--reset ()
+ (when jabber-rtt-ewoc-node
+ (ewoc-delete jabber-chat-ewoc jabber-rtt-ewoc-node))
+ (when (timerp jabber-rtt-timer)
+ (cancel-timer jabber-rtt-timer))
+ (setq jabber-rtt-ewoc-node nil
+ jabber-rtt-last-seq nil
+ jabber-rtt-message nil
+ jabber-rtt-pending-events nil
+ jabber-rtt-timer nil))
+
+(defun jabber-rtt--enqueue-actions (new-actions)
+ (setq jabber-rtt-pending-events
+ ;; Ensure that the queue never contains more than 700 ms worth
+ ;; of wait events.
+ (jabber-rtt--fix-waits (append jabber-rtt-pending-events new-actions)))
+ (unless jabber-rtt-timer
+ (jabber-rtt--process-actions (current-buffer))))
+
+(defun jabber-rtt--process-actions (buffer)
+ (with-current-buffer buffer
+ (setq jabber-rtt-timer nil)
+ (catch 'wait
+ (while jabber-rtt-pending-events
+ (let ((action (pop jabber-rtt-pending-events)))
+ (case (jabber-xml-node-name action)
+ ((t)
+ ;; insert text
+ (let* ((p (jabber-xml-get-attribute action 'p))
+ (position (if p (string-to-number p) (length jabber-rtt-message))))
+ (setq position (max position 0))
+ (setq position (min position (length jabber-rtt-message)))
+ (setf (substring jabber-rtt-message position position)
+ (car (jabber-xml-node-children action)))
+
+ (ewoc-set-data jabber-rtt-ewoc-node (list :notice (concat "[typing...] " jabber-rtt-message)))
+ (let ((inhibit-read-only t))
+ (ewoc-invalidate jabber-chat-ewoc jabber-rtt-ewoc-node))))
+ ((e)
+ ;; erase text
+ (let* ((p (jabber-xml-get-attribute action 'p))
+ (position (if p (string-to-number p) (length jabber-rtt-message)))
+ (n (jabber-xml-get-attribute action 'n))
+ (number (if n (string-to-number n) 1)))
+ (setq position (max position 0))
+ (setq position (min position (length jabber-rtt-message)))
+ (setq number (max number 0))
+ (setq number (min number position))
+ ;; Now erase the NUMBER characters before POSITION.
+ (setf (substring jabber-rtt-message (- position number) position)
+ "")
+
+ (ewoc-set-data jabber-rtt-ewoc-node (list :notice (concat "[typing...] " jabber-rtt-message)))
+ (let ((inhibit-read-only t))
+ (ewoc-invalidate jabber-chat-ewoc jabber-rtt-ewoc-node))))
+ ((w)
+ (setq jabber-rtt-timer
+ (run-with-timer
+ (/ (string-to-number (jabber-xml-get-attribute action 'n)) 1000.0)
+ nil
+ #'jabber-rtt--process-actions
+ buffer))
+ (throw 'wait nil))))))))
+
+(defun jabber-rtt--fix-waits (actions)
+ ;; Ensure that the sum of all wait events is no more than 700 ms.
+ (let ((sum 0))
+ (dolist (action actions)
+ (when (eq (jabber-xml-node-name action) 'w)
+ (let ((n (jabber-xml-get-attribute action 'n)))
+ (setq n (string-to-number n))
+ (when (>= n 0)
+ (setq sum (+ sum n))))))
+
+ (if (<= sum 700)
+ actions
+ (let ((scale (/ 700.0 sum)))
+ (mapcar
+ (lambda (action)
+ (if (eq (jabber-xml-node-name action) 'w)
+ (let ((n (jabber-xml-get-attribute action 'n)))
+ (setq n (string-to-number n))
+ (setq n (max n 0))
+ `(w ((n . ,(number-to-string (* scale n)))) nil))
+ action))
+ actions)))))
+
+;;;; Sending events
+
+(defvar jabber-rtt-send-timer nil)
+(make-variable-buffer-local 'jabber-rtt-send-timer)
+
+(defvar jabber-rtt-send-seq nil)
+(make-variable-buffer-local 'jabber-rtt-send-seq)
+
+(defvar jabber-rtt-outgoing-events nil)
+(make-variable-buffer-local 'jabber-rtt-outgoing-events)
+
+(defvar jabber-rtt-send-last-timestamp nil)
+(make-variable-buffer-local 'jabber-rtt-send-last-timestamp)
+
+;;;###autoload
+(define-minor-mode jabber-rtt-send-mode
+ "Show text to recipient as it is being typed.
+This lets the recipient see every change made to the message up
+until it's sent. The recipient's client needs to implement
+XEP-0301, In-Band Real Time Text."
+ nil " Real-Time" nil
+ (if (null jabber-rtt-send-mode)
+ (progn
+ (remove-hook 'after-change-functions #'jabber-rtt--queue-update t)
+ (remove-hook 'jabber-chat-send-hooks #'jabber-rtt--message-sent t)
+ (jabber-rtt--cancel-send))
+ (unless (derived-mode-p 'jabber-chat-mode)
+ (error "Real Time Text only makes sense in chat buffers"))
+ (when (timerp jabber-rtt-send-timer)
+ (cancel-timer jabber-rtt-send-timer))
+ (setq jabber-rtt-send-timer nil
+ jabber-rtt-send-seq nil
+ jabber-rtt-outgoing-events nil
+ jabber-rtt-send-last-timestamp nil)
+ (jabber-rtt--send-current-text nil)
+ (add-hook 'after-change-functions #'jabber-rtt--queue-update nil t)
+ (add-hook 'jabber-chat-send-hooks #'jabber-rtt--message-sent nil t)))
+
+(defun jabber-rtt--cancel-send ()
+ (when (timerp jabber-rtt-send-timer)
+ (cancel-timer jabber-rtt-send-timer))
+ (setq jabber-rtt-send-seq (1+ jabber-rtt-send-seq))
+ (jabber-send-sexp jabber-buffer-connection
+ `(message ((to . ,jabber-chatting-with)
+ (type . "chat"))
+ (rtt ((xmlns . "urn:xmpp:rtt:0")
+ (seq . ,(number-to-string jabber-rtt-send-seq))
+ (event . "cancel"))
+ nil)))
+ (setq jabber-rtt-send-timer nil
+ jabber-rtt-send-seq nil
+ jabber-rtt-outgoing-events nil
+ jabber-rtt-send-last-timestamp nil))
+
+(defun jabber-rtt--send-current-text (resetp)
+ (let ((text (buffer-substring-no-properties jabber-point-insert (point-max))))
+ ;; This should give us enough room to avoid wrap-arounds, even
+ ;; with just 28 bits...
+ (setq jabber-rtt-send-seq (random 100000))
+ (jabber-send-sexp jabber-buffer-connection
+ `(message ((to . ,jabber-chatting-with)
+ (type . "chat"))
+ (rtt ((xmlns . "urn:xmpp:rtt:0")
+ (seq . ,(number-to-string jabber-rtt-send-seq))
+ (event . ,(if resetp "reset" "new")))
+ (t () ,text))))))
+
+(defun jabber-rtt--queue-update (beg end pre-change-length)
+ (unless (or (< beg jabber-point-insert)
+ (< end jabber-point-insert))
+ (let ((timestamp (current-time)))
+ (when jabber-rtt-send-last-timestamp
+ (let* ((time-difference (time-subtract timestamp jabber-rtt-send-last-timestamp))
+ (interval (truncate (* 1000 (float-time time-difference)))))
+ (when (and (> interval 0)
+ ;; Don't send too long intervals - this should have
+ ;; been sent by our timer already.
+ (< interval 1000))
+ (push `(w ((n . ,(number-to-string interval))) nil)
+ jabber-rtt-outgoing-events))))
+ (setq jabber-rtt-send-last-timestamp timestamp))
+
+ (when (> pre-change-length 0)
+ ;; Some text was deleted. Let's check if we can use a shorter
+ ;; tag:
+ (let ((at-end (= end (point-max)))
+ (erase-one (= pre-change-length 1)))
+ (push `(e (
+ ,@(unless at-end
+ `((p . ,(number-to-string
+ (+ beg
+ (- jabber-point-insert)
+ pre-change-length)))))
+ ,@(unless erase-one
+ `((n . ,(number-to-string pre-change-length))))))
+ jabber-rtt-outgoing-events)))
+
+ (when (/= beg end)
+ ;; Some text was inserted.
+ (let ((text (buffer-substring-no-properties beg end))
+ (at-end (= end (point-max))))
+ (push `(t (
+ ,@(unless at-end
+ `((p . ,(number-to-string (- beg jabber-point-insert))))))
+ ,text)
+ jabber-rtt-outgoing-events)))
+
+ (when (null jabber-rtt-send-timer)
+ (setq jabber-rtt-send-timer
+ (run-with-timer 0.7 nil #'jabber-rtt--send-queued-events (current-buffer))))))
+
+(defun jabber-rtt--send-queued-events (buffer)
+ (with-current-buffer buffer
+ (setq jabber-rtt-send-timer nil)
+ (when jabber-rtt-outgoing-events
+ (let ((event (if jabber-rtt-send-seq "edit" "new")))
+ (setq jabber-rtt-send-seq
+ (if jabber-rtt-send-seq
+ (1+ jabber-rtt-send-seq)
+ (random 100000)))
+ (jabber-send-sexp jabber-buffer-connection
+ `(message ((to . ,jabber-chatting-with)
+ (type . "chat"))
+ (rtt ((xmlns . "urn:xmpp:rtt:0")
+ (seq . ,(number-to-string jabber-rtt-send-seq))
+ (event . ,event))
+ ,@(nreverse jabber-rtt-outgoing-events))))
+ (setq jabber-rtt-outgoing-events nil)))))
+
+(defun jabber-rtt--message-sent (_text _id)
+ ;; We're sending a <body/> element; reset our state
+ (when (timerp jabber-rtt-send-timer)
+ (cancel-timer jabber-rtt-send-timer))
+ (setq jabber-rtt-send-timer nil
+ jabber-rtt-send-seq nil
+ jabber-rtt-outgoing-events nil
+ jabber-rtt-send-last-timestamp nil))
+
+(provide 'jabber-rtt)
+;;; jabber-rtt.el ends here