;;; noise-receive.el --- Receive messages from signal-cli -*- lexical-binding: t; -*- ;;; Commentary: ;; Fetches new envelopes from signal-cli via SSE or JSON-RPC ;; and stores them in the local database: incoming messages, messages ;; sent from other linked devices (sync), delivery/read receipts, and ;; typing indicators. `noise-receive-start-sse' connects to the SSE ;; endpoint for real-time message reception. ;;; Code: (require 'noise-rpc) (require 'noise-db) (require 'noise-conversation) (require 'noise-sse) (require 'cl-lib) (declare-function noise-chat-list--render "noise-chat-list") ;;;###autoload (defun noise-receive () "Fetch and store new envelopes from signal-cli. Returns the number of new messages stored." (interactive) (noise-db-init) (let ((entries (noise-rpc-call-for-account "receive")) (new 0) (touched nil)) (seq-do (lambda (entry) (when-let ((envelope (noise-receive--field entry "envelope"))) (when-let ((conv-id (noise-receive--process-envelope envelope))) (cl-incf new) (cl-pushnew conv-id touched :test #'equal)))) entries) (when touched (dolist (conv-id touched) (noise-conversation-refresh-for conv-id)) (when-let ((buf (get-buffer "*signal*"))) (with-current-buffer buf (noise-chat-list--render)))) (when (called-interactively-p 'interactive) (message "Received %d new message%s" new (if (= new 1) "" "s"))) new)) (defun noise-receive--announce-message (conv-id) "Show a brief echo-area notification for a new message in CONV-ID." (let* ((conv (noise-db-get-conversation conv-id)) (name (if conv (or (gethash "name" conv) conv-id) conv-id))) (message "[Noise] %s" name))) (defun noise-receive--handle-sse-event (event) "Handle an SSE EVENT from signal-cli. Processes the envelope and updates UI." (noise-db-init) (when-let ((envelope (noise-receive--field event "envelope"))) (when-let ((conv-id (noise-receive--process-envelope envelope))) (let ((buf (noise-conversation--buffer-for conv-id))) (if (and buf (get-buffer-window buf t)) (noise-conversation-refresh-for conv-id) (noise-receive--announce-message conv-id))) (when-let ((buf (get-buffer "*signal*"))) (with-current-buffer buf (noise-chat-list--render)))))) ;; Register the receive handler with the SSE client. (setq noise-sse--event-handler #'noise-receive--handle-sse-event) ;;;###autoload (defun noise-receive-start-sse () "Start receiving messages via SSE." (interactive) (noise-sse-connect)) ;;;###autoload (defun noise-receive-stop-sse () "Stop receiving messages via SSE." (interactive) (noise-sse-disconnect)) ;;; Envelope processing (defun noise-receive--field (obj key) "Get KEY from hash table OBJ, treating JSON null as nil." (when (hash-table-p obj) (let ((v (gethash key obj))) (unless (eq v :null) v)))) (defun noise-receive--process-envelope (envelope) "Process one ENVELOPE from signal-cli. Returns the conversation ID when a new message was stored, else nil." (let* ((sender-id (or (noise-receive--field envelope "sourceNumber") (noise-receive--field envelope "source") (noise-receive--field envelope "sourceUuid"))) (sender-name (or (noise-receive--field envelope "sourceName") sender-id))) (cond ((noise-receive--field envelope "dataMessage") (noise-receive--store-incoming (noise-receive--field envelope "dataMessage") sender-id sender-name)) ((noise-receive--field envelope "syncMessage") (noise-receive--store-sync (noise-receive--field envelope "syncMessage"))) ((noise-receive--field envelope "receiptMessage") (noise-receive--apply-receipt (noise-receive--field envelope "receiptMessage") sender-id) nil) ((noise-receive--field envelope "typingMessage") (noise-receive--apply-typing (noise-receive--field envelope "typingMessage") sender-id sender-name) nil)))) (defun noise-receive--timestamp (ms) "Convert a signal-cli millisecond timestamp MS to float seconds." (/ ms 1000.0)) (defun noise-receive--attachment-name (data-message) "Return the filename of the first attachment in DATA-MESSAGE, or nil." (let ((attachments (noise-receive--field data-message "attachments"))) (when (and attachments (> (length attachments) 0)) (let ((a (elt attachments 0))) (or (noise-receive--field a "filename") (noise-receive--field a "id")))))) (defun noise-receive--store-incoming (dm sender-id sender-name) "Store an incoming data message DM from SENDER-ID / SENDER-NAME. Returns the conversation ID, or nil if there was nothing to store." (let* ((body (or (noise-receive--field dm "message") "")) (group (noise-receive--field dm "groupInfo")) (group-id (and group (noise-receive--field group "groupId"))) (attachment (noise-receive--attachment-name dm)) (conv-id (or group-id sender-id)) (ts (noise-receive--timestamp (noise-receive--field dm "timestamp")))) (when (and conv-id (or (not (string-empty-p body)) attachment)) (when sender-id (noise-db-upsert-contact sender-id :name sender-name)) (unless (noise-db-get-conversation conv-id) (noise-db-upsert-conversation conv-id :type (if group-id "group" "direct") :name (if group-id (or (noise-receive--field group "groupName") "") sender-name) :members (and sender-id (list sender-id)))) (noise-db-insert-message conv-id :timestamp ts :source sender-name :body body :has-attachment (and attachment t) :attachment-path (or attachment "")) ;; Direct chats show the preview without a sender prefix (the chat ;; list only prefixes group senders and "me"). (unless group-id (noise-db-set-last-message-sender conv-id "")) (when (and attachment (string-empty-p body)) (noise-db-upsert-conversation conv-id :last-message-preview (format "photo: %s" attachment))) (let ((buf (noise-conversation--buffer-for conv-id))) (if (and buf (get-buffer-window buf t)) (noise-db-mark-conversation-read conv-id) (noise-db-increment-unread conv-id))) conv-id))) (defun noise-receive--store-sync (sync) "Store a message sent from another linked device, found in SYNC. Returns the conversation ID, or nil." (when-let ((sent (noise-receive--field sync "sentMessage"))) (let* ((body (or (noise-receive--field sent "message") "")) (group (noise-receive--field sent "groupInfo")) (group-id (and group (noise-receive--field group "groupId"))) (dest (or (noise-receive--field sent "destinationNumber") (noise-receive--field sent "destination") (noise-receive--field sent "destinationUuid"))) (conv-id (or group-id dest)) (attachment (noise-receive--attachment-name sent)) (ts (noise-receive--timestamp (noise-receive--field sent "timestamp")))) (when (and conv-id (or (not (string-empty-p body)) attachment)) (unless (noise-db-get-conversation conv-id) (noise-db-upsert-conversation conv-id :type (if group-id "group" "direct") :name (if group-id "" (or dest "")) :members (and dest (not group-id) (list dest)))) (noise-db-insert-message conv-id :timestamp ts :source "me" :body body :receipt-state "sent" :has-attachment (and attachment t) :attachment-path (or attachment "")) conv-id)))) (defun noise-receive--apply-receipt (rm sender-id) "Apply receipt message RM from SENDER-ID to stored messages." (let ((state (cond ((eq (noise-receive--field rm "isRead") t) "read") ((eq (noise-receive--field rm "isDelivery") t) "delivered"))) (timestamps (noise-receive--field rm "timestamps"))) (when (and state timestamps) (seq-do (lambda (ts-ms) (noise-db-update-receipt-state (noise-receive--timestamp ts-ms) state)) timestamps) (when sender-id (noise-conversation-refresh-for sender-id))))) (defun noise-receive--apply-typing (tm sender-id sender-name) "Apply typing message TM from SENDER-ID / SENDER-NAME." (let ((conv-id (or (noise-receive--field tm "groupId") sender-id)) (action (noise-receive--field tm "action"))) (when conv-id (noise-conversation-set-typing conv-id sender-name action)))) (provide 'noise-receive) ;;; noise-receive.el ends here