Writing a telegram bot in Clojure
goal
You would like to create a telegram bot, that can be invited to channels and chats and can search and answer something for you … This post is for you.
The plan of today is to get through all the steps to get this working properly by yourself.
This bot will be done using a webhook, which is a little bit harder to setup than regular polling due to its use of https, so yes this will be covered as well.
Ask the telegram botfather to create a new bot for you.
This is done from telegram itself. And those are the steps:
- send a message to botfather @BotFather
- check help with /help
- create new bot with /newbot
Give it a name, should end in bot, so you can
Name: fruits_ai_bot
Token: 404849973:AAGuyH5NjKh05t-9FqEPx_2UlfHe2O2lvds
URL: t.me/fruits_ai_bot
Your bot can now accept payments for goods and services via Telegram. :)
set up the bot to use a certificate
The original morse library does not support setting self-signed certificates to telegram, so we will do that using curl. Basically a simple http request to the telegram api of your bot, to let it know your custom certificate when doing the SSL handshake. Token is the token received when you have setup your own bot with botfather.
curl -F "url=https://hellonico.tokyo/handler" \
-F "certificate=@/home/niko/timebot/ssl/YOURPUBLIC.pem" \
https://api.telegram.org/bot<token>/setWebhook
bot settings: join groups and settings
Two more settings need to be done with the botfather, so your bot is available for groups, and can also read messages.
First the join group settings:
Second the privacy settings:
Create a new Clojure project template with Leiningen
Leiningen will be used to generate the clojure project template:
lein new timebot
The rest is fun coding…
update the project.clj file
Your bot will be using a set of common and not-so common Clojure libraries. In the original example, the morse library was used for handlers, but it does not
(defproject timebot "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:main timebot.core
:profiles {:uberjar {:aot :all}}
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:repositories {"hellonico" {:sign-releases false :url "http://hellonico.tokyo:8081/repository/hellonico/"}}
:dependencies [
[org.clojure/clojure "1.8.0"]
[org.hellonico/morse "0.2.5"]
[org.hellonico/jakaroma "0.2"]
[compojure "1.6.0"]
[ring/ring-json "0.4.0"]
[http-kit "2.2.0"]
[javax.servlet/servlet-api "2.5"] ])
write some clojure code using morse
The bot will simply talk about the current time, when a message is sent to him, either from a direct chat, or from a channel; indeed the bot can also be added to a channel and read channel messages with the settings you have done before.
Settings are kept in a separate file, settings.clj, so you can customize important details outside the app.
{
:token "400083184:AAH6njcJv9iZtRnaE-Kv9-hCus2XSh0eROA"
:date-format "MM-dd-yyyy (HH:mm:ss)"
:not-found-message "Not Found"
:starting-message "Starting time bot ..."
:port "9090"
}
Those settings will be slurped after from the main Clojure namespace. Simple sending of text can be see using the morse library, where the token is your bot’s token, and the chatid can be retrieved from the message, (or some previous chats…)
(t/send-text
token
chatid
"some text")
Routing is done through some simple compojure routes, where mostly only the body of the request is of interest in this example:
(defroutes app-routes
(POST "/debug" {body :body} (clojure.pprint/pprint body))
(POST "/handler" {body :body} (timebot-api body))
(route/not-found (settings :not-found-message)))
For convenience json messages are wrapped for you to clojure key-ified collections using wrap-json-body:
(def app
(-> (handler/site app-routes)
(wrap-json-body {:keywords? true})))
The full Clojure code listing is shown below:
(ns timebot.core
(:use [ring.adapter.jetty])
(:import [fr.free.nrw.jakaroma Jakaroma])
(:require
[cheshire.core :refer :all]
[ring.middleware.json :refer [wrap-json-body]]
[compojure.core :refer :all]
[compojure.route :as route]
[compojure.handler :only [site] :as handler]
[org.httpkit.server :as httpkit]
[morse.api :as t])
(:gen-class))
(def settings
(read-string (slurp "settings.clj")))
(def token
(settings :token))
(defn what-time-is-it-now[]
(.format
(java.text.SimpleDateFormat. (settings :date-format))
(java.util.Date.)))
(def not-nil? (complement nil?))
(defn handle-channel-post[channel_post]
(let [chatid (-> channel_post :chat :id) ]
(t/send-text
token
chatid
(what-time-is-it-now))))
(defn handle-message[message]
(let [chatid (-> message :chat :id) ]
(t/send-text
token
chatid
(what-time-is-it-now))))
(defn timebot-api [ {message :message, channel_post :channel_post, :as all}]
(println (generate-string all))
(if (not-nil? message)
(handle-message message))
(if (not-nil? channel_post)
(handle-channel-post channel_post))
{:status 200 :headers {}})
(defroutes app-routes
(POST "/debug" {body :body} (clojure.pprint/pprint body))
(POST "/handler" {body :body} (timebot-api body))
(route/not-found (settings :not-found-message)))
(def app
(-> (handler/site app-routes)
(wrap-json-body {:keywords? true})))
(defn -main[& args]
(clojure.pprint/pprint settings)
(httpkit/run-server (handler/site #'app) {:port (Integer/parseInt (settings :port))})
(println (settings :starting-message))
(Thread/sleep Long/MAX_VALUE))
run the bot
Running the bot, can be done with the run command of leiningen:
lein run
or obviously through the uberjar path.
lein uberjar
java -jar target/timebot-standalone*.jar
telegram message format
Message coming to a private chat have the json format below, the top interesting element is message
{:update_id 571443652,
:message
{:message_id 87,
:from
{:id 121843071,
:first_name "Nico",
:last_name "Nico",
:username "hellonico",
:language_code "en"},
:chat
{:id 121843071,
:first_name "Nico",
:last_name "Nico",
:username "hellonico",
:type "private"},
:date 1498630563,
:text "hello"}}
While message coming through a channel are slightly different, with the top interesting element being channel_post
{:update_id 571443636,
:channel_post
{:message_id 13,
:from
{:id 121843071,
:first_name "Nico",
:last_name "Nico",
:username "hellonico",
:language_code "en"},
:chat {:id -1001064008907, :title "hello", :type "channel"},
:date 1498628307,
:text "oui bonjour"}}
A mention of the bot’s name in the message has one more extra field:
{:update_id 571443635,
:channel_post
{:message_id 12,
:from
{:id 121843071,
:first_name "Nico",
:last_name "Nico",
:username "hellonico",
:language_code "en"},
:chat {:id -1001064008907, :title "hello", :type "channel"},
:date 1498628258,
:text "@myowntime_bot hello super news",
:entities [{:type "mention", :offset 0, :length 14}]}}
replay messages with curl
Messages can be replayed using standard curl on your https handler. Note the usage of the curl parameter -k so that curl acknowledge your self-signed certificate. You can of course remove it if you have real ones.
echo '{"update_id":571443653,"message":{"message_id":89,"from":{"id":121843071,"first_name":"Nico","last_name":"Nico","username":"hellonico","language_code":"en"},"chat":{"id":121843071,"first_name":"Nico","last_name":"Nico","username":"hellonico","type":"private"},"date":1498630765,"text":"hello"}}' | curl -k -H "Content-Type: application/json" -X POST "https://hellonico.tokyo/handler" -T -