org.clojars.hsestupin/slark0.0.5Monitor your Telegram bot activity with ease dependencies
| (this space intentionally left almost blank) | ||||||||||||||||||
(ns slark.telegram (:require [clj-http.client :as http] [clojure.data.json :as json] [clojure.java.io :as io] [clojure.string :as str] [environ.core :refer :all])) | |||||||||||||||||||
API basicsAll public API methods such as | |||||||||||||||||||
| (comment (send-message 1234 "Hello world" {:token "1234:ABCD" :dry-run? true :entire-response? false :http-options {:retry-handler (fn [ex try-count http-context] (println "Got:" ex) (if (> try-count 4) false true))}}) ;; returned value would contain just request information {:throw-exceptions? false, :accept :json, :query-params {"chat_id" 1234, "text" "Hello world"}, :retry-handler "#function[slark.api/eval23020/fn--23021]", :method :get, :url "https://api.telegram.org/bot1234:ABCD/sendMessage"} ) | ||||||||||||||||||
(def base-url "https://api.telegram.org/bot") (def default-post-optional-keys #{:disable-notification :reply-to-message-id :reply-markup}) | |||||||||||||||||||
(defn get-token [] (env :telegram-bot-token)) | |||||||||||||||||||
(defn- to-telegram-format [k] (-> k name (str/replace "-" "_"))) | |||||||||||||||||||
Recursively transforms all map keys to telegram format replacing '-' with '_' | (defn- to-telegram-format-keys [m] (let [to-telegram-format (fn [[k v]] [(to-telegram-format k) v])] (clojure.walk/postwalk (fn [x] (if (map? x) (into {} (map to-telegram-format x)) x)) m))) | ||||||||||||||||||
(defn- build-telegram-api-url [{:keys [token] :or {token (get-token)}} url-suffix] {:pre [(some? token)]} (str base-url token url-suffix)) | |||||||||||||||||||
(defn extract-telegram-payload [response] (-> response :body ;; clojurify response keys. Write :chat-id instead of chat_id (json/read-str :key-fn #(keyword (str/replace % "_" "-"))))) | |||||||||||||||||||
(defn- get-result [entire-response? response] (if entire-response? response (extract-telegram-payload response))) | |||||||||||||||||||
Do a HTTP GET request to a telegram bot API with specified url-suffix and query-params | (defn- do-get-request [{:keys [http-options entire-response? dry-run?] :as options} query-params url-suffix] (let [telegram-api-url (build-telegram-api-url options url-suffix) request-options (merge {:throw-exceptions? false :accept :json :query-params (to-telegram-format-keys query-params)} http-options)] (if dry-run? (merge request-options {:method :get :url telegram-api-url}) (get-result entire-response? (http/get telegram-api-url request-options))))) | ||||||||||||||||||
(defn- merge-multipart [required-data optional-params] (reduce (fn [data [k v]] (conj data {:name k :content (str v)})) required-data optional-params)) | |||||||||||||||||||
Do a HTTP POST multipart/form-data request to a telegram bot API with specified url-suffix and multipart-data | (defn- do-post-request ([options url-suffix required-data] (do-post-request options url-suffix required-data default-post-optional-keys)) ([{:keys [http-options entire-response? dry-run?] :as options} url-suffix required-data optional-keys] (let [optional-data (to-telegram-format-keys (select-keys options optional-keys)) multipart-data (merge-multipart required-data optional-data) telegram-api-url (build-telegram-api-url options url-suffix) request-options (merge {:throw-exceptions? false :accept :json :multipart multipart-data} http-options)] (if dry-run? (merge request-options {:method :post :url telegram-api-url}) (get-result entire-response? (http/post telegram-api-url request-options)))))) | ||||||||||||||||||
Receive updates from telegram Bot via long-polling. For more info look at official doc. Supplied options:
| (defn get-updates [& [options]] (let [query-params (merge {:timeout 1 :offset 0 :limit 100} options)] (do-get-request options query-params "/getUpdates"))) | ||||||||||||||||||
Send setWebhook request to telegram API. For more detailed information look at official doc. How to generate self-signed certificate - https://core.telegram.org/bots/self-signed Supplied options:
| (defn set-webhook [& [{:keys [url certificate] :or {url ""} :as options}]] (let [partial-data [{:name "url" :content url}] multipart-data (if certificate (conj partial-data {:name "certificate" :content certificate}) partial-data)] (do-post-request options "/setWebhook" multipart-data []))) | ||||||||||||||||||
A simple method for testing your bot's auth token. Returns basic information about the bot in form of a User object. official doc | (defn get-me [& [options]] (do-get-request options {} "/getMe")) | ||||||||||||||||||
Use this method to send text messages. On success, the sent Message is returned. official doc
| (defn send-message [chat-id text & [options]] (let [optional-params (select-keys options [:parse-mode :disable-web-page-preview :disable-notification :reply-to-message-id :reply-markup]) query-params (merge {:chat-id chat-id :text text} optional-params)] (do-get-request options query-params "/sendMessage"))) | ||||||||||||||||||
Use this method to forward messages of any kind. On success, the sent Message is returned. official doc
| (defn forward-message [chat-id from-chat-id message-id & [options]] (let [optional-params (select-keys options [:disable-notification]) query-params (merge {:chat-id chat-id :from-chat-id from-chat-id :message-id message-id} optional-params)] (do-get-request options query-params "/forwardMessage"))) | ||||||||||||||||||
Use this method to send photos. On success, the sent Message is returned. official doc
| (defn send-photo [chat-id photo & [options]] {:pre [(some? chat-id) (some? photo)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "photo" :content photo}]] (do-post-request options "/sendPhoto" required-data (conj default-post-optional-keys :caption)))) | ||||||||||||||||||
Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .mp3 format. On success, the sent Message is returned. official doc
| (defn send-audio [chat-id audio & [options]] {:pre [(some? chat-id) (some? audio)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "audio" :content audio}]] (do-post-request options "/sendAudio" required-data (conj default-post-optional-keys :duration :performer :title)))) | ||||||||||||||||||
Use this method to send general files. On success, the sent Message is returned. official doc
| (defn send-document [chat-id document & [options]] {:pre [(some? chat-id) (some? document)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "document" :content document}]] (do-post-request options "/sendDocument" required-data (conj default-post-optional-keys :caption)))) | ||||||||||||||||||
Use this method to send .webp stickers. On success, the sent Message is returned. official doc
| (defn send-sticker [chat-id sticker & [options]] {:pre [(some? chat-id) (some? sticker)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "sticker" :content sticker}]] (do-post-request options "/sendSticker" required-data))) | ||||||||||||||||||
Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). On success, the sent Message is returned. official doc
| (defn send-video [chat-id video & [options]] {:pre [(some? chat-id) (some? video)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "video" :content video}]] (do-post-request options "/sendVideo" required-data (conj default-post-optional-keys :duration :width :height :caption)))) | ||||||||||||||||||
Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .ogg file encoded with OPUS (other formats may be sent as Audio or Document). On success, the sent Message is returned. official doc
| (defn send-voice [chat-id voice & [options]] {:pre [(some? chat-id) (some? voice)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "voice" :content voice}]] (do-post-request options "/sendVoice" required-data (conj default-post-optional-keys :duration)))) | ||||||||||||||||||
Use this method to send point on the map. On success, the sent Message is returned. official doc
| (defn send-location [chat-id latitude longitude & [options]] {:pre [(some? chat-id) (some? latitude) (some? longitude)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "latitude" :content (str latitude)} {:name "longitude" :content (str longitude)}]] (do-post-request options "/sendLocation" required-data))) | ||||||||||||||||||
Use this method to send information about a venue. On success, the sent Message is returned. official doc
| (defn send-venue [chat-id latitude longitude title address & [options]] {:pre [(some? chat-id) (some? latitude) (some? longitude) (some? title) (some? address)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "latitude" :content (str latitude)} {:name "longitude" :content (str longitude)} {:name "title" :content (str title)} {:name "address" :content (str address)}]] (do-post-request options "/sendVenue" required-data (conj default-post-optional-keys :foursquare-id)))) | ||||||||||||||||||
Use this method to send phone contacts. On success, the sent Message is returned. official doc
| (defn send-contact [chat-id phone-number first-name & [options]] {:pre [(some? chat-id) (some? phone-number) (some? first-name)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "phone_number" :content (str phone-number)} {:name "first_name" :content (str first-name)}]] (do-post-request options "/sendContact" required-data (conj default-post-optional-keys :last-name)))) | ||||||||||||||||||
Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). official doc
| (defn send-chat-action [chat-id action & [options]] {:pre [(some? chat-id) (some? action)]} (let [required-data [{:name "chat_id" :content (str chat-id)} {:name "action" :content (if (keyword? action) (to-telegram-format action) (str action))}]] (do-post-request options "/sendChatAction" required-data {}))) | ||||||||||||||||||
For debugging purposes | (comment (do (def chat-id (Integer/parseInt (env :chat-id))))) | ||||||||||||||||||
(ns slark (:require [clojure.string :as str] [environ.core :refer :all] [slark.telegram :refer :all] [taoensso.timbre :as timbre :refer (log trace debug info warn error fatal report logf tracef debugf infof warnf errorf fatalf reportf spy get-env log-env)] [clojure.core.async :as async :refer (close! put! poll! go go-loop chan <! <!! >! >!! alt! alts! alt!! alts!!)])) | |||||||||||||||||||
True if this update represents a bot command. Otherwise false | (defn bot-command? [{:keys [entities]}] (not-empty (filterv #(= (:type %) "bot_command") entities))) | ||||||||||||||||||
Returns corresponding handler to message if it's a bot like message which looks like | (defn get-command [{:keys [text] :as message}] (when (bot-command? message) (first (.split (.substring text 1) " ")))) | ||||||||||||||||||
(defn get-message [update] (or (:message update) (:edited-message update))) | |||||||||||||||||||
(defn handle-update [handlers update state] (let [message (get-message update) command (get-command message) handler (handlers command)] (if handler (do (debug "Update" (:update-id update) "will be handled by" command) (handler update state)) state))) | |||||||||||||||||||
Async execute get-udpates function because it might utilize blocking IO. | (defn- get-updates-async [get-updates-fn offset] (let [result-ch (chan 0)] (go (>! result-ch (get-updates-fn offset))) result-ch)) | ||||||||||||||||||
Puts telegram updates obtained via Supplied Also there are additional optional arguments: By default the supplied channel will not be closed after telegram error or termination request, but it can be determined by the | (defn updates-onto-chan [ch & [{:keys [initial-offset close? get-updates-fn] :or {initial-offset 0 close? false get-updates-fn (fn [offset] (get-updates {:offset offset}))} :as opts}]] (let [terminate-ch (chan) close-ch-fn (fn [] (when close? (close! ch)))] (go-loop [offset initial-offset] (debug "Trying to get-udpates with offset" offset) (let [get-updates-ch (get-updates-async get-updates-fn offset)] (alt! terminate-ch (do (warn "Termination request") (close-ch-fn)) get-updates-ch ([{:keys [ok result] :as response}] (alt! [[ch response]] (if ok (recur (reduce max offset (mapv (comp inc :update-id) result))) (do (warn "Telegram error" response) (close-ch-fn))) terminate-ch (do (warn "Termination request") (close-ch-fn))))))) (fn terminate! [] (put! terminate-ch :terminate)))) | ||||||||||||||||||
Creates transducer which handles supplied | (defn command-handling [command handler] (fn [rf] (fn ([] (rf)) ([result] (rf result)) ([result update] (let [message (get-message update)] (when (and (bot-command? message) (= command (get-command message))) (handler update)) (rf result update)))))) | ||||||||||||||||||
(comment (do (defn- echo [update] (let [message (get-message update) chat-id (get-in message [:chat :id]) text (:text message)] (send-message chat-id (str "received '" text "'")))) (def c (chan 1 (comp (filter (comp not-empty :result)) (map #(do (println %) (:result %))) cat (command-handling "echo" echo)))) (def terminate (updates-onto-chan c)) (go-loop [update (<! c)] (when update (do (info "Update" (:update-id update) "processed") (recur (<! c))))))) | |||||||||||||||||||