Tiny Telegram Bot in Racket

Published April 21, 2022 · 507 words · 3 minute read

I’ve decided to go all-in on functional programming on personal projects for a while, reading HTDP and learning Racket as a foundation. I’m only on chapter 5 of HTDP, but decided to jump the gun a bit and attempt to write a Telegram bot with Racket and raw HTTP requests. Look ma, no library!

Please excuse my inelegance in Racket. It’ll come with time.

The program below simply echoes back comments to trusted users, and tells untrusted users to talk to the admin.

Telegram bots work by interacting with the Telegram API, either via webhooks or long polling. To use webhooks you must expose a web service to the internet, and since I wanted to write and test something today, I opted for the simpler action of POSTing to get messages and send replies.

API Documentation: core.telegram.org/bots/api .

#lang racket

(require net/http-easy)

; Program Variables

(define TRUSTED_MEMBERS (list 000000000))
(define TOKEN "0000000000:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
(define API_URL "https://api.telegram.org")
(define API_REQUEST_BASESTRING (string-append API_URL "/bot" TOKEN "/"))
(define REQ_GETUPDATES (string-append API_REQUEST_BASESTRING "getUpdates"))
(define REQ_SENDMESSAGE (string-append API_REQUEST_BASESTRING "sendMessage"))

(define LONG_TIMEOUT (make-timeout-config 
                      #:lease 20
                      #:connect 5
                      #:request 300
                      ))

; Main Functions

(define (mainloop offset)
  (sleep 1) ; Don't spin the main loop too fast. TODO: fix long polling.
  (define res
    (post REQ_GETUPDATES
          #:json (hash 'offset (+ offset 1))
          #:timeouts LONG_TIMEOUT
          ))

  (define telegram-messages (hash-ref (response-json res) 'result))

  ; Handle incoming telegram messages based on server response
  (cond
    [(empty? telegram-messages)
     ;(println "No messages, looping.")
     (mainloop 0)]
    [(not (eq? 200 (response-status-code res)))
     (println "Non-200 response, looping in 5s.")
     (sleep 5)
     (mainloop 0)]
    [else (handle-messages telegram-messages)]
    )
  )

(define (handle-messages messages)
  (for ([m messages]) (handle-one-message m))
  ; Return ID of last message, so server can correctly return next set.
  (mainloop (get-message-id (last messages))))

(define (handle-one-message telegram-message)
  (show-message-info telegram-message)
  (if (is-trusted-member? (get-user-id telegram-message))
      (process-message telegram-message)
      (reject-message telegram-message)))

(define (process-message telegram-message)
  (send-reply (get-chat-id telegram-message) (get-message-text telegram-message)))

(define (reject-message telegram-message)
  (send-reply (get-chat-id telegram-message) "You are untrusted. Contact admin."))

(define (send-reply chat-id message-text)
  (post REQ_SENDMESSAGE
        #:json (make-hash (list
                           (cons 'chat_id chat-id)
                           (cons 'text message-text)
                           )
                          )))

; Helper Functions

(define (is-trusted-member? user-id)
  (member user-id TRUSTED_MEMBERS))

(define (get-message-id telegram-message)
  (hash-ref telegram-message 'update_id))

(define (get-user-id telegram-message)
  (nested-hash-ref telegram-message '(message from id)))

(define (get-chat-id telegram-message)
  (nested-hash-ref telegram-message '(message chat id)))

(define (get-message-text telegram-message)
  (nested-hash-ref telegram-message '(message text)))

(define (show-message-info telegram-message)
  (printf "id ")
  (printf (number->string (get-user-id telegram-message)))
  (printf " says: '")
  (printf (get-message-text telegram-message))
  (printf "'\n"))

; Helper Helper Functions

(define (nested-hash-ref hash ref-list)
  (cond
    [(null? ref-list) hash]
    [(null? (cdr ref-list)) (hash-ref hash (car ref-list))]
    [else (nested-hash-ref (hash-ref hash (car ref-list)) (cdr ref-list))]
    ))


; Run: Start the main loop (with ID 0 so all new messages are fetched.)

(mainloop 0)

…and that’s all I wrote!

Yes, there are problems. Please comment below, I’d genuinely love to hear how insecure and inefficient this thing is, and why it’s long polling incorrectly.

If you throw in your own token (obtained by talking to the Botfather,) run the bot to see your ID, and throw that ID in the trusted users list, you’ll have a reasonable basis for constructing your own little Telegram bot. Enjoy!

Comments