:PROPERTIES: :ID: gateway-telegram-skill :CREATED: [2026-04-11 Sat 15:50] :END: #+TITLE: SKILL: Telegram Gateway (Universal Literate Note) #+STARTUP: content #+FILETAGS: :gateway:telegram:io:autonomy: #+DEPENDS_ON: id:credentials-vault-skill * 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) :PROPERTIES: :STATUS: SIGNED :END: ** 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) :PROPERTIES: :STATUS: SIGNED :END: ** 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 - `(:type :EVENT :meta (:source :telegram :chat-id "...") :payload (:sensor :user-input :text "..."))` - `(:type :REQUEST :target :telegram :payload (:action :message :text "..."))` * Phase D: Build (Implementation) ** Package Context #+begin_src lisp #+end_src ** State: Update Tracking Tracks the last processed message ID to prevent duplicates. #+begin_src lisp (defvar *telegram-last-update-id* 0) #+end_src ** State: Polling Thread Reference to the background thread responsible for message reception. #+begin_src lisp (defvar *telegram-polling-thread* nil) #+end_src ** State: Authorized Chats Whitelist of chat IDs permitted to interact with the agent. #+begin_src lisp (defvar *telegram-authorized-chats* nil "List of chat IDs allowed to interact with the bot. Hydrated from environment.") #+end_src ** Token Retrieval Fetches the Bot API token from the secure vault. #+begin_src lisp (defun get-telegram-token () (vault-get-secret :telegram)) #+end_src ** Actuator: sendMessage #+begin_src lisp (defun execute-telegram-action (action context) "Sends a message back to Telegram." (declare (ignore context)) (let* ((payload (getf action :payload)) (meta (getf action :meta)) (chat-id (or (getf meta :chat-id) (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)))))) #+end_src ** Sensor: getUpdates & Injection #+begin_src lisp (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 :meta (list :source :telegram :chat-id (format nil "~a" chat-id)) :payload (list :sensor :user-input :text text))))))) (error (c) (harness-log "TELEGRAM POLL ERROR: ~a" c)))))) #+end_src ** Start Polling Initializes the Telegram background thread. #+begin_src lisp (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."))) #+end_src ** Stop Polling Gracefully terminates the background thread. #+begin_src lisp (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))) #+end_src ** Registration: Actuator Register the Telegram channel as a physical actuator. #+begin_src lisp (register-actuator :telegram #'execute-telegram-action) #+end_src ** Registration: Skill Define the passive skill entry for the gateway. #+begin_src lisp (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)) #+end_src ** Initialization Trigger the polling loop upon loading. #+begin_src lisp (start-telegram-gateway) #+end_src