Skip to content

Instantly share code, notes, and snippets.

@kix
Last active December 5, 2019 16:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kix/9031aa6be7363ddb6091ddce1a6aacaa to your computer and use it in GitHub Desktop.
Save kix/9031aa6be7363ddb6091ddce1a6aacaa to your computer and use it in GitHub Desktop.
(ns taskbot.telegram
(:require [clj-http.client :as http]
[cheshire.core :as json]
[clojure.core.async :as a :refer [chan put! close! take! go go-loop <! >! alts!]]
[environ.core :refer [env]]
[clojure.tools.logging :as log]
[clojure.spec.alpha :as s]
[clojure.spec.test.alpha :refer [instrument]]))
(def base-url (or (env :base-url) "https://api.telegram.org/bot"))
(s/def ::chat_id int?)
(s/def ::text string?)
(s/def ::url (partial re-matches #"(?i)^(http(s?)|tg)://.*"))
(s/def ::forward_text string?)
(s/def ::bot_username string?)
(s/def ::callback_data string?)
(s/def ::request_write_access boolean?)
(s/def ::pay boolean?)
(s/def ::login_url (s/keys :req-un [::url]
:opt-un [::forward_text ::bot_username ::request_write_access]))
(s/def ::parse_mode #{:Markdown :HTML})
(s/def ::callback_game (constantly true))
(s/def ::switch_inline_query string?)
(s/def ::switch_inline_query_current_chat string?)
(s/def ::inline_keyboard_key
(s/keys
:req-un [::text]
:opt-un [::url ::login_url ::callback_data ::switch_inline_query
::switch_inline_query_current_chat ::pay]))
(s/def ::inline_keyboard (s/coll-of (s/coll-of ::inline_keyboard_key)))
(s/def ::reply_markup (s/keys :req-un [::inline_keyboard]))
(s/def ::disable_web_page_preview boolean?)
(s/def ::disable_notification boolean?)
(s/def ::reply_to_message_id int?)
(defn get-me
"Returns bot's Telegram profile. Useful for testing."
[token]
(let [url (str base-url token "/getMe")
resp (http/get url {:content-type :json
:as :json})]
(:body resp)))
(defn send-message!
"Sends a Telegram message."
[token options]
(let [url (str base-url token "/sendMessage")
resp (http/post url {:content-type :json
:as :json
:form-params options})]
(:body resp)))
(s/fdef send-message!
:args (s/and
(s/cat
:token string?
:options (s/keys :req-un [::chat_id ::text]
:opt-un [::parse_mode ::inline_keyboard ::reply_to_message_id
::reply_markup ::disable_web_page_preview
::disable_notification])))
:ret map?)
(instrument `send-message!)
(defn get-updates [token {:keys [timeout limit offset]}]
"Fetches newest updates and returns a chan for those"
(log/info "Getting updates")
(let [url (str base-url token "/getUpdates")
query {:timeout (or timeout 1)
:offset (or offset 0)
:limit (or limit 100)}
request {:query-params query
:async? true}
result (chan)
on-success (fn [resp]
(if-let [data (-> resp :body (json/parse-string true) :result)]
(do
(log/info (str "Successfully received " (count data) " updates"))
(put! result data))
(put! result ::error)))
on-failure (fn [err]
(put! result ::error)
(close! result))]
(log/info (str "Fetch params: " query))
(http/get url request on-success on-failure)
result))
(defn update-type [update]
"Figures out message type depending on its content"
(cond
(some
#(= (:type %) "bot_command")
(-> update :message :entities))
:bot_command
(map? (-> update :callback_query))
:callback
(map? (-> update :message :sticker))
:sticker
(map? (-> update :message :location))
:location
(map? (-> update :message :document))
:document
(map? (-> update :message :photo))
:photo
(map? (-> update :message :text))
:text
:default
:message))
(defn make-consumer [control-chan handler]
(go-loop []
(when-let [updates (<! control-chan)]
(handler updates)
(recur))))
(defn make-producer [token {:keys [timeout limit offset] :as options}]
(let [control-chan (chan)
last-seen-id (atom 0)]
(go-loop []
(log/trace "Fetching from last-seen" @last-seen-id)
(let [updates-chan (get-updates token (assoc options :offset @last-seen-id))
[data chan] (alts! [updates-chan control-chan])]
(case data
nil
(do
(log/info "Closing polling")
(close! control-chan))
(let [last-id (or (-> data last :update_id) 0)]
(if (> 0 last-id)
(log/trace "Updating last-seen from" @last-seen-id " to " last-id)
(reset! last-seen-id (inc last-id)))
(doseq [item data] (>! control-chan (assoc item :type (update-type item))))
(recur)))))
control-chan))
(defn start [token handler & options]
(let [control-chan (make-producer token options)
consumer-chan (make-consumer control-chan handler)]
control-chan))
(comment
(defmulti handler :type)
(defmethod handler :message
[message]
(clojure.pprint/pprint message))
(defmethod handler :bot_command
[message]
(clojure.pprint/pprint message))
(start "<token>" handler)
(def control-chan (make-producer "Your token here" {}))
(def consumer-chan
(make-consumer control-chan handler))
(close! consumer-chan)
(close! control-chan))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment