{"id":18607866,"url":"https://github.com/licht1stein/clj-telegram-bot","last_synced_at":"2025-04-10T21:31:56.759Z","repository":{"id":60657601,"uuid":"537060907","full_name":"licht1stein/clj-telegram-bot","owner":"licht1stein","description":"Data driven Clojure bot library","archived":false,"fork":false,"pushed_at":"2023-03-20T10:02:06.000Z","size":90,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-25T04:37:07.597Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/licht1stein.png","metadata":{"files":{"readme":"README.org","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-09-15T14:19:31.000Z","updated_at":"2024-04-17T22:22:44.000Z","dependencies_parsed_at":"2024-11-07T02:37:44.098Z","dependency_job_id":"2e076c8d-6068-4223-97ab-20e7caddb987","html_url":"https://github.com/licht1stein/clj-telegram-bot","commit_stats":{"total_commits":69,"total_committers":3,"mean_commits":23.0,"dds":0.04347826086956519,"last_synced_commit":"bacc02c1e0bd402346c39fafd2195feb874bf59e"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/licht1stein%2Fclj-telegram-bot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/licht1stein%2Fclj-telegram-bot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/licht1stein%2Fclj-telegram-bot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/licht1stein%2Fclj-telegram-bot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/licht1stein","download_url":"https://codeload.github.com/licht1stein/clj-telegram-bot/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248301534,"owners_count":21080908,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-07T02:32:55.799Z","updated_at":"2025-04-10T21:31:51.705Z","avatar_url":"https://github.com/licht1stein.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"#+TITLE: Clojure Telegram Bot\nData driven Clojure bot library.\n\n* Warning!\n*This library is under active development. APIs will probably change. Feel free to play with it though, it's pretty usable by now.*\n\nIf 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.\n\n* Motivation\n\nThis 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.\n\nThat's the goal.\n\n* Table of contents                                                     :toc_4:\n- [[#warning][Warning!]]\n- [[#motivation][Motivation]]\n- [[#installation][Installation]]\n  - [[#using-depsedn][Using deps.edn]]\n  - [[#using-lein][Using lein]]\n- [[#examples][Examples]]\n  - [[#pingpong-bot][Ping/pong bot]]\n  - [[#echo-bot][Echo bot]]\n  - [[#simple-command-bot][Simple command bot]]\n- [[#handlers][Handlers]]\n  - [[#required-keys][Required keys]]\n    - [[#type][:type]]\n    - [[#filter][:filter]]\n    - [[#actions][:actions]]\n  - [[#optional-keys][Optional keys]]\n    - [[#user][:user]]\n    - [[#passthrough][:passthrough]]\n    - [[#doc][:doc]]\n- [[#middleware][Middleware]]\n  - [[#example-middleware][Example middleware]]\n    - [[#ping-pong-bot-with-middleware][Ping-Pong bot with middleware]]\n  - [[#included-middleware][Included middleware]]\n    - [[#auth-middleware][Auth middleware]]\n- [[#usage][Usage]]\n  - [[#creating-context][Creating context]]\n    - [[#from-bot-token][From bot token]]\n    - [[#from-environment-variables][From environment variables]]\n    - [[#from-password-managers][From password managers]]\n      - [[#pass][pass]]\n      - [[#1password][1Password]]\n    - [[#from-an-arbitrary-function][From an arbitrary function]]\n\n* Installation\nYou can install the library from [[https://clojars.org/com.github.licht1stein/clj-telegram-bot][Clojars]]:\n\n** Using deps.edn\n#+begin_src clojure\n  com.github.licht1stein/clj-telegram-bot {:mvn/version \"0.1\"}\n#+end_src\n\n** Using lein\n#+begin_src clojure\n  [com.github.licht1stein/clj-telegram-bot \"0.1\"]\n#+end_src\n\n* Examples\nI 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:\n\n** Ping/pong bot\nSource: [[./examples/ping_pong_bot.clj][examples/ping_pong_bot.clj]]\n\nA 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.\n\n#+begin_src clojure\n  (ns ping-pong-bot\n    (:require [telegram.core :as t]\n              [telegram.bot.dispatcher :as t.d]))\n\n  (def *ctx (t/from-token \"YOUR_BOT_TOKEN\"))\n\n  (def handlers\n    [{:type :message\n      :filter #\"ping\"\n      :actions [{:reply-text {:text \"pong\"}}]}])\n\n  (def dispatcher (t.d/make-dispatcher *ctx handlers))\n  (def updater (t/start-polling *ctx dispatcher))\n\n  (comment\n    \"Run this to stop long-polling updater\"\n    (t/stop-polling updater))\n#+end_src\n\n** Echo bot\nSource: [[./examples/echo_bot.clj][examples/echo_bot.clj]]\n\nClassical example of a bot that responds with the same text user sent. Note the ~:any~ filter, it will return true to every message.\n\n#+begin_src clojure\n  (ns echo-bot\n    (:require[telegram.core :as t]\n             [telegram.updates :as t.u]   ; update helpers\n             [telegram.bot.dispatcher :as t.d]))\n\n  (def *ctx (t/from-token \"YOUR_BOT_TOKEN\"))\n\n  (def handlers\n    [{:type :message\n      :filter :any\n      :actions [(fn [upd ctx] {:reply-text {:text (t.u/message-text? upd)}})]}])\n\n  (def dispatcher (t.d/make-dispatcher *ctx handlers))\n  (def updater (t/start-polling *ctx dispatcher))\n\n  (comment\n    (t/stop-polling updater))\n#+end_src\n\n** Simple command bot\nSource: [[./examples/simple_command_bot.clj][examples/simple_command_bot.clj]]\n\nAnother 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:\n\n#+begin_src clojure\n  (ns simple-command-bot\n    (:require[telegram.core :as t]\n             [telegram.updates :as t.u]   ; update helpers\n             [telegram.bot.dispatcher :as t.d]))\n\n  (def *ctx (t/from-token \"YOUR_BOT_TOKEN\"))\n\n  (def handlers\n    [{:type :command\n      :filter \"/start\"\n      :actions [{:reply-text {:text \"You called the /start command\"}}]}\n\n     {:type :command\n      :filter #\"/help\"\n      :actions [{:reply-text {:text \"This bot does nothing useful\"}}]}\n\n     {:type :command\n      :filter (fn [upd ctx] (= (t.u/message-text? upd) \"/fn_command\"))\n      :actions [{:reply-text {:text \"Note that you can use functions for :filter and :actions for more complex filtering and action logic\"}}]}])\n\n  (def dispatcher (t.d/make-dispatcher *ctx handlers))\n  (def updater (t/start-polling *ctx dispatcher))\n\n  (comment\n    (t/stop-polling updater))\n#+end_src\n\n* Handlers\nWhen 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~.\n\nLet's take a look at the handler we used for our ping-pong bot example:\n\n#+begin_src clojure\n  {:type :message\n   :filter #\"ping\"\n   :actions [{:reply-text {:text \"pong\"}}]}\n#+end_src\n** Required keys\n*** :type\nThis 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.\n\nOnce 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.\n\n*** :filter\nThe 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.\n\nYou can also provide a ~(fn [upd ctx])~ function as a filter to implement logic of any complexity.\n\nDispatcher 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.\n\n*** :actions\nVector 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.\n\nAction 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).\n\nYou can provide multiple actions for a single handler to allow triggering multiple actions by a single update.\n\n** Optional keys\n*** :user\nAdditional 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.\n\nFor a complete example see [[./examples/rights_checker_command_bot.clj][examples/rights_checker_command_bot.clj]]\n\n*** :passthrough\nIf 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.\n\n*** :doc\nDocumentation describing this handler.\n\n* Middleware\nWhen 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.\n\nTelegram [[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.\n\nAny 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.\n\nSo 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.\n\n** Example middleware\n*** Ping-Pong bot with middleware\nSource: [[./examples/ping_pong_middleware_bot.clj][examples/ping_pong_middleware_bot.clj]]m\n\nHere's and example of a modified ping-pong bot that also logs and saves every incoming update:\n\n#+begin_src clojure\n  (ns ping-pong-middleware-bot\n    (:require [telegram.core :as t]\n              [telegram.bot.dispatcher :as t.d]))\n\n  (def *ctx (t/from-token \"YOUR_BOT_TOKEN\"))\n\n  (def handlers\n    [{:type :message\n      :filter #\"ping\"\n      :actions [{:reply-text {:text \"pong\"}}]}])\n\n  (defn log-update [upd ctx]\n    (println upd)\n    upd)\n\n  (defn spit-update [upd ctx]\n    (spit \"last-update.edn\" upd)\n    upd)\n\n  (def dispatcher (t.d/make-dispatcher *ctx handlers :update-middleware [spit-update log-update]))\n  (def updater (t/start-polling *ctx dispatcher))\n\n  (comment\n    (t/stop-polling updater))\n#+end_src\n\n** Included middleware\nFor your convenience *clj-telegram-bot* comes with some helpers to create often used middleware.\n\n*** Auth middleware\nOne 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.\n\nThe ~: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.\n\n#+begin_src clojure\n  (ns auth-middleware\n    (:require [telegram.middleware.auth :as t.auth]))\n\n  (def user-db\n    \"This is a simple example of some sort of database that stores user information.\"\n    {1234567 {:user \"Owner\"\n              :admin? true}})\n\n  (defn user-auth\n    \"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.\"\n    [telegram-id]\n    (user-db telegram-id))\n\n  (def auth-middleware\n    \"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.\"\n    (t.auth/make-auth-middleware user-auth))\n\n  ;; Now we can add the middleware when instantiating our dispatcher.\n  (def dispatcher (t.d/make-dispatcher *ctx handlers :update-middleware [auth-middleware]))\n#+end_src\n\nFor 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]]\n\n* Usage\n** Creating context\n*** From bot token\nIf 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]].\n\nFirst 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.\n\n#+begin_src clojure\n  (require '[telegram.core :as t])\n\n  (def telegram (t/from-token \"YOUR_TOKEN\"))\n#+end_src\n\nHowever 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:\n\n*** From environment variables\nVery popular and useful if deploying to services like Heroku. Set an environment variable ~BOT_TOKEN~ to use it:\n\n#+begin_src clojure\n  (def telegram (t/from-env))\n#+end_src\n\n*** From password managers\nAnother 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]].\n\n**** pass\nNormally you would use pass from command line like this:\n\n#+begin_src bash\n  pass my-t/token\n#+end_src\n\nSo for example above the usage would be:\n\n#+begin_src clojure\n  (def telegram (t/from-pass \"my-t/token\"))\n#+end_src\n\n**** 1Password\nFor 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:\n\n#+begin_src bash\n  op item get \"ITEM_ID\" --fields \"FIELD_NAME\"\n#+end_src\n\nSo the corresponding code is:\n\n#+begin_src clojure\n  (def telegram (t/from-op \"ITEM_ID\" \"FIELD_NAME\"))\n#+end_src\n\n*** From an arbitrary function\nYou can also initiate the config by passing an arbitrary function that takes no arguments and returns a string with bot token in it:\n\n#+begin_src clojure\n  (defn my-token-getter []\n    ;; some magical code that gets the token\n    )\n\n  (def telegram (t/from-fn my-token-getter))\n#+end_src\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flicht1stein%2Fclj-telegram-bot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flicht1stein%2Fclj-telegram-bot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flicht1stein%2Fclj-telegram-bot/lists"}