;;; 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