A Telegram captcha bot written in Clojure and compiled with GraalVM native
image. Runs on bare Linux/MacOS with no requirements. Fast and robust.
Telegram chats suffer from spammers who are pretty smart nowadays. They don't
use bots; instead, they register ordinary accounts and automate them with
Selenium + web-version of Telegram. Personally, I found Shieldy and other bots
useless when dealing with such kind of spammers. This project aims the goal to
finish that mess.
Another reason I opened Teleward for is to try my skills in developing Clojure
applications with GraalVM. Binary applications are nice: they are fast, and they
don't need installing JDK. At the same time, they're are still Clojure: REPL is
here, and that's amazing.
- This is Clojure, so you have REPL! During development, you call Telegram API
directly from REPL and see what's going on.
- The bot can be delivered either as a Jar file or a binary file (with Graal).
- When Graal-compiled, needs no requirements (Java SDK, etc). The binary size is
about 30 Mb.
- Supports both long polling and webhook modes to obtain messages.
- Keeps all the state in memory and thus doesn't need any kind of a
database. The only exception is the current offset value which is tracked in a
- Supports English and Russian languages.
- Two captcha styles: normal "1 + 2" and Lisp captcha "(+ 1 2)".
* operators are corresponding Unicode characters that
prevent captcha from naive evaluation.
The bot listens for all the messages in a group. Once a new pack of messages
arrives, the bot applies the following procedure to each message:
- Mark new members as locked.
- Send a captcha message to all members.
- Unless an author of a message is locked, delete that message.
- If a message is short and matches the captcha's solution, unlock a user and
delete the catpcha message.
- If a locked user has posted three messages with no solution, ban them.
- If a locked user hasn't solved captcha in time, ban them as well.
Please note: the bot processes only messages no older than two minutes from
now. In other words, the bot is interested in what is happening now (with a
slight delay), but not in the far past. This is to prevent a sutuation what a
bot had been inactive and then has started to consume messages. Without this
condition, it will send captcha to chat members who have already joined and
To make a Jar artefact, run:
uberjar target calls
lein uberjar and also injects the
into it. The output file is
Linux version is built inside a Docker image, namely the
ghcr.io/graalvm/graalvm-ce one with
native-image extension preinstalled. Run
the following command:
The output binary file appears at
gu install native-image
The output will be
To run the bot, first you need a token. Contact
@BotFather in Telegram to
create a new bot. Copy the token and don't share it.
Add your new bot into a Telegram group. Promote it to admins. At least the bot
must be able to 1) send messages, 2) delete messages, and 3) ban users.
Run the bot locally:
teleward -t <telegram-token> -l debug
If everything is fine, the bot will start consuming messages and print them in
See the version with
-v, and help with
-h. The bot takes into account plenty
of settings, yet not all of them are available for configuration for now. Below,
we name the most important parameters you will need.
-t, --telegram.token: the Telegram token you obtain from
BotFather. Required, can be set via an env variable
-m, --mode: Working mode. Either
webhook, default is polling.
--webhook.path: Webhook path, default is
--webhook.server.host: Hostname of the webhook server, default is
-p, --webhook.server.port: Port to listen in webhook mode, default is 8090.
-l, --logging.level: the logging level. Can be
debug, info, error. Default
info. In production, most likely you will set
--telegram.offset-file: where to store offset number for the next
getUpdates call. Default is
TELEGRAM_OFFSET in the current working
--language: the language for messages. Can be
en, ru, default is
--captcha.style: a type of captcha. When
lisp, the captcha looks like
(+ 4 3). Any other value type will produce
4 + 3. The operator is taken
./target/teleward -t <...> -l debug \
--language=en --telegram.offset-file=mybot.offset \
For the rest of the config, see the
Under the hood, Teleward uses Cprop for configuration. This library
takes into account env vars to override default values. Set the
variable to see the log of configuration startup.
sudo useradd -s /bin/bash -d /home/ivan/ -m -G sudo ivan
sudo passwd ivan
- Compile the file locally and copy it to the machine:
scp ./builds/teleward-Linux-x86_64 ivan@hostname:/home/ivan/teleward/
- Create a new
sudo mcedit /etc/systemd/system/teleward.service
- Paste the following config:
Description = Teleward bot
After = network.target
Type = simple
Restart = always
RestartSec = 1
User = ivan
WorkingDirectory = /home/ivan/teleward/
ExecStart = /home/ivan/teleward/teleward-Linux-x86_64 -l debug
Environment = TELEGRAM__TOKEN=xxxxxxxxxxxxxx
WantedBy = multi-user.target
sudo systemctl enable teleward
- Manage the service with commands:
sudo systemctl stop teleward
sudo systemctl start teleward
sudo systemctl status teleward
For Jar, the config file would be almost the same except the
section. There, you specify something like
java -jar teleward.jar ....
teleward.service file, specify the
-m webhook parameter:
ExecStart = .../teleward-Linux-x86_64 -m webhook -p 8090 ...
Install Caddy server for SSL. Modify its service config:
# sudo mcedit /lib/systemd/system/caddy.service
ExecStart=caddy reverse-proxy --from <DOMAIN> --to localhost:8090
/conf directory for configuration.
Compile uberjar with with a special profile:
In Dynamo DB or Yandex Db, create a table with
(chat_id, user_id) pair for the
primary key (both integers).
Zip and upload this jar into S3/YC bucket. Create a lambda/function with these
|bucket||the name of the bucket|
|object||path to the zip file|
|timeout||minimum 5 seconds|
|memory||128 is enough|
Setup the env vars:
|your telegram token|
|table to store the state|
|aws public key|
|aws secret key|
|HTTPS URL to DynamoDB/YDB|
Make you lambda/function public. Use its URL as a webhook for your bot.
The bot accepts the
/health command which it replies to "OK".
- Add tests.
- Report uptime for
- More config parameters via CLI args.
- Better config handling.
- Widnows build.
© 2022 Ivan Grishaev