Files
passepartout/org/channel-slack.org
Amr Gharbeia b9a4318ef8 reorg: tangle to XDG, remove stale lisp files, fix tui input
- Changed all 50 org file :tangle targets from ../lisp/ to
  ~/.local/share/passepartout/lisp/ (XDG data dir)
- Removed 49 generated .lisp files from project lisp/ directory
- Removed tests/system-integration-tests.lisp (generated)
- Removed lisp/*.fasl (compiled, stale)
- Updated core-manifest.org to tangle .asd to XDG root
- Remapped quicklisp symlink: local-projects/passepartout → XDG

TUI fixes in channel-tui-main.org:
- Removed with-raw-terminal (stty raw breaks fd 0 reads in this SBCL)
- Use cat subprocess + pipe for keyboard input (via :input :interactive)
- Blocking read-char on pipe with with-timeout 0.1s for daemon processing
- Key events queued via drain-queue alongside daemon messages
- Full dialog key routing (Escape, Up/Down, Enter, filters, Backspace)
- SIGWINCH resize handling
- Post-handshake backend-size re-query
- Daemon version in status bar (was v0.5.0 hardcoded)
- Handshake version stored in state, no add-msg
- :daemon-version and :size-queried in state plist
- view-status uses draw-rect for background
- Test section gated with #+passepartout-tests
2026-05-14 12:34:06 -04:00

3.8 KiB

Channel Slack (channel-slack.org)

Channel Slack

Extracted from gateway-messaging in v0.5.0. Isolated platform — Slack-specific poll and send logic.

Overview

The Slack channel provides bidirectional communication via the Slack Web API (chat.postMessage for outbound, conversations.history for inbound polling). Messages from Slack channels are injected into the cognitive pipeline as :user-input signals with :source :slack. Outbound messages route through the actuator registry when the pipeline targets :slack.

The channel uses two functions: slack-poll (inbound sensor) and slack-send (outbound actuator). Both retrieve the bot token from the credentials vault. HITL commands are intercepted before injection so approval flows work identically across all channels.

Contract

  1. (slack-get-token): returns the Slack bot token from the vault (via vault-get-secret :slack), or nil if not configured.
  2. (slack-poll): polls configured channels via conversations.history, injects each non-bot message as a :user-input stimulus with :source :slack. Handles API errors gracefully. HITL commands are intercepted before injection.
  3. (slack-send action context): sends a message via chat.postMessage. Extracts :channel-id and :text from the action plist. Uses Bearer token authentication. Logs send failures without crashing the pipeline.

Implementation

(in-package :passepartout)
(defun slack-get-token ()
  (vault-get-secret :slack))

(defun slack-send (action context)
  "Sends a message via Slack Web API."
  (declare (ignore context))
  (let* ((payload (getf action :payload))
         (meta (getf action :meta))
         (channel (or (getf meta :channel-id) (getf payload :chat-id)))
         (text (or (getf payload :text) (getf action :text)))
         (token (slack-get-token)))
    (when (and token channel text)
      (handler-case
          (dex:post "https://slack.com/api/chat.postMessage"
                    :headers `(("Authorization" . ,(format nil "Bearer ~a" token))
                               ("Content-Type" . "application/json; charset=utf-8"))
                    :content (cl-json:encode-json-to-string
                              `((channel . ,channel) (text . ,text))))
        (error (c) (log-message "SLACK ERROR: ~a" c))))))

(defun slack-poll ()
  "Polls Slack for new messages via conversations.history."
  (let* ((token (slack-get-token)))
    (when token
      (dolist (channel '("general"))  ;; configured channel IDs
        (handler-case
            (let* ((url (format nil "https://slack.com/api/conversations.history?channel=~a&limit=5" channel))
                   (response (dex:get url :headers
                                       `(("Authorization" . ,(format nil "Bearer ~a" token))))))
              (let* ((json (ignore-errors (cl-json:decode-json-from-string response)))
                     (ok (cdr (assoc :ok json)))
                     (messages (cdr (assoc :messages json))))
                (when (and ok messages (listp messages))
                  (dolist (msg messages)
                    (let* ((text (cdr (assoc :text msg)))
                           (user (cdr (assoc :user msg)))
                           (ts (cdr (assoc :ts msg))))
                      (when (and text user (not (string= user "USLACKBOT")))
                        (unless (ignore-errors (hitl-handle-message text :slack))
                          (stimulus-inject
                           (list :type :EVENT
                                 :meta (list :source :slack :chat-id channel)
                                 :payload (list :sensor :user-input :text text))))))))))
          (error (c) (log-message "SLACK POLL ERROR: ~a" c)))))))

#+end_src