Telegram-bot

I've made separate script, that runs independently from httpGate and does long-polling requests to Telegram server. Telegram server has the maximum timeout of 60s, so the request will block for one minute if there was no new messages arrived to the server. But when new message comes to the server, it responds immediately. The script appends message to "fifo/tg_bot" file in PLIO format, sends hello-message to the user/chat and then invokes the main application via HTTP. The main application reads, truncates "fifo/tg_bot" and stores messages in the DB for futher use.

This way we never lose messages, even if main application is down, e.g. because of inactivity timeout.

JSON-library of Alexander Williams (https://github.com/aw/picolisp-json) helped a lot, because JSON-parser from PicoLisp distribution doesn't support unicode escape sequences \uNNNN.

--- run_telegram_bot ---
#!/bin/bash

echo "Starting telegram bot"
cd ~/configurator
~/picoLisp/pil app/telegram_bot.l -main >> ~/log/telegram_bot 2>&1 &


--- app/telegram_bot.l ---
# Telegram bot script
# *TgBotApiKey *TgAdminId *UpdateId *TgReadUpdatesUrl
(load "app/curl.l" "app/json.l" "app/telegram.l" "app/local_config.l")

(setq *TgBotApiKey "<your-API-key-here>")
(setq *TgAdminId "<ChatID>") # Who recieves notifications about new registrations
(setq *TgReadUpdatesUrl "http://localhost/<your-app-name>/!telegram_read_updates")

(de telegram_updateHandler (Upd)
   (let? Msg (cdr (assoc "message" Upd))
      (let (Chat (cdr (assoc "chat" Msg))
            ChatId (cdr (assoc "id" Chat))
            From (cdr (assoc "from" Msg))
            Nm (cdr (assoc "first_name" From))
            LastNm (cdr (assoc "last_name" From))
            UserNm (cdr (assoc "username" From))
            Text (cdr (assoc "text" Msg)) )
      (case Text
         ("/start"
            (msg 'handler " cmd: /start" )
            (telegram_sendText *TgBotApiKey ChatId (pack "Welcome " Nm "!"))
            (telegram_sendText *TgBotApiKey *TgAdminId (pack "reg-msg: " (sym From)))
            ) ) ) ) )

(de telegram_invoke_main_app ()
   (msg 'telegram_read_updates ": "
      (cdr (assoc "Body" (req-get *TgReadUpdatesUrl NIL (till NIL T)))) ) )

(de telegram_getUpdates (BotApiKey UpdateId Timeout)
   (default Timeout 60)
   (let (Resp (req-post (pack "https://api.telegram.org/bot" BotApiKey "/getUpdates")
               NIL (list (cons "timeout" Timeout) (cons "offset" UpdateId) )
               (decode (till NIL T)) )
         Body (cdr (assoc "Body" Resp))
         Ok (= (cdr (assoc "ok" Body)) 'true)
         Res (cddr (assoc "result" Body)) )
      (ifn Ok
         (nil (msg 'ERROR " telegram_getUpdates failed: " (str Body)))
         (out "+fifo/tg_bot"
            (for Upd Res
               (pr Upd)
               (telegram_updateHandler Upd)
               (setq *UpdateId (max *UpdateId (inc (cdr (assoc "update_id" Upd))))) ) )
         (telegram_invoke_main_app)
         T ) ) )

# main loop
(de main ()
   (unless *TgBotApiKey
      (quit "*TgBotApiKey is not set. Exiting...") )
   (unless (call 'test "-f" "fifo/tg_bot")
      (call 'mkdir "-p" "fifo") )
   (msg 'INFO " Telegram bot started")
   (zero *UpdateId)
   # find max update_id in "fifo/tg_bot"
   (and (info "fifo/tg_bot")
      (in "fifo/tg_bot"
         (while (rd)
            (setq *UpdateId (max *UpdateId (inc (cdr (assoc "update_id" @))))) ) ) )
   (if (gt0 *UpdateId)
      (telegram_invoke_main_app) )
   (loop
      (ifn (telegram_getUpdates *TgBotApiKey *UpdateId 60)
         (wait 10000)
         (wait 1000) ) ) )


--- app/curl.l ---
# Simple wrapper for command-line 'curl'

(de _htStatus ()
   (setq HttpVer (till " " T)) (char)
   (setq HttpCode (format (till " " T))) (char)
   (setq HttpMessage (line T)) )

(de curlreq (Cmd Prg Ofs)
   (use (HttpVer HttpCode HttpMessage RespHeaders Resp)
      (off Resp)
      (in Cmd
         (_htStatus)
         (when (= HttpCode 100)
            (line)
            (_htStatus) )
         (while
            (ifn (till ":^M^J")
               (line)
               (prog (push 'RespHeaders (cons (pack @) (prog (char) (skip) (line T)))) @) ) )
         (push 'Resp (cons "Headers" RespHeaders))
         (push 'Resp (cons "Cmd" Cmd))
         (push 'Resp (cons "Message" HttpMessage))
         (push 'Resp (cons "Code" HttpCode))
         (push 'Resp (cons "Version" HttpVer))
         (ifn (eof)
            (push 'Resp (cons "Body" (run Prg Ofs))) ) )
      Resp ) )

(de req-get (Url Headers . "Prg")
   (let Curl (list Url)
      (mapc '((X) (push 'Curl (pack (car X) ": " (cdr X))) (push 'Curl '-H)) Headers)
      (push 'Curl "GET" "-X" "-isS" 'curl)
      (curlreq Curl "Prg") ) )

# Data: '((param1 . "value1") (param2 . "value2"))
(de req-post (Url Headers Data . "Prg")
   (let Curl (list Url)
      (mapc '((X) (push 'Curl (pack (car X) "=" (cdr X))) (push 'Curl '-F)) Data)
      (mapc '((X) (push 'Curl (pack (car X) ": " (cdr X))) (push 'Curl '-H)) Headers)
      (push 'Curl "POST" "-X" "-isS" 'curl)
      (curlreq Curl "Prg") ) )

# not used
# (load "@lib/json.l")
# (de req-get-json (Url Headers)
#    (req-get Url Headers (readJson T)) )
#
# (de req-post-json (Url Headers Data)
#    (req-post Url Headers Data (readJson T)) )


Below are parts of the main application

--- app/er.l ---
...
# DB-class in main application for storing Telegram updates
(class +TgUpd +Entity)
(rel id (+Key +Number)) # update id
(rel data (+Any))       # whole update message
...


--- app/telegram.l ---
# some wrappers for Telegram bot API
(allow "!telegram_read_updates")

# "!telegram_read_updates" URL is invoked from "telegram_bot" script
(de telegram_read_updates ()
   # read content of "fifo/tg_bot" file, write to DB, truncate the file
   (dbSync)
   (use (Upd)
      (in "fifo/tg_bot"
         (while (setq Upd (rd))
            (msg 'tg_update " " (sym Upd))
            (request '(+TgUpd) 'id (cdr (assoc "update_id" Upd)) 'data Upd) ) ) )
   (commit 'upd)
   (out "fifo/tg_bot") # truncate the file
   (respond (sym (maxKey (tree 'id '+TgUpd)))) ) # respond with max ‘update_id’ from DB

# send text from bot to chat
(de telegram_sendText (BotApiKey ChatID Text)
   (and BotApiKey ChatID
      (req-post
         (pack "https://api.telegram.org/bot" BotApiKey "/sendMessage")
         NIL
         (list
            # see Telegram bot API docs to sendMessage
            # (cons "disable_notification" "true|false")
            # (cons "parse_mode" "markdown")
            (cons "chat_id" ChatID)
            (cons "text" Text) )
         # if we need to parse JSON-response from bot API
         # (decode (till NIL T))
         ) ) )

https://picolisp.com/wiki/?telegrambot

07apr18    m_mans