Data driven Clojure bot library.
This library is under active development. APIs will probably change. Feel free to play with it though, it’s pretty usable by now.
If you want to stay informed about a production-ready release, subscribe to our Telegram channel. I will change the public APIs many times before release.
This library is inspired by the excellent python-telegram-bot library. Making bots with python-telegram-bot is a pleasure and a breeze. However, doing the same with Clojure should be even easier.
That’s the goal.
You can install the library from Clojars:
com.github.licht1stein/clj-telegram-bot {:mvn/version "0.1"}
[com.github.licht1stein/clj-telegram-bot "0.1"]
I know you want examples first and explanations later, so there’s an examples folder, where we’ll put all the interesting usage examples. But to get you started here’s a couple of popular ones:
Source: examples/ping_pong_bot.clj
A simple bot that answers “pong” if users sends him “ping”. Not that the filter is a regex pattern, but it can also be just a simple string “ping”, in this case the result is the same. Increase bot example below will show a better usage of regex.
(ns ping-pong-bot
(:require [telegram.core :as t]
[telegram.bot.dispatcher :as t.d]))
(def *ctx (t/from-token "YOUR_BOT_TOKEN"))
(def handlers
[{:type :message
:filter #"ping"
:actions [{:reply-text {:text "pong"}}]}])
(def dispatcher (t.d/make-dispatcher *ctx handlers))
(def updater (t/start-polling *ctx dispatcher))
(comment
"Run this to stop long-polling updater"
(t/stop-polling updater))
Source: examples/echo_bot.clj
Classical example of a bot that responds with the same text user sent. Note the :any
filter, it will return true to every message.
(ns echo-bot
(:require[telegram.core :as t]
[telegram.updates :as t.u] ; update helpers
[telegram.bot.dispatcher :as t.d]))
(def *ctx (t/from-token "YOUR_BOT_TOKEN"))
(def handlers
[{:type :message
:filter :any
:actions [(fn [upd ctx] {:reply-text {:text (t.u/message-text? upd)}})]}])
(def dispatcher (t.d/make-dispatcher *ctx handlers))
(def updater (t/start-polling *ctx dispatcher))
(comment
(t/stop-polling updater))
Source: examples/simple_command_bot.clj
Another classical example of a bot that responds to a command. This one responds to three commands: /start
and /help
, as recommended by the official guide, as well as /fn_command
to demonstrate a function based filter:
(ns simple-command-bot
(:require[telegram.core :as t]
[telegram.updates :as t.u] ; update helpers
[telegram.bot.dispatcher :as t.d]))
(def *ctx (t/from-token "YOUR_BOT_TOKEN"))
(def handlers
[{:type :command
:filter "/start"
:actions [{:reply-text {:text "You called the /start command"}}]}
{:type :command
:filter #"/help"
:actions [{:reply-text {:text "This bot does nothing useful"}}]}
{:type :command
:filter (fn [upd ctx] (= (t.u/message-text? upd) "/fn_command"))
:actions [{:reply-text {:text "Note that you can use functions for :filter and :actions for more complex filtering and action logic"}}]}])
(def dispatcher (t.d/make-dispatcher *ctx handlers))
(def updater (t/start-polling *ctx dispatcher))
(comment
(t/stop-polling updater))
When you create a dispatcher, you need to provide a vector of handlers. In fact that’s the main thing you want to do with your bot — handle incoming updates. A handler is a map with several required keys: :type
, :filter
, :actions
and bunch of optional keys, like :doc
or :passthrough
.
Let’s take a look at the handler we used for our ping-pong bot example:
{:type :message
:filter #"ping"
:actions [{:reply-text {:text "pong"}}]}
This describes the type of update that this handler will be applied to. Simple types are :message
, :command
, :inline-query
and :callback-query
. Later we will add more types for more exotic cases, but these will already let you do a lot.
Once a bot received an update, dispatcher will check it’s type and select all handlers for this type of update. After that it will look for handlers for which the :filter
matches.
The filter is a way for dispatcher to check if handler should be applied to this particular update. For messages the simplest forms of a filter is a string, which is simply checked for equality or a regex pattern, which is matched against the message text.
You can also provide a (fn [upd ctx])
function as a filter to implement logic of any complexity.
Dispatcher checks filters from first to last until it finds a match. It then applies this handler to the update and stops. If you want the dispatcher to continue looking for more matches after this handler’s actions were applied, you can achieve this by setting :passthrough true
in the handler.
Vector of actions to perform. In most cases an action is some sort of response, you can provide simplest actions as :reply-text
or :send-text
maps. These simplify working with simpler use cases and also lets you easily test your bot. Since both update and action are just maps, you can write unit tests to check if the action produces expected result given a certain update.
Action can also be a (fn [upd ctx])
function, that either produces a action map (preferable) or directly interacts with telegram API or does arbitrary things (for more complex cases).
You can provide multiple actions for a single handler to allow triggering multiple actions by a single update.
Additional filter that check the :ctb/user
map produced by Auth middleware to see if the user has the right to access this handler.
For a complete example see examples/rights_checker_command_bot.clj
If set to true
it will tell the dispatcher to continue applying handlers even if this one was a match. This gives you a simple mechanism to apply multiple handlers to a single update without cluttering.
Documentation describing this handler.
When we build a simple REST API we work with requests. In Clojure they’re normally just a map, usually conforming to ring spec. This approach proved to be amazingly productive, allowing different server and client libraries to interact by conforming to the ring standard.
Telegram update object can be viewed in a similar light: it’s a standardized map that we process. So it seemed logical to add a possibility of applying middleware to it.
Any filter, handler or middleware function in clj-telegram-bot accepts two arguments upd
and ctx
— update and context. Update is the map bot received from the telegram server, and context is a local map of clj-telegram-bot used for all kinds of interesting things.
So middleware is any function that receives upd
and ctx
and returns an upd
— modified or unmodified update map. Usages can be plenty: logging updates, saving updates to file or enriching the update object with useful information, for example authentication info.
Source: examples/ping_pong_middleware_bot.cljm
Here’s and example of a modified ping-pong bot that also logs and saves every incoming update:
(ns ping-pong-middleware-bot
(:require [telegram.core :as t]
[telegram.bot.dispatcher :as t.d]))
(def *ctx (t/from-token "YOUR_BOT_TOKEN"))
(def handlers
[{:type :message
:filter #"ping"
:actions [{:reply-text {:text "pong"}}]}])
(defn log-update [upd ctx]
(println upd)
upd)
(defn spit-update [upd ctx]
(spit "last-update.edn" upd)
upd)
(def dispatcher (t.d/make-dispatcher *ctx handlers :update-middleware [spit-update log-update]))
(def updater (t/start-polling *ctx dispatcher))
(comment
(t/stop-polling updater))
For your convenience clj-telegram-bot comes with some helpers to create often used middleware.
One of the standard tasks for a bot is telling if the user is registered or not, admin or not etc. Here’s an example of implementing authentication middleware. This middleware uses the user-auth
function to identify the user, and then adds the result to the update under :ctb/user
key.
The :ctb/user
map can then be used with the :user handler key to check if the user has the rights to access this handler.
(ns auth-middleware
(:require [telegram.middleware.auth :as t.auth]))
(def user-db
"This is a simple example of some sort of database that stores user information."
{1234567 {:user "Owner"
:admin? true}})
(defn user-auth
"This is a function that we provide to auth middleware maker. It has to accept one argument — a telegram id, and return a map or nil."
[telegram-id]
(user-db telegram-id))
(def auth-middleware
"We can use the user-auth function to create authentication middleware that will add the resulting user map to the update under `:ctb/user` key."
(t.auth/make-auth-middleware user-auth))
;; Now we can add the middleware when instantiating our dispatcher.
(def dispatcher (t.d/make-dispatcher *ctx handlers :update-middleware [auth-middleware]))
For a complete example of a bot that handles some commands only if they were sent from an admin see examples/rights_checker_command_bot.clj
If you need information about creating bots and getting a token, read this part of the official manual.
First you need to produce your telegram context map. There are many ways to do that, the simplest one is based on providing token as plain text.
(require '[telegram.core :as t])
(def telegram (t/from-token "YOUR_TOKEN"))
However this is the least recommended way, as it’s very insecure — you have to pass your token around the code base, and that’s always a bad idea with secrets. Instead there’s a bunch of helper functions to get the token from all kinds of places of varying security:
Very popular and useful if deploying to services like Heroku. Set an environment variable BOT_TOKEN
to use it:
(def telegram (t/from-env))
Another way is to get your token from password and secrets managers. Two are supported out of the box: pass and 1Password CLI.
Normally you would use pass from command line like this:
pass my-t/token
So for example above the usage would be:
(def telegram (t/from-pass "my-t/token"))
For 1Password CLI you need to provide an item name or ID (better) and field name where the token is stored. So if you have a 1Password item called my-bot
and a field called token
, your CLI command would be:
op item get "ITEM_ID" --fields "FIELD_NAME"
So the corresponding code is:
(def telegram (t/from-op "ITEM_ID" "FIELD_NAME"))
You can also initiate the config by passing an arbitrary function that takes no arguments and returns a string with bot token in it:
(defn my-token-getter []
;; some magical code that gets the token
)
(def telegram (t/from-fn my-token-getter))