Files
emacs.d/elpa/weechat-20180513.310/weechat-relay.el
Mateus Pinto Rodrigues 0024c08036 Update all my elpa files
2018-05-17 18:02:17 -03:00

633 lines
24 KiB
EmacsLisp

;;; weechat-relay --- Implementation of Weechat's relay protocol ;; -*- lexical-binding: t -*-
;; Copyright (C) 2013 Moritz Ulrich
;; Author: Moritz Ulrich (moritz@tarn-vedra.de)
;; Rüdiger Sonderfeld <ruediger@c-plusplus.de>
;; Aristid Breitkreuz <aristidb@gmail.com>
;; Keywords: irc chat network weechat
;; 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:
;; This file implements low-level code used by weechat.el to connect
;; to remote WeeChat instances using the relay protocol:
;; http://www.weechat.org/files/doc/devel/weechat_relay_protocol.en.html
(require 'weechat-core)
(require 'bindat)
(require 'format-spec)
(require 's)
(require 'pp)
(require 'cl-lib)
(defcustom weechat-relay-buffer-name "*weechat-relay*"
"Buffer holding the connection to the host weechat instance."
:type 'string
:group 'weechat-relay)
(defcustom weechat-relay-message-function nil
"Function to call when receiving a new weechat message."
:type '(choice (const :tag "Off" nil)
(function :tag "Callback Function"))
:group 'weechat-relay)
(defvar weechat-relay-ignored-message-ids '("_nicklist")
"IDs to ignore.")
(defcustom weechat-relay-disconnect-hook nil
"Hook run when the relay disconnects.
It will NOT run when the user disconnects via
`weechat-disconnect'."
:type 'hook
:group 'weechat-relay)
(defcustom weechat-relay-connect-hook nil
"Hook run when the relay connects.
Note: This DOES NOT mean the client can is already authenticated
to the relay server."
:type 'hook
:group 'weechat-relay)
(defcustom weechat-relay-ping-idle-seconds 60
"Idle time in seconds after weechat.el will ping the server."
:type '(choice integer
(const nil))
:group 'weechat-relay)
;;; Code:
(defvar weechat--relay-id-callback-hash (make-hash-table :test 'equal)
"Alist mapping from ids to functions.
Incoming message-ids will be searched in this alist and the
corresponding function will be called.")
(defun weechat--relay-send-message (text &optional id)
"Send message TEXT with optional ID.
Trim TEXT prior to sending it."
(let ((msg (concat (when id (format "(%s) " id)) (s-trim text) "\n")))
(weechat-relay-log (format "Sending msg: '%s'" (s-trim msg))
:debug)
(let ((process (get-buffer-process weechat-relay-buffer-name)))
(if process
(send-string process msg)
(weechat-warn "`get-buffer-process' returned nil for `weechat-relay-buffer-name'")))))
(defun weechat-relay-authenticate (password &optional compression)
"Authenticate to weechat with PASSWORD.
PASSWORD can be a string, a function or nil.
If COMPRESSION is non-nil, enable compression on this connection.
Currently unsupported."
(cl-assert (or (null password)
(stringp password)
(functionp password)))
(let ((pass (if (functionp password)
(funcall password)
password)))
(when (stringp pass)
(setq pass (if (s-blank? (s-trim pass))
nil
(s-trim pass))))
(weechat--relay-send-message
(concat "init "
(s-join ","
(cl-remove-if #'s-blank?
(list
(when pass (format "password=%s" (s-trim pass)))
(format "compression=%s" (if compression "zlib" "off")))))
"\n"))))
(defun weechat--relay-bindat-unsigned-to-signed (num bytes)
"Convert an unsigned int NUM to signed int.
NUM is in two-complement representation with BYTES bytes.
Useful because bindat does not support signed numbers."
(if (> num (- (expt 2 (- (* 8 bytes) 1)) 1))
(- num (expt 2 (* 8 bytes)))
num))
(defun weechat--relay-unpack-int (data)
"Unpack a four-byte signed integer from unibyte string DATA.
Return the value and number of bytes consumed."
(cl-values
(weechat--relay-bindat-unsigned-to-signed
(bindat-get-field
(bindat-unpack '((val u32)) data)
'val)
4)
4))
(defconst weechat--relay-lon-spec
'((len u8)
(val str (eval (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field struct 'len)
1)))))
(defun weechat--relay-unpack-lon (data)
(let ((obj (bindat-unpack weechat--relay-lon-spec data)))
(cl-values (string-to-number (decode-coding-string (bindat-get-field obj 'val) 'utf-8))
(bindat-length weechat--relay-lon-spec obj))))
(defun weechat--relay-unpack-chr (data)
"Unpack a one byte char from unibyte string DATA.
Returns value and bytes consumed."
(cl-values
(bindat-get-field
(bindat-unpack '((val u8)) data)
'val)
1))
(defconst weechat--relay-str-spec
'((len u32)
(val str (eval (let ((len (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field struct 'len)
4)))
;; Hack for signed/unsigned problems
(if (<= len 0) 0 len))))))
(defun weechat--relay-unpack-str (data)
"Unpacks a weechat-relay-string from unibyte string DATA.
Optional second return value contains length of parsed data."
(let ((obj (bindat-unpack weechat--relay-str-spec data)))
(cl-values (decode-coding-string (bindat-get-field obj 'val) 'utf-8)
(bindat-length weechat--relay-str-spec obj))))
(defconst weechat--relay-buf-spec
'((len u32)
(val vec (eval (let ((len (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field struct 'len)
4)))
;; Hack for signed/unsigned problems
(if (<= len 0) 0 len))))))
(defun weechat--relay-unpack-buf (data)
(let ((obj (bindat-unpack weechat--relay-buf-spec data)))
(cl-values (bindat-get-field obj 'val)
(bindat-length weechat--relay-buf-spec obj))))
(defconst weechat--relay-ptr-spec
'((len u8)
(val str (eval (let ((len (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field struct 'len)
1)))
;; Hack for signed/unsigned problems
(if (<= len 0) 0 len))))))
(defun weechat--relay-unpack-ptr (data)
"Unpack a string encoded in weechat's binary representation.
DATA must be an unibyte string. Return string-value and number
of bytes consumed."
(let ((obj (bindat-unpack weechat--relay-ptr-spec data)))
(cl-values (unless (string= "0" (bindat-get-field obj 'val))
(concat "0x" (bindat-get-field obj 'val)))
(bindat-length weechat--relay-ptr-spec obj))))
(defconst weechat--relay-tim-spec
'((len u8)
(val str (eval (let ((len (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field struct 'len)
1)))
;; Hack for signed/unsigned problems
(if (<= len 0) 0 len))))))
(defun weechat--relay-unpack-tim (data)
(let ((obj (bindat-unpack weechat--relay-tim-spec data)))
(cl-values (let ((val (string-to-number
(bindat-get-field obj 'val))))
(unless (zerop val)
(seconds-to-time val)))
(bindat-length weechat--relay-tim-spec obj))))
(defconst weechat--relay-htb-spec
'((key-type str 3)
(val-type str 3)
(count u32)))
(defun weechat--relay-unpack-htb (data)
(let* ((obj (bindat-unpack weechat--relay-htb-spec data))
(count (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field obj 'count)
4))
(key-type (bindat-get-field obj 'key-type))
(val-type (bindat-get-field obj 'val-type))
(key-fn (symbol-function (intern (concat "weechat--relay-unpack-" key-type))))
(val-fn (symbol-function (intern (concat "weechat--relay-unpack-" val-type))))
(offset (bindat-length weechat--relay-htb-spec obj))
(acc ()))
(dotimes (_ count)
(cl-multiple-value-bind (key key-len) (funcall key-fn (substring data offset))
(cl-multiple-value-bind (val val-len) (funcall val-fn (substring data (+ offset key-len)))
(setq acc (cons (cons key val) acc))
(setq offset (+ offset key-len val-len)))))
(cl-values acc
offset)))
(defconst weechat--relay-arr-spec
'((type str 3)
(count u32)))
(defun weechat--relay-unpack-arr (data)
(let* ((obj (bindat-unpack weechat--relay-arr-spec data))
(count (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field obj 'count)
4))
(type (bindat-get-field obj 'type))
(unpack-fn (symbol-function (intern (concat "weechat--relay-unpack-" type))))
(offset (bindat-length weechat--relay-arr-spec obj))
(acc ()))
(dotimes (_ count)
(cl-multiple-value-bind (val val-len) (funcall unpack-fn (substring data offset))
(setq acc (append acc (list val)))
(setq offset (+ offset val-len))))
(cl-values acc offset)))
(defalias 'weechat--relay-parse-chr 'weechat--relay-unpack-chr)
(defalias 'weechat--relay-parse-int 'weechat--relay-unpack-int)
(defalias 'weechat--relay-parse-lon 'weechat--relay-unpack-lon)
(defalias 'weechat--relay-parse-str 'weechat--relay-unpack-str)
(defalias 'weechat--relay-parse-buf 'weechat--relay-unpack-buf)
(defalias 'weechat--relay-parse-ptr 'weechat--relay-unpack-ptr)
(defalias 'weechat--relay-parse-tim 'weechat--relay-unpack-tim)
(defalias 'weechat--relay-parse-arr 'weechat--relay-unpack-arr)
(defun weechat--relay-parse-inf (data)
(cl-multiple-value-bind (name len) (weechat--relay-unpack-str data)
(cl-multiple-value-bind (value len*) (weechat--relay-unpack-str (substring data len))
(cl-values (cons name value)
(+ len len*)))))
(defconst weechat--relay-inl-item-spec
'((name struct weechat--relay-str-spec)
(type str 3)))
(defun weechat--relay-parse-inl-item (data)
(let* ((count (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field
(bindat-unpack '((len u32)) data) 'len)
4))
(offset 4)
(acc ()))
(while (< (length acc) count)
(let* ((obj (bindat-unpack weechat--relay-inl-item-spec
(substring data offset)))
(fun (symbol-function (intern (concat "weechat--relay-unpack-"
(bindat-get-field obj 'type))))))
(setq offset (+ offset (bindat-length weechat--relay-inl-item-spec obj)))
(cl-multiple-value-bind (value offset*) (funcall fun (substring data offset))
(setq offset (+ offset offset*))
(setq acc (cons
(cons (bindat-get-field obj 'name 'val) value)
acc)))))
(cl-values acc
offset)))
(defconst weechat--relay-inl-spec
'((name struct weechat--relay-str-spec)
(count u32)))
(defun weechat--relay-parse-inl (data)
(let* ((obj (bindat-unpack weechat--relay-inl-spec data))
(acc ())
(count (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field obj 'count)
4))
(offset (bindat-length weechat--relay-inl-spec obj)))
(dotimes (_ count)
(cl-multiple-value-bind (item offset*) (weechat--relay-parse-inl-item (substring data offset))
(setq acc (cons item acc))
(setq offset (+ offset offset*))))
(cl-values acc
offset)))
(defun weechat--relay-parse-hda-item (h-path-length name-type-alist data)
(let ((p-path ())
(offset 0)
(result ()))
(dotimes (_ h-path-length)
(cl-multiple-value-bind (el offset*) (weechat--relay-unpack-ptr (substring data offset))
(setq p-path (cons el p-path))
(setq offset (+ offset offset*))))
(dolist (name-type name-type-alist)
(let ((fun (symbol-function (intern (concat "weechat--relay-unpack-" (cdr name-type))))))
(cl-multiple-value-bind (obj offset*) (funcall fun (substring data offset))
(setq result (cons (cons (car name-type) obj) result))
(setq offset (+ offset offset*)))))
(cl-values (cons (reverse p-path) result)
offset)))
(defconst weechat--relay-hdh-spec
'((h-path struct weechat--relay-str-spec)
(keys struct weechat--relay-str-spec)
(count u32)))
;;; from http://lists.gnu.org/archive/html/help-gnu-emacs/2009-06/msg00764.html
(defun weechat--partition-list (list length)
(cl-loop while list
collect (cl-subseq list 0 length)
do (setf list (nthcdr length list))))
(defun weechat--hda-split-keys-string (str)
(mapcar (lambda (x)
(cons (car x)
(cadr x)))
(weechat--partition-list (split-string str "[:,]") 2)))
(defun weechat--relay-parse-hda (data)
(let* ((obj (bindat-unpack weechat--relay-hdh-spec data))
(count (weechat--relay-bindat-unsigned-to-signed
(bindat-get-field obj 'count)
4))
(name-type-alist (weechat--hda-split-keys-string
(bindat-get-field obj 'keys 'val)))
(h-path-length (length (split-string (bindat-get-field obj 'h-path 'val) "[/]")))
(offset (+ (bindat-length weechat--relay-hdh-spec obj)))
(acc ()))
(dotimes (_ count)
(cl-multiple-value-bind (obj offset*) (weechat--relay-parse-hda-item
h-path-length name-type-alist (substring data offset))
(setq acc (cons obj acc))
(setq offset (+ offset offset*))))
(let ((h-path (bindat-get-field obj 'h-path 'val)))
(cl-values (list h-path acc)
offset))))
(defconst weechat--relay-message-spec
'((length u32)
(compression u8)
(id struct weechat--relay-str-spec)
(data vec (eval (let ((l (- (bindat-get-field struct 'length)
4 ;length
1 ;compression
(+ 4 (length (bindat-get-field struct 'id 'val))))))
l)))))
(defun weechat--unpack-message-contents (data)
(let* ((type (substring data 0 3))
(fun (symbol-function (intern (concat "weechat--relay-parse-" type)))))
(cl-multiple-value-bind (obj len) (funcall fun (string-make-unibyte (substring data 3)))
(cl-values obj
(+ len 3)))))
(defun weechat-unpack-message (message-data)
"Unpack weechat relay message in MESSAGE-DATA.
Return a list: (id data)."
(let* ((msg (bindat-unpack weechat--relay-message-spec message-data))
(data (concat (bindat-get-field msg 'data)))
(msg-id (bindat-get-field msg 'id 'val))
(ignore-msg (member msg-id weechat-relay-ignored-message-ids))
(offset 0)
(acc ()))
;; Only no-compression is supported atm
(unless (= 0 (bindat-get-field msg 'compression))
(error "Compression not supported"))
(unless ignore-msg
(while (< offset (length data))
(cl-multiple-value-bind (obj offset*) (weechat--unpack-message-contents
(substring data offset))
(setq offset (+ offset offset*))
(setq acc (cons obj acc)))))
(cl-values (cons msg-id (if ignore-msg '(ignored) (reverse acc)))
(bindat-get-field msg 'length))))
(defun weechat--message-available-p ()
"Check if a weechat relay message available in current buffer."
(and (> (buffer-size) 5)
(>= (buffer-size)
(bindat-get-field
(bindat-unpack '((len u32))
(buffer-string))
'len))))
(defun weechat--relay-parse-new-message ()
(when (weechat--message-available-p)
(cl-multiple-value-bind (ret len) (weechat-unpack-message
(buffer-string))
(weechat-relay-log (format "Consumed %d bytes" len) :debug)
(let ((inhibit-read-only t))
(delete-region (point-min) (+ (point-min) len)))
ret)))
(defun weechat-relay-get-id-callback (id)
(gethash id weechat--relay-id-callback-hash))
(defun weechat-relay-remove-id-callback (id)
(let ((fun (weechat-relay-get-id-callback id)))
(remhash id weechat--relay-id-callback-hash)
fun))
(defun weechat-relay-add-id-callback (id function &optional one-shot force)
(unless id
(error "ID must not be nil"))
(when (weechat-relay-get-id-callback id)
(unless force
(error "ID '%s' is already in `weechat--relay-id-callback-hash'" id))
(weechat-relay-remove-id-callback id))
(let ((function* (if one-shot
(lambda (x)
(funcall function x)
(weechat-relay-remove-id-callback id))
function)))
(puthash id function* weechat--relay-id-callback-hash)))
(defun weechat-relay-send-command (command &optional callback)
"Send COMMAND to relay and call CALLBACK with reply.
CALLBACK takes one argument (the response data) which is a list."
(let ((id (symbol-name (cl-gensym))))
(when (functionp callback)
(weechat-relay-add-id-callback id callback 'one-shot))
(weechat--relay-send-message command id)))
(defvar weechat-relay-last-receive nil
"Stores the time when the last message was received.")
(defun weechat--relay-send-ping (&optional pong-callback)
(weechat-relay-send-command "info version" pong-callback))
;;; TODO: Move this functionality to weechat.el
(defvar weechat--relay-ping-timer nil)
(defun weechat--relay-stop-ping-timer ()
(when (timerp weechat--relay-ping-timer)
(cancel-timer weechat--relay-ping-timer)
(setq weechat--relay-ping-timer nil)))
(defun weechat--relay-start-ping-timer ()
(weechat--relay-stop-ping-timer)
(setq weechat--relay-ping-timer
(run-with-timer 0
(/ weechat-relay-ping-idle-seconds 2)
(lambda ()
(if (weechat-relay-connected-p)
(when (>= (float-time (time-since weechat-relay-last-receive))
weechat-relay-ping-idle-seconds)
(weechat--relay-send-ping))
;; Stop the ping timer if we aren't connected
(weechat--relay-stop-ping-timer))))))
(defun weechat--relay-process-filter (proc string)
(with-current-buffer (process-buffer proc)
(weechat-relay-log (format "Received %d bytes" (length string)) :debug)
;; Insert the text, advancing the process marker.
(goto-char (point-max))
(let ((inhibit-read-only t))
(insert (string-make-unibyte string)))
(let ((inhibit-redisplay t))
(while (weechat--message-available-p)
(let* ((data (weechat--relay-parse-new-message))
(id (weechat--message-id data)))
(setq weechat-relay-last-receive (current-time))
;; If buffer is available, log message.
(when (eq weechat-relay-log-level :debug)
(weechat-relay-log (pp-to-string data) :debug))
;; Call `weechat-relay-message-function'
(when (functionp weechat-relay-message-function)
(funcall weechat-relay-message-function data))
;; Call callback from `weechat--relay-id-callback-hash'
(if (functionp (weechat-relay-get-id-callback id))
(funcall (weechat-relay-get-id-callback id)
(weechat--message-data data))))))))
(defvar weechat--relay-connected-callback)
(defun weechat--relay-handle-process-status (status)
(weechat-relay-log (format "Received status event: %s\n" status))
(cl-case status
((closed exit) (run-hooks 'weechat-relay-disconnect-hook))
(failed (error "Failed to connect to weechat relay")
(weechat-relay-disconnect))))
(defun weechat--relay-process-sentinel (proc _)
(weechat--relay-handle-process-status (process-status proc)))
(defun weechat--relay-open-socket (name buffer host service)
(make-network-process :name name
:buffer buffer
:host host
:service service
:coding 'binary
:keepalive t
:noquery t))
(defun weechat--relay-plain-socket (bname host port)
(weechat-relay-log (format "PLAIN %s:%d" host port) :info)
(weechat--relay-open-socket "weechat-relay" bname host port))
(defun weechat--relay-tls-socket (bname host port)
(weechat-relay-log (format "TLS %s:%d" host port) :info)
(require 'gnutls)
(open-network-stream "weechat-relay-tls"
bname
host
port
:type 'tls
:coding 'binary))
(defun weechat--relay-from-command (cmdspec)
(lambda (bname host port)
(let ((cmd (format-spec cmdspec (format-spec-make
?h host
?p port))))
(weechat-relay-log (format "COMMAND %s:%s: `%s'" host port cmd))
(let ((process-connection-type nil)) ; Use a pipe.
(start-process-shell-command "weechat-relay-cmd" bname cmd)))))
(defun weechat-relay-connect (host port mode &optional callback)
"Open a new weechat relay connection to HOST at PORT.
Argument MODE Null or 'plain for a plain socket, t or 'ssl for a TLS socket;
a string denotes a command to run. You can use %h and %p to interpolate host
and port number respectively.
Optional argument CALLBACK Called after initialization is finished."
;; Clean relay buffer to start with clean state
(with-current-buffer (get-buffer-create weechat-relay-buffer-name)
(let ((inhibit-read-only t))
(delete-region (point-min) (point-max))))
(let* ((pfun (cond
((or (null mode) (eq mode 'plain)) #'weechat--relay-plain-socket)
((or (eq mode t) (eq mode 'ssl)) #'weechat--relay-tls-socket)
((stringp mode) (weechat--relay-from-command mode))))
(process
(funcall pfun weechat-relay-buffer-name host port)))
(set-process-sentinel process #'weechat--relay-process-sentinel)
(set-process-coding-system process 'binary)
(set-process-filter process #'weechat--relay-process-filter)
(with-current-buffer (get-buffer weechat-relay-buffer-name)
(setq buffer-read-only t)
(set-buffer-multibyte nil)
(buffer-disable-undo)))
(with-current-buffer (get-buffer-create
weechat-relay-log-buffer-name)
(buffer-disable-undo))
(when (functionp callback)
(funcall callback))
(run-hooks 'weechat-relay-connect-hook))
(defun weechat-relay-connected-p ()
(and (get-buffer weechat-relay-buffer-name)
(get-buffer-process weechat-relay-buffer-name)
(process-live-p (get-buffer-process weechat-relay-buffer-name))
t))
(defun weechat-relay-disconnect ()
"Disconnect current weechat relay connection and close all buffers."
(when (weechat-relay-connected-p)
(weechat--relay-send-message "quit")
(with-current-buffer weechat-relay-buffer-name
(let ((proc (get-buffer-process (current-buffer))))
;; When disconnecting interactively (e.g. when this function
;; is called), prevent running any disconnect hooks.
(set-process-sentinel proc nil)
(delete-process proc)
;; Stop the ping timer
(weechat--relay-stop-ping-timer))
(kill-buffer))
(when (get-buffer weechat-relay-log-buffer-name)
(kill-buffer weechat-relay-log-buffer-name))))
(defun weechat--message-id (message)
(car message))
(defun weechat--message-data (message)
"Return a list with data in MESSAGE."
(cdr message))
(defun weechat--hdata-path (hdata)
(car hdata))
(defun weechat--hdata-values (hdata)
(cadr hdata))
(defun weechat--hdata-value-pointer-path (value)
(car value))
(defun weechat--hdata-value-alist (value)
(cdr value))
(defun weechat-unload-function ()
(ad-disable-advice 'open-gnutls-stream 'around 'weechat-verifying))
(provide 'weechat-relay)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; weechat-relay.el ends here