AtlatestRepositorysigil-telegram
1# Telegram
2
3> Telegram Bot API client with long-polling, command handlers, MarkdownV2 formatting, and file uploads.
4
5```scheme
6(import (sigil telegram))
7```
8
9## Two Approaches
11There are two ways to use the package depending on your use case:
13**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:
15```scheme
16(import (sigil telegram))
18(define client (tg-client token: "123456:ABC-DEF..."))
20;; Fetch all pending messages (non-blocking, confirms receipt)
21(for-each
22 (lambda (update)
23 (let ((msg (tg-update-message update)))
24 (when (and msg (tg-message-text msg))
25 (tg-send-message client
26 (tg-chat-id (tg-message-chat msg))
27 (string-append "You said: " (tg-escape-markdown (tg-message-text msg)))
28 parse-mode: "MarkdownV2"))))
29 (tg-fetch-updates client))
30```
32**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`:
34```scheme
35(import (sigil telegram))
37(define bot (make-tg-bot token: "123456:ABC-DEF..."))
39(tg-on-command bot "/start"
40 (lambda (bot msg)
41 (tg-reply bot msg "Hello! I'm a Sigil bot.")))
43(tg-bot-run bot)
44```
46Both approaches share the same client, types, formatting, and media APIs.
48## Sending Messages
50```scheme
51;; Plain text
52(tg-send-message client chat-id "Hello!")
54;; With MarkdownV2 formatting
55(tg-send-message client chat-id
56 (string-append (tg-bold "Status") ": " (tg-escape-markdown "All systems go!"))
57 parse-mode: "MarkdownV2")
59;; With reply markup (inline keyboard)
60(tg-send-message client chat-id "Choose one:"
61 reply-markup: (tg-inline-keyboard
62 (list (list (tg-button "Yes" callback: "yes")
63 (tg-button "No" callback: "no")))))
64```
66## Message Formatting (MarkdownV2)
68Telegram's MarkdownV2 requires escaping 18 special characters outside of formatting entities. Use `tg-escape-markdown` on all dynamic text before composing messages.
70### Escaping
72```scheme
73;; These characters must be escaped: _ * [ ] ( ) ~ ` > # + - = | { } . !
74(tg-escape-markdown "Hello! Price: $5.00 (USD)")
75; => "Hello\\! Price: $5\\.00 \\(USD\\)"
76```
78### Formatting Helpers
80All helpers wrap text with the appropriate MarkdownV2 syntax. They do NOT auto-escape their input, so escape dynamic content first.
82```scheme
83(tg-bold "important") ; => "*important*"
84(tg-italic "emphasis") ; => "_emphasis_"
85(tg-underline "noted") ; => "__noted__"
86(tg-strike "removed") ; => "~removed~"
87(tg-code "variable") ; => "`variable`"
88(tg-spoiler "hidden") ; => "||hidden||"
90(tg-link "Click here" "https://example.com")
91; => "[Click here](https://example.com)"
93(tg-code-block "(+ 1 2)")
94; => "```\n(+ 1 2)```"
96(tg-code-block "(+ 1 2)" language: "scheme")
97; => "```scheme\n(+ 1 2)```"
98```
100### Composing Formatted Messages
102Escape dynamic text with `tg-escape-markdown`, then wrap with formatting helpers:
104```scheme
105(define (format-status-report name status duration)
106 (string-append
107 (tg-bold "Build Report") "\n\n"
108 (tg-bold "Package: ") (tg-escape-markdown name) "\n"
109 (tg-bold "Status: ") (tg-italic (tg-escape-markdown status)) "\n"
110 (tg-bold "Duration: ") (tg-code (tg-escape-markdown duration))))
112(tg-send-message client chat-id
113 (format-status-report "sigil-http" "passed" "42s")
114 parse-mode: "MarkdownV2")
115```
117## Sending Photos and Documents
119Three ways to send media:
121### By URL
123Telegram downloads the file from the URL:
125```scheme
126(tg-send-photo client chat-id "https://example.com/photo.jpg"
127 caption: "A nice photo")
129(tg-send-document client chat-id "https://example.com/report.pdf"
130 caption: "Monthly report")
131```
133### By file_id
135Reference a file already on Telegram's servers (e.g., from a received message):
137```scheme
138(let ((photo-id (dict-ref (array-ref (tg-message-photo msg) 0) file_id:)))
139 (tg-send-photo client other-chat-id photo-id))
140```
142### By Local File Upload
144Upload a file from the local filesystem. MIME type is auto-detected:
146```scheme
147(tg-upload-photo client chat-id "/tmp/screenshot.png"
148 caption: "Build output")
150(tg-upload-document client chat-id "/tmp/report.pdf"
151 caption: "Generated report"
152 parse-mode: "MarkdownV2")
153```
155### Photo with Formatted Caption
157```scheme
158(tg-upload-photo client chat-id "/tmp/chart.png"
159 caption: (string-append
160 (tg-bold "Daily Metrics") "\n"
161 (tg-escape-markdown "Generated at 2024-01-15 09:00"))
162 parse-mode: "MarkdownV2")
163```
165## Inline Keyboards
167Build interactive buttons that trigger callback queries:
169```scheme
170;; Create keyboard with callback buttons
171(tg-send-message client chat-id "Rate this:"
172 reply-markup: (tg-inline-keyboard
173 (list
174 (list (tg-button "1" callback: "rate_1")
175 (tg-button "2" callback: "rate_2")
176 (tg-button "3" callback: "rate_3"))
177 (list (tg-button "Visit docs" url: "https://example.com")))))
179;; Handle button presses
180(tg-on-callback bot
181 (lambda (bot query)
182 (tg-answer-callback (tg-bot-client bot)
183 (dict-ref query id:)
184 text: (string-append "You pressed: " (dict-ref query data:)))))
185```
187## Bot Framework
189### Creating a Bot
191```scheme
192(define bot (make-tg-bot
193 token: "123456:ABC-DEF..."
194 allowed-chats: '(12345 67890) ; Optional: restrict to these chat IDs
195 allowed-users: '(111 222))) ; Optional: restrict to these user IDs
196```
198Both `allowed-chats:` and `allowed-users:` default to `#f` (allow all). When set, both must pass (AND logic). Unauthorized messages are silently ignored.
200### Registering Handlers
202All handlers receive `(bot msg)` as arguments:
204```scheme
205;; Command handler - matches /command messages
206(tg-on-command bot "/start"
207 (lambda (bot msg)
208 (tg-reply bot msg "Welcome!")))
210;; Message handler - called for every authorized message
211(tg-on-message bot
212 (lambda (bot msg)
213 (when (tg-message-text msg)
214 (display (tg-message-text msg))
215 (newline))))
217;; Callback query handler - inline keyboard presses
218(tg-on-callback bot
219 (lambda (bot query)
220 (tg-answer-callback (tg-bot-client bot) (dict-ref query id:))))
222;; Error handler - called on exceptions during update processing
223;; Return 'stop to halt, 'continue to skip backoff, anything else for default 5s backoff
224(tg-on-error bot
225 (lambda (exn)
226 (display "Error: " (current-error-port))
227 (display exn (current-error-port))
228 (newline (current-error-port))))
229```
231### Running the Bot
233```scheme
234;; Blocking polling loop (default 30s long-poll timeout)
235(tg-bot-run bot)
236(tg-bot-run bot timeout: 10)
238;; Stop the bot (from a handler or another thread)
239(tg-bot-stop bot)
241;; Non-blocking single poll (for custom event loops)
242(tg-bot-tick bot)
243```
245## Reply Helpers
247```scheme
248;; Text reply
249(tg-reply bot msg "Got it!")
251;; Formatted reply
252(tg-reply bot msg (tg-bold "Done") parse-mode: "MarkdownV2")
254;; Quote the original message
255(tg-reply bot msg "Replying to you" quote?: #t)
257;; Reply with photo (URL/file_id)
258(tg-reply-photo bot msg "https://example.com/photo.jpg"
259 caption: "Here you go!")
261;; Reply with uploaded local photo
262(tg-reply-photo/upload bot msg "/tmp/result.png"
263 caption: "Processing complete")
264```
266## Low-Level Client
268For direct API access without the bot framework:
270```scheme
271(define client (tg-client token: "123456:ABC-DEF..."))
273;; Call any Telegram Bot API method
274(tg-api-call client "getMe")
275; => #{ id: 123 is_bot: #t first_name: "MyBot" ... }
277;; With parameters
278(tg-api-call client "sendMessage"
279 params: (dict chat_id: 12345 text: "Hello from low-level API"))
280```
282## Message and Update Types
284### tg-message
286```scheme
287(tg-message-message-id msg) ; => 42
288(tg-message-chat msg) ; => tg-chat record
289(tg-message-from msg) ; => tg-user record or #f
290(tg-message-date msg) ; => unix timestamp
291(tg-message-text msg) ; => "Hello" or #f
292(tg-message-photo msg) ; => array of photo sizes or #f
293(tg-message-document msg) ; => dict or #f
294(tg-message-reply-to msg) ; => tg-message or #f
295(tg-message-raw msg) ; => full API dict (access any field)
296```
298### tg-chat
300```scheme
301(tg-chat-id chat) ; => 12345
302(tg-chat-type chat) ; => "private", "group", "supergroup", "channel"
303(tg-chat-title chat) ; => "My Group" or #f
304(tg-chat-username chat) ; => "alice" or #f
305```
307### tg-user
309```scheme
310(tg-user-id user) ; => 12345
311(tg-user-is-bot user) ; => #t or #f
312(tg-user-first-name user) ; => "Alice"
313(tg-user-last-name user) ; => "Smith" or #f
314(tg-user-username user) ; => "alice" or #f
315```
317### Accessing Raw API Fields
319Any Telegram API field not modeled as a struct field is accessible via `tg-message-raw`:
321```scheme
322;; Access sticker data from a message
323(dict-ref (tg-message-raw msg) sticker: #f)
325;; Access location data
326(dict-ref (tg-message-raw msg) location: #f)
327```
329## Common Patterns
331### Echo Bot
333```scheme
334(import (sigil telegram))
336(define bot (make-tg-bot token: "123456:ABC-DEF..."))
338(tg-on-message bot
339 (lambda (bot msg)
340 (when (tg-message-text msg)
341 (tg-reply bot msg (tg-message-text msg)))))
343(tg-bot-run bot)
344```
346### Command Bot
348```scheme
349(import (sigil telegram))
351(define bot (make-tg-bot token: "123456:ABC-DEF..."))
353(tg-on-command bot "/start"
354 (lambda (bot msg)
355 (tg-reply bot msg
356 (string-append
357 (tg-bold "Welcome\\!") "\n\n"
358 "Available commands:\n"
359 "/help \\- Show help\n"
360 "/status \\- Check status")
361 parse-mode: "MarkdownV2")))
363(tg-on-command bot "/help"
364 (lambda (bot msg)
365 (tg-reply bot msg "Send me a message and I'll echo it back.")))
367(tg-on-command bot "/status"
368 (lambda (bot msg)
369 (tg-reply bot msg
370 (string-append (tg-bold "Status: ") (tg-code "online"))
371 parse-mode: "MarkdownV2")))
373(tg-bot-run bot)
374```
376### Fetch-and-Respond Script
378Process pending messages without a long-running loop. Ideal for cron jobs, AI agents, or any script that runs periodically:
380```scheme
381(import (sigil telegram))
383(define client (tg-client token: "123456:ABC-DEF..."))
385(for-each
386 (lambda (update)
387 (let ((msg (tg-update-message update)))
388 (when msg
389 (let ((text (tg-message-text msg))
390 (chat-id (tg-chat-id (tg-message-chat msg))))
391 (cond
392 ((and text (string=? text "/status"))
393 (tg-send-message client chat-id
394 (string-append (tg-bold "Status: ") (tg-code "all systems go"))
395 parse-mode: "MarkdownV2"))
396 (text
397 (tg-send-message client chat-id
398 (string-append "You said: " (tg-escape-markdown text))
399 parse-mode: "MarkdownV2")))))))
400 (tg-fetch-updates client))
401```
403### Notification Sender
405Send one-shot messages from scripts or AI agents without checking for updates:
407```scheme
408(import (sigil telegram))
410(define client (tg-client token: "123456:ABC-DEF..."))
411(define chat-id 12345)
413;; Send a formatted status report
414(tg-send-message client chat-id
415 (string-append
416 (tg-bold "Build Complete") "\n\n"
417 "Package: " (tg-escape-markdown "sigil-http") "\n"
418 "Status: " (tg-italic "passed") "\n"
419 "Duration: " (tg-code "42s"))
420 parse-mode: "MarkdownV2")
422;; Upload a screenshot
423(tg-upload-photo client chat-id "/tmp/screenshot.png"
424 caption: "Build output")
425```
427### Photo Bot
429```scheme
430(import (sigil telegram))
432(define bot (make-tg-bot token: "123456:ABC-DEF..."))
434(tg-on-command bot "/photo"
435 (lambda (bot msg)
436 (tg-reply-photo bot msg "https://picsum.photos/400/300"
437 caption: "Random photo")))
439(tg-on-command bot "/upload"
440 (lambda (bot msg)
441 (tg-reply-photo/upload bot msg "/tmp/generated-chart.png"
442 caption: (string-append (tg-bold "Chart") "\nGenerated just now")
443 parse-mode: "MarkdownV2")))
445(tg-bot-run bot)
446```