Hosting a Telegram Bot on FreeBSD
2020-08-26Let's host a Telegram bot. Initially, the endeavor looks quite challenging, but it's actually a lot simpler than I thought.
Environment
To start, I'm creating a new jail. This is not a necessary or required step, but I like to separate things that have no business being connected.
Like many FreeBSD users, I manage my jails using iocage
:
$ sudo iocage create -r 12.1-RELEASE -n bot
In this case, I am using a bot that simply polls the Telegram servers, so no port redirects are necessary. Letting the jail connect to the internet is enough, which I am using my firewall (pf
) for:
/etc/pf.conf (excerpt): ... nat on $ext_if inet from $jail_if:network to any -> $ext_if ...
Now enter the new jail:
$ sudo iocage console -f bot
The bot is also getting its own user account. While it is in a jail, it's never a good idea to run things as root that don't have to. Additionally, I intend to use the same jail for more bots I will host in the future, with each bot running under a different user.
The Bot
Now, with the environment constructed, we can start setting up the bot. In this case, I am using a simple Markov Chain Bot that consists of a single Python script.
After using the official "BotFather" to define a username and obtain an API token, as well as giving that token to the bot (in this case by defining a variable in the Python file), it already works in principle. We can manually execute the script (python3 ./bot.py
), open Telegram and send messages — it should already work.
Control
Of course, we're not finished yet. Manually starting the bot every time the server reboots or the jail is restarted is way too cumbersome. It could be launched by a cronjob (e.g. @reboot python3 ./bot.py
), however that's still a pretty dirty way to do it. For instance, to turn it off or restart it, we'd need to first use ps -aux
to find its PID, then manually kill
the process.
What we need instead is an rc.d script. This will let us start and stop the bot just like other services:
$ service bot start $ service bot stop
Daemonizing
First of all, the bot needs to run as a daemon. That is, once started, it runs in the background instead of blocking the shell it was launched from. If you are hosting a bot that already does this on its own, you can skip this step.
Unfortunately, the bot I am using here does not daemonize itself. Using something like 'python3 ./bot.py > /dev/null &
' to suppress the output and force it into the background would (mostly) work, but it's not a very good or clean solution.
Instead, we'll make use of the daemon(8)
utility included in FreeBSD:
daemon -u markov -p ./bot.pid python3 ./markov_bot.py > /dev/null 2>> ./bot.log
'-u markov'
sets the user the bot runs as (a child process ofdaemon
)'-p ./bot.pid'
creates a pidfile which can later be used to send signals to the bot (e.g.'kill $(cat ./bot.pid)'
).- '
> /dev/null
' swallowsSTDOUT
- '
2>> ./bot.log
' writesSTDERR
to a logfile
rc.d
Having figured out how to daemonize the bot, the above command can be wrapped into an rc.d
script:
#!/bin/sh # PROVIDE: markov_bot 1 # REQUIRE: LOGIN . /etc/rc.subr name="markov_bot" procname="/usr/local/bin/python3"2 pidfile="/var/run/${name}.pid" sig_stop="INT"3 command="/usr/sbin/daemon" command_args="-u markov -p $pidfile4 $procname /bots/markov/markov_bot.py >> /dev/null 2>> /bots/markov/bot.log" markov_bot_chdir="/bots/markov"5 rcvar="${name}_enable"6 load_rc_config $name run_rc_command "$1"
-
rcorder(8)
block:- '
PROVIDE: markov_bot
' defines the name of the "condition" (or "service") thisrc.d
script provides -
'
REQUIRE: LOGIN
' defines the conditions it needs to run. In this case, it will be run at the very end of the init sequence
PROVIDE
' line while 'REQUIRE
' was optional. - '
-
This variable tells the system what kind of process the pidfile points at. By default, it expects the process in
$command
(here:/usr/sbin/daemon
) and will not use the pidfile if it points somewhere else. -
For some reason, this particular bot needs to be stopped by
SIGINT
, it doesn't save its state if terminated by the defaultSIGTERM
. -
Because
daemon(8)
does not forwardSIGINT
, the lower case '-p
' flag is used (instead of '-P
') to create a pidfile for the child process. It will be stopped directly instead of going through thedaemon(8)
process.daemon(8)
itself will stop once the child process exits. -
This bot expects a subdirectory "
markov/
" in its working directory. Settingmarkov_bot_chdir
tellsrc.subr
tocd
there. The name of this variable always needs to be '${name}_chdir
'. -
The name of the variable that will be used to enable the daemon in
/etc/rc.conf
Also have a look at the manpages, especially daemon(8)
and rc.subr(8)
. The latter contains a list of variables that rc.d
scripts can set.
Wrapping up
After writing the rc.d
script, we can test it:
$ ./bot.sh onestart Starting markov_bot. $ ./bot.sh onestatus markov_bot is running as pid 12345. $ ./bot.sh onestop Stopping markov_bot. Waiting for PIDS: 12345.
Having confirmed that it works, the finished script can be moved to /usr/local/etc/rc.d
and activated by setting markov_bot_enable="YES"
in /etc/rc.conf
:
$ mv ./bot.sh /usr/local/etc/rc.d/markov_bot $ sysrc markov_bot_enable="YES"
It can now be controlled with the service(8)
utility:
$ service markov_bot (start|stop|restart|status)