Skip to content

Akeboshiwind/tg-clj

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tg-clj

A simple-as-possible telegram bot api client inspired by aws-api.

Installation | Getting Started | Handling Updates | tg-clj-server

Why

This library gets out of the way so you can just use the Telegram Bot API (almost) directly.

(require '[tg-clj.core :as tg])

(def client (tg/make-client {:token "<Your bot token here>"}))

(tg/invoke client {:op :sendMessage
                   :request {:chat_id 1234 ; Replace with your chat_id
                             :text "Hello!"}})
;; => {:ok true,
;;     :result
;;     {:message_id 4321,
;;      :from
;;      {:id 123456789,
;;       :is_bot true,
;;       :first_name "My Awesome Bot",
;;       :username "mybot"},
;;      :chat
;;      {:id 987654321,
;;       :first_name "My",
;;       :last_name "Name",
;;       :username "myusername",
;;       :type "private"},
;;      :date 1709377902,
;;      :text "Hello!"}}

Installation

Use as a dependency in deps.edn or bb.edn:

io.github.akeboshiwind/tg-clj {:git/tag "v0.2.2" :git/sha "f742d7e"}

Getting Started

The workflow is as simple as it gets.

First require the namespace:

(require '[tg-clj.core :as tg])

Make a client (to learn how to create a bot and/or get it's token see here):

(def client (tg/make-client {:token "<Your bot token here>"}))

The browse telegram's documentation for a method you want to call.

Then invoke it as :op:

(tg/invoke client {:op :getMe})
;; => {:ok true,
;;     :result
;;     {:id 123456789,
;;      :is_bot true,
;;      :first_name "My Awesome Bot",
;;      :username "mybot",
;;      :can_join_groups true,
;;      :can_read_all_group_messages true,
;;      :supports_inline_queries true}}

You can provide parameters using the :request key:

(tg/invoke client {:op :sendMessage
                   :request {:chat_id 1234 ; Replace with your chat_id
                             :text "Hello!"}})
;; => {:ok true,
;;     :result
;;     {:message_id 4321,
;;      :from
;;      {:id 123456789,
;;       :is_bot true,
;;       :first_name "My Awesome Bot",
;;       :username "mybot"},
;;      :chat
;;      {:id 987654321,
;;       :first_name "My",
;;       :last_name "Name",
;;       :username "myusername",
;;       :type "private"},
;;      :date 1709377902,
;;      :text "Hello!"}}

If you provide a File as a top level parameter then the request will be sent correctly (using multipart/form-data):

(require '[clojure.java.io :as io])
(tg/invoke client {:op :sendPhoto
                   :request {:chat_id 1234
                             :photo (io/file "/path/to/my/pic.png")}})
;; => {:ok true,
;;     :result
;;     {:message_id 4321,
;;      :from
;;      {:id 123456789,
;;       :is_bot true,
;;       :first_name "My Awesome Bot",
;;       :username "mybot"},
;;      :chat
;;      {:id 987654321,
;;       :first_name "My",
;;       :last_name "Name",
;;       :username "myusername",
;;       :type "private"},
;;      :date 1709377902,
;;      :photo [ <snip> ]}}

Other than client errors, errors are given how telegram represents them:

(tg/invoke client {:op :sendMessage
                   ; Oops, missing the `text` field!
                   :request {:chat_id 1234}})
;; => {:ok false,
;;     :error_code 400,
;;     :description "Bad Request: message text is empty"}

If you want to inspect the full response in more detail, it's attached as metadata:

(meta (tg/invoke client {:op :getMe}))
;; => {:http-response
;;     {:opts
;;      {:as :text,
;;       :headers {"Accept" "application/json"},
;;       :method :post,
;;       :url
;;       "https://api.telegram.org/bot<your-token>/getMe"},
;;      :status 200,
;;      :headers
;;      { <snip> },
;;      :body
;;      "{\"ok\":true,\"result\":{\"id\":123456789,\"is_bot\":true,\"first_name\":\"My Awesome Bot\",\"username\":\"mybot\",\"can_join_groups\":true,\"can_read_all_group_messages\":true,\"supports_inline_queries\":true}}"}}

Please note that the contents of :http-response is an implementation detail from http-kit and may change.

Handling updates

(Checkout tg-clj-server if this is too "manual" for you)

The simplest way to get updates is to just invoke :getUpdates with a timeout (i.e. long polling):

(tg/invoke client {:op :getUpdates
                   :request {:offset 0
                             :timeout 5}})
;; => {:ok true,
;;     :result
;;     [ <snip> ]}

A simple loop to handle basic command events might look like this:

(defn contains-command? [u cmd]
  (when-let [text (get-in u [:message :text])]
    (let [pattern (str "^" cmd "($| )")]
      (re-find (re-pattern pattern) text))))

(defn hello-handler [u]
  (let [chat-id (get-in u [:message :chat :id])
        message-id (get-in u [:message :message_id])]
    {:op :sendMessage
     :request {:chat_id chat-id
               :text "Hello, world!"
               :reply_parameters {:message_id message-id}}}))

(loop [offset 0]
  (let [{:keys [ok result]}
        (invoke client {:op :getUpdates
                        :request {:offset offset
                                  :timeout 5}})]
    (if (and ok (seq result))
      (do (doseq [u result]
            (when (contains-command? u "/hello")
              (when-let [response (hello-handler u)]
                (invoke client response))))
          (recur (->> result (map :update_id) (apply max) inc)))
      (recur offset))))

Releasing

  1. Tag the commit v<version>
  2. git push --tags
  3. Update the README.md with the new version and git hash
  4. Update the CHANGELOG.md