sigil-telegram / tree / docstelegram.md
Telegram
Telegram Bot API client with long-polling, command handlers, MarkdownV2 formatting, and file uploads.
(import (sigil telegram))Two Approaches
There are two ways to use the package depending on your use case:
Fetch-and-respond — Best for scripts, cron jobs, and AI agents that wake up, check for messages, respond, and exit. Use tg-fetch-updates with a tg-client directly:
(import (sigil telegram))
(define client (tg-client token: "123456:ABC-DEF..."))
;; Fetch all pending messages (non-blocking, confirms receipt)
(for-each
(lambda (update)
(let ((msg (tg-update-message update)))
(when (and msg (tg-message-text msg))
(tg-send-message client
(tg-chat-id (tg-message-chat msg))
(string-append "You said: " (tg-escape-markdown (tg-message-text msg)))
parse-mode: "MarkdownV2"))))
(tg-fetch-updates client))Long-running bot — Best for interactive bots that stay online and respond in real time. Use make-tg-bot with handler registration and tg-bot-run:
(import (sigil telegram))
(define bot (make-tg-bot token: "123456:ABC-DEF..."))
(tg-on-command bot "/start"
(lambda (bot msg)
(tg-reply bot msg "Hello! I'm a Sigil bot.")))
(tg-bot-run bot)Both approaches share the same client, types, formatting, and media APIs.
Sending Messages
;; Plain text
(tg-send-message client chat-id "Hello!")
;; With MarkdownV2 formatting
(tg-send-message client chat-id
(string-append (tg-bold "Status") ": " (tg-escape-markdown "All systems go!"))
parse-mode: "MarkdownV2")
;; With reply markup (inline keyboard)
(tg-send-message client chat-id "Choose one:"
reply-markup: (tg-inline-keyboard
(list (list (tg-button "Yes" callback: "yes")
(tg-button "No" callback: "no")))))Message Formatting (MarkdownV2)
Telegram's MarkdownV2 requires escaping 18 special characters outside of formatting entities. Use tg-escape-markdown on all dynamic text before composing messages.
Escaping
;; These characters must be escaped: _ * [ ] ( ) ~ ` > # + - = | { } . !
(tg-escape-markdown "Hello! Price: $5.00 (USD)")
; => "Hello\\! Price: $5\\.00 \\(USD\\)"Formatting Helpers
All helpers wrap text with the appropriate MarkdownV2 syntax. They do NOT auto-escape their input, so escape dynamic content first.
(tg-bold "important") ; => "*important*"
(tg-italic "emphasis") ; => "_emphasis_"
(tg-underline "noted") ; => "__noted__"
(tg-strike "removed") ; => "~removed~"
(tg-code "variable") ; => "`variable`"
(tg-spoiler "hidden") ; => "||hidden||"
(tg-link "Click here" "https://example.com")
; => "[Click here](https://example.com)"
(tg-code-block "(+ 1 2)")
; => "```\n(+ 1 2)```"
(tg-code-block "(+ 1 2)" language: "scheme")
; => "```scheme\n(+ 1 2)```"Composing Formatted Messages
Escape dynamic text with tg-escape-markdown, then wrap with formatting helpers:
(define (format-status-report name status duration)
(string-append
(tg-bold "Build Report") "\n\n"
(tg-bold "Package: ") (tg-escape-markdown name) "\n"
(tg-bold "Status: ") (tg-italic (tg-escape-markdown status)) "\n"
(tg-bold "Duration: ") (tg-code (tg-escape-markdown duration))))
(tg-send-message client chat-id
(format-status-report "sigil-http" "passed" "42s")
parse-mode: "MarkdownV2")Sending Photos and Documents
Three ways to send media:
By URL
Telegram downloads the file from the URL:
(tg-send-photo client chat-id "https://example.com/photo.jpg"
caption: "A nice photo")
(tg-send-document client chat-id "https://example.com/report.pdf"
caption: "Monthly report")By file_id
Reference a file already on Telegram's servers (e.g., from a received message):
(let ((photo-id (dict-ref (array-ref (tg-message-photo msg) 0) file_id:)))
(tg-send-photo client other-chat-id photo-id))By Local File Upload
Upload a file from the local filesystem. MIME type is auto-detected:
(tg-upload-photo client chat-id "/tmp/screenshot.png"
caption: "Build output")
(tg-upload-document client chat-id "/tmp/report.pdf"
caption: "Generated report"
parse-mode: "MarkdownV2")Photo with Formatted Caption
(tg-upload-photo client chat-id "/tmp/chart.png"
caption: (string-append
(tg-bold "Daily Metrics") "\n"
(tg-escape-markdown "Generated at 2024-01-15 09:00"))
parse-mode: "MarkdownV2")Inline Keyboards
Build interactive buttons that trigger callback queries:
;; Create keyboard with callback buttons
(tg-send-message client chat-id "Rate this:"
reply-markup: (tg-inline-keyboard
(list
(list (tg-button "1" callback: "rate_1")
(tg-button "2" callback: "rate_2")
(tg-button "3" callback: "rate_3"))
(list (tg-button "Visit docs" url: "https://example.com")))))
;; Handle button presses
(tg-on-callback bot
(lambda (bot query)
(tg-answer-callback (tg-bot-client bot)
(dict-ref query id:)
text: (string-append "You pressed: " (dict-ref query data:)))))Bot Framework
Creating a Bot
(define bot (make-tg-bot
token: "123456:ABC-DEF..."
allowed-chats: '(12345 67890) ; Optional: restrict to these chat IDs
allowed-users: '(111 222))) ; Optional: restrict to these user IDsBoth allowed-chats: and allowed-users: default to #f (allow all). When set, both must pass (AND logic). Unauthorized messages are silently ignored.
Registering Handlers
All handlers receive (bot msg) as arguments:
;; Command handler - matches /command messages
(tg-on-command bot "/start"
(lambda (bot msg)
(tg-reply bot msg "Welcome!")))
;; Message handler - called for every authorized message
(tg-on-message bot
(lambda (bot msg)
(when (tg-message-text msg)
(display (tg-message-text msg))
(newline))))
;; Callback query handler - inline keyboard presses
(tg-on-callback bot
(lambda (bot query)
(tg-answer-callback (tg-bot-client bot) (dict-ref query id:))))
;; Error handler - called on exceptions during update processing
;; Return 'stop to halt, 'continue to skip backoff, anything else for default 5s backoff
(tg-on-error bot
(lambda (exn)
(display "Error: " (current-error-port))
(display exn (current-error-port))
(newline (current-error-port))))Running the Bot
;; Blocking polling loop (default 30s long-poll timeout)
(tg-bot-run bot)
(tg-bot-run bot timeout: 10)
;; Stop the bot (from a handler or another thread)
(tg-bot-stop bot)
;; Non-blocking single poll (for custom event loops)
(tg-bot-tick bot)Reply Helpers
;; Text reply
(tg-reply bot msg "Got it!")
;; Formatted reply
(tg-reply bot msg (tg-bold "Done") parse-mode: "MarkdownV2")
;; Quote the original message
(tg-reply bot msg "Replying to you" quote?: #t)
;; Reply with photo (URL/file_id)
(tg-reply-photo bot msg "https://example.com/photo.jpg"
caption: "Here you go!")
;; Reply with uploaded local photo
(tg-reply-photo/upload bot msg "/tmp/result.png"
caption: "Processing complete")Low-Level Client
For direct API access without the bot framework:
(define client (tg-client token: "123456:ABC-DEF..."))
;; Call any Telegram Bot API method
(tg-api-call client "getMe")
; => #{ id: 123 is_bot: #t first_name: "MyBot" ... }
;; With parameters
(tg-api-call client "sendMessage"
params: (dict chat_id: 12345 text: "Hello from low-level API"))Message and Update Types
tg-message
(tg-message-message-id msg) ; => 42
(tg-message-chat msg) ; => tg-chat record
(tg-message-from msg) ; => tg-user record or #f
(tg-message-date msg) ; => unix timestamp
(tg-message-text msg) ; => "Hello" or #f
(tg-message-photo msg) ; => array of photo sizes or #f
(tg-message-document msg) ; => dict or #f
(tg-message-reply-to msg) ; => tg-message or #f
(tg-message-raw msg) ; => full API dict (access any field)tg-chat
(tg-chat-id chat) ; => 12345
(tg-chat-type chat) ; => "private", "group", "supergroup", "channel"
(tg-chat-title chat) ; => "My Group" or #f
(tg-chat-username chat) ; => "alice" or #ftg-user
(tg-user-id user) ; => 12345
(tg-user-is-bot user) ; => #t or #f
(tg-user-first-name user) ; => "Alice"
(tg-user-last-name user) ; => "Smith" or #f
(tg-user-username user) ; => "alice" or #fAccessing Raw API Fields
Any Telegram API field not modeled as a struct field is accessible via tg-message-raw:
;; Access sticker data from a message
(dict-ref (tg-message-raw msg) sticker: #f)
;; Access location data
(dict-ref (tg-message-raw msg) location: #f)Common Patterns
Echo Bot
(import (sigil telegram))
(define bot (make-tg-bot token: "123456:ABC-DEF..."))
(tg-on-message bot
(lambda (bot msg)
(when (tg-message-text msg)
(tg-reply bot msg (tg-message-text msg)))))
(tg-bot-run bot)Command Bot
(import (sigil telegram))
(define bot (make-tg-bot token: "123456:ABC-DEF..."))
(tg-on-command bot "/start"
(lambda (bot msg)
(tg-reply bot msg
(string-append
(tg-bold "Welcome\\!") "\n\n"
"Available commands:\n"
"/help \\- Show help\n"
"/status \\- Check status")
parse-mode: "MarkdownV2")))
(tg-on-command bot "/help"
(lambda (bot msg)
(tg-reply bot msg "Send me a message and I'll echo it back.")))
(tg-on-command bot "/status"
(lambda (bot msg)
(tg-reply bot msg
(string-append (tg-bold "Status: ") (tg-code "online"))
parse-mode: "MarkdownV2")))
(tg-bot-run bot)Fetch-and-Respond Script
Process pending messages without a long-running loop. Ideal for cron jobs, AI agents, or any script that runs periodically:
(import (sigil telegram))
(define client (tg-client token: "123456:ABC-DEF..."))
(for-each
(lambda (update)
(let ((msg (tg-update-message update)))
(when msg
(let ((text (tg-message-text msg))
(chat-id (tg-chat-id (tg-message-chat msg))))
(cond
((and text (string=? text "/status"))
(tg-send-message client chat-id
(string-append (tg-bold "Status: ") (tg-code "all systems go"))
parse-mode: "MarkdownV2"))
(text
(tg-send-message client chat-id
(string-append "You said: " (tg-escape-markdown text))
parse-mode: "MarkdownV2")))))))
(tg-fetch-updates client))Notification Sender
Send one-shot messages from scripts or AI agents without checking for updates:
(import (sigil telegram))
(define client (tg-client token: "123456:ABC-DEF..."))
(define chat-id 12345)
;; Send a formatted status report
(tg-send-message client chat-id
(string-append
(tg-bold "Build Complete") "\n\n"
"Package: " (tg-escape-markdown "sigil-http") "\n"
"Status: " (tg-italic "passed") "\n"
"Duration: " (tg-code "42s"))
parse-mode: "MarkdownV2")
;; Upload a screenshot
(tg-upload-photo client chat-id "/tmp/screenshot.png"
caption: "Build output")Photo Bot
(import (sigil telegram))
(define bot (make-tg-bot token: "123456:ABC-DEF..."))
(tg-on-command bot "/photo"
(lambda (bot msg)
(tg-reply-photo bot msg "https://picsum.photos/400/300"
caption: "Random photo")))
(tg-on-command bot "/upload"
(lambda (bot msg)
(tg-reply-photo/upload bot msg "/tmp/generated-chart.png"
caption: (string-append (tg-bold "Chart") "\nGenerated just now")
parse-mode: "MarkdownV2")))
(tg-bot-run bot)