Files
passepartout/org/channel-discord.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

4.3 KiB

Channel Discord (channel-discord.org)

Channel Discord

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

Overview

The Discord channel provides bidirectional communication via the Discord REST API and Gateway WebSocket. Messages received from Discord channels are injected into the cognitive pipeline as :user-input signals with :source :discord. Outbound messages route through the actuator registry when the pipeline targets :discord.

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

Contract

  1. (discord-get-token): returns the Discord bot token from the vault (via vault-get-secret :discord), or nil if not configured.
  2. (discord-poll): polls configured channels via GET /channels/{id}/messages, injects each non-bot message as a :user-input stimulus with :source :discord. Handles JSON parse failures and API errors gracefully. HITL commands are intercepted before injection.
  3. (discord-send action context): sends a message via POST /channels/{id}/messages. Extracts :channel-id and :text from the action plist. Uses bot token authentication. Logs send failures without crashing the pipeline.

Implementation

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

(defun discord-send (action context)
  "Sends a message via Discord REST API."
  (declare (ignore context))
  (let* ((payload (getf action :payload))
         (meta (getf action :meta))
         (channel-id (or (getf meta :channel-id) (getf payload :chat-id)))
         (text (or (getf payload :text) (getf action :text)))
         (token (discord-get-token)))
    (when (and token channel-id text)
      (handler-case
          (dex:post (format nil "https://discord.com/api/v10/channels/~a/messages" channel-id)
                    :headers '(("Authorization" . ,(format nil "Bot ~a" token))
                               ("Content-Type" . "application/json"))
                    :content (cl-json:encode-json-to-string
                              `((content . ,text))))
        (error (c) (log-message "DISCORD ERROR: ~a" c))))))

(defun discord-poll ()
  "Polls Discord via HTTP GET /channels/{id}/messages. In production,
a WebSocket connection to the Gateway is preferred for real-time events."
  (let* ((token (discord-get-token)))
    (when token
      (handler-case
          (dolist (channel '("channel-id-here"))  ;; configured channel IDs
            (let* ((last-id (getf (gethash "discord" *gateway-configs*) :last-update-id 0))
                   (url (format nil "https://discord.com/api/v10/channels/~a/messages?after=~a"
                               channel last-id))
                   (response (dex:get url :headers
                                       `(("Authorization" . ,(format nil "Bot ~a" token))))))
              (let ((messages (ignore-errors
                               (cdr (assoc :message
                                      (cl-json:decode-json-from-string response))))))
                (dolist (msg (and (listp messages) messages))
                  (let* ((id (cdr (assoc :id msg)))
                         (content (cdr (assoc :content msg)))
                         (author (cdr (assoc :author msg)))
                         (author-id (cdr (assoc :id author)))
                         (is-bot (cdr (assoc :bot author))))
                    (when (and id content (not is-bot))
                      (setf (getf (gethash "discord" *gateway-configs*) :last-update-id) id)
                      (unless (ignore-errors (hitl-handle-message content :discord))
                        (stimulus-inject
                         (list :type :EVENT
                               :meta (list :source :discord :chat-id channel)
                               :payload (list :sensor :user-input :text content))))))))))
        (error (c) (log-message "DISCORD POLL ERROR: ~a" c))))))

#+end_src