Skip to content

licht1stein/clj-telegram-bot

Repository files navigation

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 Telegram channel. I will change the public APIs many times before release.

Motivation

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.

Table of contents

Installation

You can install the library from Clojars:

Using deps.edn

com.github.licht1stein/clj-telegram-bot {:mvn/version "0.1"}

Using lein

[com.github.licht1stein/clj-telegram-bot "0.1"]

Examples

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:

Ping/pong bot

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))

Echo bot

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))

Simple command bot

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))

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:

{:type :message
 :filter #"ping"
 :actions [{:reply-text {:text "pong"}}]}

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 to see if the user has the right to access this handler.

For a complete example see 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 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.

Example middleware

Ping-Pong bot with middleware

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))

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 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

Usage

Creating context

From bot token

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:

From environment variables

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

(def telegram (t/from-env))

From password managers

Another way is to get your token from password and secrets managers. Two are supported out of the box: pass and 1Password CLI.

pass

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"))

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:

op item get "ITEM_ID" --fields "FIELD_NAME"

So the corresponding code is:

(def telegram (t/from-op "ITEM_ID" "FIELD_NAME"))

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:

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

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

About

Data driven Clojure bot library

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published