Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/licht1stein/clj-telegram-bot

Data driven Clojure bot library
https://github.com/licht1stein/clj-telegram-bot

Last synced: about 2 months ago
JSON representation

Data driven Clojure bot library

Awesome Lists containing this project

README

        

#+TITLE: Clojure Telegram Bot
Data driven Clojure bot library.

* Warning!
*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 [[https://t.me/clj_telegram_bot][Telegram channel]]. I will change the public APIs many times before release.

* Motivation

This library is inspired by the excellent [[https://python-telegram-bot.org/][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.

* Table of contents :toc_4:
- [[#warning][Warning!]]
- [[#motivation][Motivation]]
- [[#installation][Installation]]
- [[#using-depsedn][Using deps.edn]]
- [[#using-lein][Using lein]]
- [[#examples][Examples]]
- [[#pingpong-bot][Ping/pong bot]]
- [[#echo-bot][Echo bot]]
- [[#simple-command-bot][Simple command bot]]
- [[#handlers][Handlers]]
- [[#required-keys][Required keys]]
- [[#type][:type]]
- [[#filter][:filter]]
- [[#actions][:actions]]
- [[#optional-keys][Optional keys]]
- [[#user][:user]]
- [[#passthrough][:passthrough]]
- [[#doc][:doc]]
- [[#middleware][Middleware]]
- [[#example-middleware][Example middleware]]
- [[#ping-pong-bot-with-middleware][Ping-Pong bot with middleware]]
- [[#included-middleware][Included middleware]]
- [[#auth-middleware][Auth middleware]]
- [[#usage][Usage]]
- [[#creating-context][Creating context]]
- [[#from-bot-token][From bot token]]
- [[#from-environment-variables][From environment variables]]
- [[#from-password-managers][From password managers]]
- [[#pass][pass]]
- [[#1password][1Password]]
- [[#from-an-arbitrary-function][From an arbitrary function]]

* Installation
You can install the library from [[https://clojars.org/com.github.licht1stein/clj-telegram-bot][Clojars]]:

** Using deps.edn
#+begin_src clojure
com.github.licht1stein/clj-telegram-bot {:mvn/version "0.1"}
#+end_src

** Using lein
#+begin_src clojure
[com.github.licht1stein/clj-telegram-bot "0.1"]
#+end_src

* Examples
I know you want examples first and explanations later, so there's an [[./examples][examples]] folder, where we'll put all the interesting usage examples. But to get you started here's a couple of popular ones:

** Ping/pong bot
Source: [[./examples/ping_pong_bot.clj][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.

#+begin_src clojure
(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))
#+end_src

** Echo bot
Source: [[./examples/echo_bot.clj][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.

#+begin_src clojure
(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))
#+end_src

** Simple command bot
Source: [[./examples/simple_command_bot.clj][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:

#+begin_src clojure
(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))
#+end_src

* Handlers
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:

#+begin_src clojure
{:type :message
:filter #"ping"
:actions [{:reply-text {:text "pong"}}]}
#+end_src
** Required keys
*** :type
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.

*** :filter
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.

*** :actions
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.

** Optional keys
*** :user
Additional filter that check the ~:ctb/user~ map produced by [[#auth-middleware][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][examples/rights_checker_command_bot.clj]]

*** :passthrough
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.

*** :doc
Documentation describing this handler.

* Middleware
When we build a simple REST API we work with requests. In Clojure they're normally just a map, usually conforming to [[https://github.com/ring-clojure/ring][ring]] spec. This approach proved to be amazingly productive, allowing different server and client libraries to interact by conforming to the ring standard.

Telegram [[https://core.telegram.org/bots/api#update][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.

** Example middleware
*** Ping-Pong bot with middleware
Source: [[./examples/ping_pong_middleware_bot.clj][examples/ping_pong_middleware_bot.clj]]m

Here's and example of a modified ping-pong bot that also logs and saves every incoming update:

#+begin_src clojure
(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))
#+end_src

** Included middleware
For your convenience *clj-telegram-bot* comes with some helpers to create often used middleware.

*** Auth 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][:user]] handler key to check if the user has the rights to access this handler.

#+begin_src clojure
(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]))
#+end_src

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][examples/rights_checker_command_bot.clj]]

* Usage
** Creating context
*** From bot token
If you need information about creating bots and getting a token, read [[https://core.telegram.org/bots/api#authorizing-your-bot][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.

#+begin_src clojure
(require '[telegram.core :as t])

(def telegram (t/from-token "YOUR_TOKEN"))
#+end_src

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:

*** From environment variables
Very popular and useful if deploying to services like Heroku. Set an environment variable ~BOT_TOKEN~ to use it:

#+begin_src clojure
(def telegram (t/from-env))
#+end_src

*** From password managers
Another way is to get your token from password and secrets managers. Two are supported out of the box: [[https://www.passwordstore.org/][pass]] and [[https://developer.1password.com/docs/cli/][1Password CLI]].

**** pass
Normally you would use pass from command line like this:

#+begin_src bash
pass my-t/token
#+end_src

So for example above the usage would be:

#+begin_src clojure
(def telegram (t/from-pass "my-t/token"))
#+end_src

**** 1Password
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:

#+begin_src bash
op item get "ITEM_ID" --fields "FIELD_NAME"
#+end_src

So the corresponding code is:

#+begin_src clojure
(def telegram (t/from-op "ITEM_ID" "FIELD_NAME"))
#+end_src

*** From an arbitrary function
You can also initiate the config by passing an arbitrary function that takes no arguments and returns a string with bot token in it:

#+begin_src clojure
(defn my-token-getter []
;; some magical code that gets the token
)

(def telegram (t/from-fn my-token-getter))
#+end_src