feat: add JSON-RPC client with connection check command
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
;;; noise-rpc.el --- JSON-RPC client for signal-cli -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Communicates with a signal-cli daemon over HTTP JSON-RPC.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'json)
|
||||
(require 'url)
|
||||
(require 'cl-lib)
|
||||
|
||||
(defvar noise-signal-cli-address)
|
||||
|
||||
(define-error 'noise-rpc-error "signal-cli JSON-RPC error")
|
||||
|
||||
(defvar noise-rpc--next-id 0
|
||||
"Monotonically increasing request ID for JSON-RPC.")
|
||||
|
||||
(defun noise-rpc--plist-to-alist (plist)
|
||||
"Convert PLIST to an alist for json-encode.
|
||||
Strips the leading colon from keyword symbols."
|
||||
(let (alist)
|
||||
(while plist
|
||||
(let ((key-name (symbol-name (car plist))))
|
||||
(when (string-prefix-p ":" key-name)
|
||||
(setq key-name (substring key-name 1)))
|
||||
(push (cons key-name (cadr plist)) alist))
|
||||
(setq plist (cddr plist)))
|
||||
(nreverse alist)))
|
||||
|
||||
(defun noise-rpc--build-request (method &rest params)
|
||||
"Build a JSON-RPC 2.0 request string for METHOD with PARAMS.
|
||||
PARAMS is a plist of :key value pairs."
|
||||
(let* ((id (cl-incf noise-rpc--next-id))
|
||||
(req `((jsonrpc . "2.0")
|
||||
(method . ,method)
|
||||
(id . ,id))))
|
||||
(when params
|
||||
(setq req (append req `((params . ,(noise-rpc--plist-to-alist params))))))
|
||||
(json-encode req)))
|
||||
|
||||
(defun noise-rpc-call (method &rest params)
|
||||
"Call signal-cli JSON-RPC METHOD with PARAMS.
|
||||
PARAMS is a plist of :key value pairs.
|
||||
Returns the parsed `result` field on success.
|
||||
Signals `noise-rpc-error` on failure or connection error."
|
||||
(let* ((url-request-method "POST")
|
||||
(url-request-extra-headers
|
||||
'(("Content-Type" . "application/json")))
|
||||
(url-request-data (apply #'noise-rpc--build-request method params))
|
||||
(url (concat noise-signal-cli-address "/api/v1/rpc"))
|
||||
(url-show-status nil)
|
||||
(buf (condition-case nil
|
||||
(url-retrieve-synchronously url t)
|
||||
(error (signal 'noise-rpc-error
|
||||
(list (format "Could not connect to signal-cli at %s. Is signal-cli daemon running?"
|
||||
noise-signal-cli-address)))))))
|
||||
(if (not buf)
|
||||
(signal 'noise-rpc-error (list (format "No response from signal-cli at %s" noise-signal-cli-address)))
|
||||
(unwind-protect
|
||||
(with-current-buffer buf
|
||||
(goto-char (point-min))
|
||||
(re-search-forward "\n\n" nil t)
|
||||
(let* ((body (buffer-substring (point) (point-max)))
|
||||
(result (noise-rpc--parse-response body)))
|
||||
result))
|
||||
(kill-buffer buf)))))
|
||||
|
||||
(defun noise-rpc--parse-response (raw)
|
||||
"Parse RAW JSON-RPC response body.
|
||||
Returns the `result` on success.
|
||||
Signals `noise-rpc-error` on JSON-RPC error or malformed response."
|
||||
(let* ((parsed (condition-case nil
|
||||
(json-parse-string raw)
|
||||
(error (signal 'noise-rpc-error (list "signal-cli returned invalid JSON response"))))))
|
||||
(if-let ((err (gethash "error" parsed)))
|
||||
(signal 'noise-rpc-error
|
||||
(list (format "signal-cli error %s: %s"
|
||||
(gethash "code" err)
|
||||
(gethash "message" err))))
|
||||
(gethash "result" parsed))))
|
||||
|
||||
(defun noise-rpc-check ()
|
||||
"Check connectivity to signal-cli.
|
||||
Returns the version string on success.
|
||||
Signals `noise-rpc-error` on failure."
|
||||
(let ((result (noise-rpc-call "getVersion")))
|
||||
(gethash "version" result)))
|
||||
|
||||
(provide 'noise-rpc)
|
||||
;;; noise-rpc.el ends here
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'noise-rpc)
|
||||
|
||||
(defgroup noise nil
|
||||
"Signal Messenger client for Emacs."
|
||||
:group 'comm
|
||||
@@ -22,6 +24,16 @@ Format: http://HOST:PORT"
|
||||
:type 'string
|
||||
:group 'noise)
|
||||
|
||||
;;;###autoload
|
||||
(defun noise-check-connection ()
|
||||
"Check connectivity to the signal-cli JSON-RPC daemon."
|
||||
(interactive)
|
||||
(condition-case err
|
||||
(let ((version (noise-rpc-check)))
|
||||
(message "signal-cli connected (version %s)" version))
|
||||
(noise-rpc-error
|
||||
(message "Error: %s" (cdr err)))))
|
||||
|
||||
;;;###autoload
|
||||
(define-minor-mode noise-mode
|
||||
"Toggle Noise Signal client mode."
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
;;; noise-rpc-test.el --- Tests for noise-rpc -*- lexical-binding: t; -*-
|
||||
|
||||
(require 'ert)
|
||||
(require 'noise-rpc)
|
||||
|
||||
(ert-deftest noise-rpc--build-request-no-params ()
|
||||
"Test that noise-rpc--build-request produces valid JSON-RPC 2.0 without params."
|
||||
(let* ((body (noise-rpc--build-request "getVersion"))
|
||||
(parsed (json-parse-string body)))
|
||||
(should (string= "2.0" (gethash "jsonrpc" parsed)))
|
||||
(should (string= "getVersion" (gethash "method" parsed)))
|
||||
(should (integerp (gethash "id" parsed)))
|
||||
(should-not (gethash "params" parsed))))
|
||||
|
||||
(ert-deftest noise-rpc--build-request-with-params ()
|
||||
"Test that params are included when provided."
|
||||
(let* ((body (noise-rpc--build-request "sendMessage" :recipient "+123" :message "hi"))
|
||||
(parsed (json-parse-string body)))
|
||||
(should (string= "sendMessage" (gethash "method" parsed)))
|
||||
(let ((params (gethash "params" parsed)))
|
||||
(should params)
|
||||
(should (string= "+123" (gethash "recipient" params)))
|
||||
(should (string= "hi" (gethash "message" params))))))
|
||||
|
||||
(ert-deftest noise-rpc--parse-response-success ()
|
||||
"Test parsing a successful JSON-RPC response."
|
||||
(let* ((raw "{\"jsonrpc\":\"2.0\",\"result\":{\"version\":\"0.13.0\"},\"id\":1}")
|
||||
(result (noise-rpc--parse-response raw)))
|
||||
(should result)
|
||||
(should (string= "0.13.0" (gethash "version" result)))))
|
||||
|
||||
(ert-deftest noise-rpc--parse-response-error ()
|
||||
"Test parsing a JSON-RPC error raises noise-rpc-error."
|
||||
(let ((raw "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"method not found\"},\"id\":1}"))
|
||||
(should-error (noise-rpc--parse-response raw) :type 'noise-rpc-error)))
|
||||
|
||||
(ert-deftest noise-rpc--parse-response-malformed ()
|
||||
"Test that malformed JSON raises noise-rpc-error."
|
||||
(should-error (noise-rpc--parse-response "not json") :type 'noise-rpc-error))
|
||||
Reference in New Issue
Block a user