AtlatestRepositorysigil-telegram
sigil-telegram / tree / docstelegram.md
1
# Telegram3
> Telegram Bot API client with long-polling, command handlers, MarkdownV2 formatting, and file uploads.5
```scheme6
(import (sigil telegram))7
```9
## Two Approaches11
There 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
```scheme16
(import (sigil telegram))18
(define client (tg-client token: "123456:ABC-DEF..."))20
;; Fetch all pending messages (non-blocking, confirms receipt)21
(for-each22
(lambda (update)23
(let ((msg (tg-update-message update)))24
(when (and msg (tg-message-text msg))25
(tg-send-message client26
(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
```scheme35
(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
```46
Both approaches share the same client, types, formatting, and media APIs.48
## Sending Messages50
```scheme51
;; Plain text52
(tg-send-message client chat-id "Hello!")54
;; With MarkdownV2 formatting55
(tg-send-message client chat-id56
(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-keyboard62
(list (list (tg-button "Yes" callback: "yes")63
(tg-button "No" callback: "no")))))64
```66
## Message Formatting (MarkdownV2)68
Telegram's MarkdownV2 requires escaping 18 special characters outside of formatting entities. Use `tg-escape-markdown` on all dynamic text before composing messages.70
### Escaping72
```scheme73
;; These characters must be escaped: _ * [ ] ( ) ~ ` > # + - = | { } . !74
(tg-escape-markdown "Hello! Price: $5.00 (USD)")75
; => "Hello\\! Price: $5\\.00 \\(USD\\)"76
```78
### Formatting Helpers80
All helpers wrap text with the appropriate MarkdownV2 syntax. They do NOT auto-escape their input, so escape dynamic content first.82
```scheme83
(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 Messages102
Escape dynamic text with `tg-escape-markdown`, then wrap with formatting helpers:104
```scheme105
(define (format-status-report name status duration)106
(string-append107
(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-id113
(format-status-report "sigil-http" "passed" "42s")114
parse-mode: "MarkdownV2")115
```117
## Sending Photos and Documents119
Three ways to send media:121
### By URL123
Telegram downloads the file from the URL:125
```scheme126
(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_id135
Reference a file already on Telegram's servers (e.g., from a received message):137
```scheme138
(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 Upload144
Upload a file from the local filesystem. MIME type is auto-detected:146
```scheme147
(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 Caption157
```scheme158
(tg-upload-photo client chat-id "/tmp/chart.png"159
caption: (string-append160
(tg-bold "Daily Metrics") "\n"161
(tg-escape-markdown "Generated at 2024-01-15 09:00"))162
parse-mode: "MarkdownV2")163
```165
## Inline Keyboards167
Build interactive buttons that trigger callback queries:169
```scheme170
;; Create keyboard with callback buttons171
(tg-send-message client chat-id "Rate this:"172
reply-markup: (tg-inline-keyboard173
(list174
(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 presses180
(tg-on-callback bot181
(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 Framework189
### Creating a Bot191
```scheme192
(define bot (make-tg-bot193
token: "123456:ABC-DEF..."194
allowed-chats: '(12345 67890) ; Optional: restrict to these chat IDs195
allowed-users: '(111 222))) ; Optional: restrict to these user IDs196
```198
Both `allowed-chats:` and `allowed-users:` default to `#f` (allow all). When set, both must pass (AND logic). Unauthorized messages are silently ignored.200
### Registering Handlers202
All handlers receive `(bot msg)` as arguments:204
```scheme205
;; Command handler - matches /command messages206
(tg-on-command bot "/start"207
(lambda (bot msg)208
(tg-reply bot msg "Welcome!")))210
;; Message handler - called for every authorized message211
(tg-on-message bot212
(lambda (bot msg)213
(when (tg-message-text msg)214
(display (tg-message-text msg))215
(newline))))217
;; Callback query handler - inline keyboard presses218
(tg-on-callback bot219
(lambda (bot query)220
(tg-answer-callback (tg-bot-client bot) (dict-ref query id:))))222
;; Error handler - called on exceptions during update processing223
;; Return 'stop to halt, 'continue to skip backoff, anything else for default 5s backoff224
(tg-on-error bot225
(lambda (exn)226
(display "Error: " (current-error-port))227
(display exn (current-error-port))228
(newline (current-error-port))))229
```231
### Running the Bot233
```scheme234
;; 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 Helpers247
```scheme248
;; Text reply249
(tg-reply bot msg "Got it!")251
;; Formatted reply252
(tg-reply bot msg (tg-bold "Done") parse-mode: "MarkdownV2")254
;; Quote the original message255
(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 photo262
(tg-reply-photo/upload bot msg "/tmp/result.png"263
caption: "Processing complete")264
```266
## Low-Level Client268
For direct API access without the bot framework:270
```scheme271
(define client (tg-client token: "123456:ABC-DEF..."))273
;; Call any Telegram Bot API method274
(tg-api-call client "getMe")275
; => #{ id: 123 is_bot: #t first_name: "MyBot" ... }277
;; With parameters278
(tg-api-call client "sendMessage"279
params: (dict chat_id: 12345 text: "Hello from low-level API"))280
```282
## Message and Update Types284
### tg-message286
```scheme287
(tg-message-message-id msg) ; => 42288
(tg-message-chat msg) ; => tg-chat record289
(tg-message-from msg) ; => tg-user record or #f290
(tg-message-date msg) ; => unix timestamp291
(tg-message-text msg) ; => "Hello" or #f292
(tg-message-photo msg) ; => array of photo sizes or #f293
(tg-message-document msg) ; => dict or #f294
(tg-message-reply-to msg) ; => tg-message or #f295
(tg-message-raw msg) ; => full API dict (access any field)296
```298
### tg-chat300
```scheme301
(tg-chat-id chat) ; => 12345302
(tg-chat-type chat) ; => "private", "group", "supergroup", "channel"303
(tg-chat-title chat) ; => "My Group" or #f304
(tg-chat-username chat) ; => "alice" or #f305
```307
### tg-user309
```scheme310
(tg-user-id user) ; => 12345311
(tg-user-is-bot user) ; => #t or #f312
(tg-user-first-name user) ; => "Alice"313
(tg-user-last-name user) ; => "Smith" or #f314
(tg-user-username user) ; => "alice" or #f315
```317
### Accessing Raw API Fields319
Any Telegram API field not modeled as a struct field is accessible via `tg-message-raw`:321
```scheme322
;; Access sticker data from a message323
(dict-ref (tg-message-raw msg) sticker: #f)325
;; Access location data326
(dict-ref (tg-message-raw msg) location: #f)327
```329
## Common Patterns331
### Echo Bot333
```scheme334
(import (sigil telegram))336
(define bot (make-tg-bot token: "123456:ABC-DEF..."))338
(tg-on-message bot339
(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 Bot348
```scheme349
(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 msg356
(string-append357
(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 msg370
(string-append (tg-bold "Status: ") (tg-code "online"))371
parse-mode: "MarkdownV2")))373
(tg-bot-run bot)374
```376
### Fetch-and-Respond Script378
Process pending messages without a long-running loop. Ideal for cron jobs, AI agents, or any script that runs periodically:380
```scheme381
(import (sigil telegram))383
(define client (tg-client token: "123456:ABC-DEF..."))385
(for-each386
(lambda (update)387
(let ((msg (tg-update-message update)))388
(when msg389
(let ((text (tg-message-text msg))390
(chat-id (tg-chat-id (tg-message-chat msg))))391
(cond392
((and text (string=? text "/status"))393
(tg-send-message client chat-id394
(string-append (tg-bold "Status: ") (tg-code "all systems go"))395
parse-mode: "MarkdownV2"))396
(text397
(tg-send-message client chat-id398
(string-append "You said: " (tg-escape-markdown text))399
parse-mode: "MarkdownV2")))))))400
(tg-fetch-updates client))401
```403
### Notification Sender405
Send one-shot messages from scripts or AI agents without checking for updates:407
```scheme408
(import (sigil telegram))410
(define client (tg-client token: "123456:ABC-DEF..."))411
(define chat-id 12345)413
;; Send a formatted status report414
(tg-send-message client chat-id415
(string-append416
(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 screenshot423
(tg-upload-photo client chat-id "/tmp/screenshot.png"424
caption: "Build output")425
```427
### Photo Bot429
```scheme430
(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
```