Last active
December 5, 2019 16:47
-
-
Save kix/9031aa6be7363ddb6091ddce1a6aacaa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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