Files
noise/noise-sse.el
T

141 lines
4.8 KiB
EmacsLisp

;;; noise-sse.el --- SSE client for signal-cli -*- lexical-binding: t; -*-
;;; Commentary:
;; Implements Server-Sent Events client for receiving real-time messages
;; from signal-cli daemon's /api/v1/events endpoint.
;;; Code:
(require 'url)
(require 'json)
(require 'cl-lib)
(defvar noise-sse--process nil
"Active SSE network process, or nil.")
(defvar noise-sse--reconnect-timer nil
"Timer for reconnection attempts, or nil.")
(defvar noise-sse--reconnect-delay 1
"Current delay in seconds before next reconnection attempt.")
(defcustom noise-sse-max-reconnect-delay 60
"Maximum delay in seconds between reconnection attempts."
:type 'integer
:group 'noise)
(defun noise-sse--increase-reconnect-delay ()
"Increase reconnect delay with exponential backoff, capped at max."
(setq noise-sse--reconnect-delay
(min (* noise-sse--reconnect-delay 2)
noise-sse-max-reconnect-delay)))
(defun noise-sse--reset-reconnect-delay ()
"Reset reconnect delay to initial value after successful connection."
(setq noise-sse--reconnect-delay 1))
(defun noise-sse--parse-event (event-string)
"Parse an SSE EVENT-STRING into a hash table.
Returns nil for empty or invalid events."
(when (and event-string (not (string-empty-p event-string)))
(let ((lines (split-string event-string "\n"))
(data-lines '()))
(dolist (line lines)
(when (string-prefix-p "data: " line)
(push (substring line 6) data-lines)))
(when data-lines
(let ((json-str (mapconcat #'identity (nreverse data-lines) "")))
(condition-case nil
(json-parse-string json-str)
(error nil)))))))
(defvar noise-sse--buffer ""
"Buffer for partial SSE event data.")
(defun noise-sse--filter (proc string)
"Process filter for SSE connection.
Buffers incoming STRING and processes complete events."
(setq noise-sse--buffer (concat noise-sse--buffer string))
(while (string-match "\n\n" noise-sse--buffer)
(let ((event-end (match-beginning 0))
(event-start 0))
(let ((event-str (substring noise-sse--buffer event-start event-end)))
(setq noise-sse--buffer
(substring noise-sse--buffer (+ event-end 2)))
(when-let ((event (noise-sse--parse-event event-str)))
(noise-sse--handle-event event))))))
(defun noise-sse--handle-event (event)
"Handle a parsed SSE EVENT.
This function will be overridden in noise-receive to process envelopes."
(message "SSE event received: %s" event))
(defvar noise-signal-cli-address)
(defvar noise-account)
(defun noise-sse--build-url ()
"Build the SSE endpoint URL for signal-cli."
(let ((base (concat noise-signal-cli-address "/api/v1/events")))
(if (and (boundp 'noise-account) noise-account)
(concat base "?account=" (url-hexify-string noise-account))
base)))
(defun noise-sse--sentinel (proc status)
"Process sentinel for SSE connection.
Handles connection close and triggers reconnection."
(when (or (equal status "finished\n")
(string-match-p "failed" status)
(string-match-p "deleted" status))
(message "Noise: SSE connection lost, reconnecting in %ds..."
noise-sse--reconnect-delay)
(setq noise-sse--process nil)
(noise-sse--schedule-reconnect)))
(defun noise-sse--schedule-reconnect ()
"Schedule a reconnection attempt after the current delay."
(when noise-sse--reconnect-timer
(cancel-timer noise-sse--reconnect-timer))
(setq noise-sse--reconnect-timer
(run-with-timer noise-sse--reconnect-delay nil
#'noise-sse-connect)))
;;;###autoload
(defun noise-sse-connect ()
"Connect to signal-cli SSE endpoint."
(interactive)
(noise-sse-disconnect)
(let* ((url (noise-sse--build-url))
(parsed (url-generic-parse-url url))
(host (url-host parsed))
(port (or (url-port parsed) 80))
(proc (make-network-process
:name "noise-sse"
:host host
:service port
:filter #'noise-sse--filter
:sentinel #'noise-sse--sentinel)))
(setq noise-sse--process proc)
(noise-sse--reset-reconnect-delay)
(process-send-string proc
(format "GET %s HTTP/1.1\r\nHost: %s\r\nAccept: text/event-stream\r\n\r\n"
(url-filename parsed)
host))
(message "Noise: SSE connected to %s" url)))
;;;###autoload
(defun noise-sse-disconnect ()
"Disconnect from signal-cli SSE endpoint."
(interactive)
(when noise-sse--process
(delete-process noise-sse--process)
(setq noise-sse--process nil))
(when noise-sse--reconnect-timer
(cancel-timer noise-sse--reconnect-timer)
(setq noise-sse--reconnect-timer nil))
(setq noise-sse--buffer "")
(message "Noise: SSE disconnected"))
(provide 'noise-sse)
;;; noise-sse.el ends here