209 lines
8.5 KiB
EmacsLisp
209 lines
8.5 KiB
EmacsLisp
;;; 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--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)))
|
|
(noise-conversation-refresh-for 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
|