Files
passepartout/skills/org-skill-gateway-telegram.org

5.8 KiB

SKILL: Telegram Gateway (Universal Literate Note)

Overview

The Telegram Gateway provides bi-directional communication between the Autonomous and the OpenCortex via the Telegram Bot API. It features a non-blocking polling sensor and a high-integrity actuator for outbound messaging.

Phase A: Demand (PRD)

1. Purpose

Enable mobile/remote access to the OpenCortex via a secure Telegram bot.

2. Success Criteria

  • Inbound: Messages from authorized Telegram IDs are injected into the harness Bus.
  • Outbound: The `:telegram` target correctly routes messages to the Bot API.
  • Persistence: The polling offset is maintained to prevent duplicate processing.

Phase B: Blueprint (PROTOCOL)

1. Architectural Intent

The gateway operates as an autonomous background service. It uses `dexador` for HTTP polling and `cl-json` for payload processing. Authentication is enforced via a whitelist of authorized `chat_id`s.

2. Semantic Interfaces

  • `(:sensor :chat-message :channel :telegram …)`
  • `(:type :REQUEST :target :telegram :chat-id "…" :text "…")`

Phase D: Build (Implementation)

Package Context

State: Update Tracking

Tracks the last processed message ID to prevent duplicates.

(defvar *telegram-last-update-id* 0)

State: Polling Thread

Reference to the background thread responsible for message reception.

(defvar *telegram-polling-thread* nil)

State: Authorized Chats

Whitelist of chat IDs permitted to interact with the agent.

(defvar *telegram-authorized-chats* nil 
  "List of chat IDs allowed to interact with the bot. Hydrated from environment.")

Token Retrieval

Fetches the Bot API token from the secure vault.

(defun get-telegram-token () (vault-get-secret :telegram))

Actuator: sendMessage

(defun execute-telegram-action (action context)
  "Sends a message back to Telegram."
  (declare (ignore context))
  (let* ((payload (getf action :payload))
         (chat-id (or (getf payload :chat-id) (getf action :chat-id)))
         (text (or (getf payload :text) (getf action :text)))
         (token (get-telegram-token))
         (url (format nil "https://api.telegram.org/bot~a/sendMessage" token)))
    (when (and token chat-id text)
      (harness-log "TELEGRAM: Sending message to ~a..." chat-id)
      (handler-case 
          (dex:post url 
                    :headers '(("Content-Type" . "application/json"))
                    :content (cl-json:encode-json-to-string 
                              `((chat_id . ,chat-id) (text . ,text))))
        (error (c) (harness-log "TELEGRAM ERROR: ~a" c))))))

Sensor: getUpdates & Injection

(defun telegram-process-updates ()
  "Polls for new messages and injects them into the harness."
  (let* ((token (get-telegram-token))
         (url (format nil "https://api.telegram.org/bot~a/getUpdates?offset=~a" 
                      token (1+ *telegram-last-update-id*))))
    (when token
      (handler-case
          (let* ((response (dex:get url))
                 (json (cl-json:decode-json-from-string response))
                 (updates (cdr (assoc :result json))))
            (dolist (update updates)
              (let* ((update-id (cdr (assoc :update--id update)))
                     (message (cdr (assoc :message update)))
                     (chat (cdr (assoc :chat message)))
                     (chat-id (cdr (assoc :id chat)))
                     (text (cdr (assoc :text message))))
                (setf *telegram-last-update-id* update-id)
                (when (and text chat-id)
                  (harness-log "TELEGRAM: Received message from ~a" chat-id)
                  (inject-stimulus 
                   (list :type :EVENT 
                         :payload (list :sensor :chat-message 
                                        :channel :telegram 
                                        :chat-id (format nil "~a" chat-id)
                                        :text text)))))))
        (error (c) (harness-log "TELEGRAM POLL ERROR: ~a" c))))))

Start Polling

Initializes the Telegram background thread.

(defun start-telegram-gateway ()
  "Initializes the Telegram background thread."
  (unless (and *telegram-polling-thread* (bt:thread-alive-p *telegram-polling-thread*))
    (setf *telegram-polling-thread*
          (bt:make-thread 
           (lambda ()
             (loop
               (telegram-process-updates)
               (sleep 3)))
           :name "opencortex-telegram-gateway"))
    (harness-log "TELEGRAM: Gateway polling active.")))

Stop Polling

Gracefully terminates the background thread.

(defun stop-telegram-gateway ()
  (when (and *telegram-polling-thread* (bt:thread-alive-p *telegram-polling-thread*))
    (bt:destroy-thread *telegram-polling-thread*)
    (setf *telegram-polling-thread* nil)))

Registration: Actuator

Register the Telegram channel as a physical actuator.

(register-actuator :telegram #'execute-telegram-action)

Registration: Skill

Define the passive skill entry for the gateway.

(defskill :skill-gateway-telegram
  :priority 150
  :trigger (lambda (ctx) (declare (ignore ctx)) nil) ;; Passive, handles its own loop
  :probabilistic nil
  :deterministic (lambda (action ctx) (declare (ignore ctx)) action))

Initialization

Trigger the polling loop upon loading.

(start-telegram-gateway)