🍄Check Website Availability

I have several websites that I host on my server. Sometimes I make configuration changes and sometimes it breaks things. I'd like to have a basic safeguard against breaking a website. Ensuring that the home page is available is enough for me.

I decided to re-learn CHICKEN Scheme for a change and made a script that solves my problem. It checks for a list of websites and sends me a Telegram notification: silent one if everything is okay and a normal one if something is broken.

Telegram setup

You should have a Telegram account. Then follow instructions on how to create a Telegram bot. You should obtain a token and a link to your bot.

Next open the chat with a bot and send it any message. We need it to get the chat ID. Send an HTTP GET request to get updates (replace AAAAAAAAA with your token):

curl 'https://api.telegram.org/botAAAAAAAAA/getUpdates'

The result is something like this:

{
  "ok": true,
  "result": [
    {
      "update_id": 969242700,
      "message": {
        "message_id": 1,
        "from": {
          "id": 1111111,
          "is_bot": false,
          "first_name": "Yourname",
          "last_name": "Yoursurname",
          "username": "yourusername",
          "language_code": "en",
          "is_premium": true
        },
        "chat": {
          "id": 2222222,
          "first_name": "Yourname",
          "last_name": "Yoursurname",
          "username": "yourusername",
          "type": "private"
        },
        "date": 1717751320,
        "text": "/start",
        "entities": [
          {
            "offset": 0,
            "length": 6,
            "type": "bot_command"
          }
        ]
      }
    }
  ]
}

Find the chat ID in result[0]["chat"]["id"], here it is 2222222.

Now save them both to a file, I have it in TelegramSecrets.rakumod for loading later in a Raku script:

unit module TelegramSecrets;
our $secret-chat-id = 270617458;
our $secret-token = "7421804383:AAHs9x3aRKMCTRmI7Ubs-Q6PPNvw36UupMo";

Also a good idea is to set stricter permissions for this file:

chmod 600 TelegramSecrets.rakumod

The Script

This is a script in Raku. I have tried writing it in several other languages, with comments:

  • CHICKEN Scheme – I got it too verbose with too much abstraction. This is in part because it was my first try of a Scheme in a while, and partly because of weird functional APIs Scheme libraries provide. Scheme is functional after all, I might have not had enough time to adjust my thinking.

  • Common Lisp – this was my first program ever, but it was pretty concise and pretty easy to develop. I can attribute this easiness to the lack of the design stage, I previously had already explored the code with Scheme. Also the programming is less alien than in Scheme. It also turned out that "scripting" in Common Lisp is a somewhat controversial topic. There's Roswell, an attempt at improvement, but it is controversial too. It seems that people don't really do "scripting" as usual: running little script programs, instead they load the whole Lisp file and do the scripting in a REPL. Also running a script in Roswell feels the same as running with sbcl --script and Roswell creates an additional abstraction layer. I think I need some more time to evaluate the whole situation, it is really complicated in the Common Lisp land.

  • Raku – this is my last and current attempt at scripting. Raku seems as a language that should be ideal for this situation, it is literally made for scripting. Since I have already spent about a month learning it, copying a program from Common Lisp seemed easy. As for the code expressiveness, I still think that the Common Lisp version is better because it is more functional.

I think I want to explore the Gauche Scheme implementation because it is made specifically for scripting, like Raku. And I want to dive more into Common Lisp to better understand its viability for scripting.

Here is the code in Raku:

#!/bin/env raku

# Monitor several websites for availability sending them GET requests to the
# home page, not tolerating redirects. Send a silent message via Telegram bot
# if everything is okay. Send a message with a notification if something
# broke.

use v6.d;

need IO::Socket::SSL;
use HTTP::Tiny;
use JSON::Fast;

# Contains secret data, like this:
# unit module TelegramSecrets;
# our $secret-chat-id = 111111111;
# our $secret-token = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
use lib $*PROGRAM.dirname;
use TelegramSecrets;

my @websites =
    "https://greenfork.me",
    "https://garden.greenfork.me",
    "https://links.greenfork.me",
    "https://fossil.greenfork.me",
    "https://dmitryxsabina.ru",
    "https://sabinahelp.ru",
    ;

my $http = HTTP::Tiny.new(:max-redirect(0));

sub check-website($website) {
    say "Checking $website...";
    my $response = $http.get($website);
    if $response<success> {
        return "$website: ok", True;
    } else {
        return "$website: code $response<status>, {$response<content>.decode}", False;
    }
}

constant base-uri = "https://api.telegram.org/bot";
my $chat-id = $TelegramSecrets::secret-chat-id;  # my private chat-id, got with /getUpdates
my $token = $TelegramSecrets::secret-token;      # see Telegram Bot API docs to get it

sub send-message($message, :$notify) {
    my %msg = chat_id => $chat-id, text => $message,
        disable_notification => not $notify,
        link_preview_options => {is_disabled => True};
    $http.post: "{base-uri}$token/sendMessage",
        :headers(content-type => "application/json")
        :content(to-json %msg);
}

my $text = "";
my $was-error = False;
for @websites -> $website {
    (my $status, my $ok) = check-website($website);
    $text ~= "[ERROR] " if not $ok;
    $text ~= $status ~ "\n";
    $was-error or= not $ok;
}
$text = "ERROR!\n" ~ $text if $was-error;

send-message($text, :notify($was-error));

Schedule

I will use a standard facility on Arch Linux (same as Server Backup Of OpenBSD) – systemd's timers! I created a directory at /var/check-websites and put there both files, the script and the secret file:

sudo mkdir /var/check-websites
sudo chown grfork:grfork /var/check-websites
cd /var/check-websites
# copy files here

Then I have a timer that schedules the service to start twice a day – at 9am and at 6pm /etc/systemd/system/backup.timer:

[Unit]
Description=Check websites availability

[Timer]
OnCalendar=*-*-* 09:00:00
OnCalendar=*-*-* 18:00:00
Persistent=true
RandomizedDelaySec=1m

[Install]
WantedBy=timers.target

And a service that starts the script /etc/systemd/system/backup.service:

[Unit]
Description=Check websites availability
After=network.target

[Service]
Type=oneshot
ExecStart=/var/check-websites/check-websites.scm
WorkingDirectory=/var/check-websites
User=grfork
Group=grfork

Start and enable the timer:

systemctl start backup.timer
systemctl enable backup.timer

Check that it is present in the list:

systemctl list-timers

Summary

Now I have more confidence in the availability of my websites. I get a notification twice a day that everything is okay, this is important so that I could also react if the script stops working for some reason. And I also get a loud notification if something is broken so I can quickly fix it.

In addition to the initially stated goal this experience opened some doors for me:

  • I can now write Scheme for useful tasks. I initially preferred Raku for such tasks but there's a question of portability and maturity. Raku is a young language with a long history, so I'm a little bit cautious writing scripts that I want to forget about.

  • Now I have a Telegram bot for notifications and a working API part for sending messages in Scheme, I can integrate more notifications there. I also liked the idea of sending silent notifications if everything is fine, to be sure that the scripts are getting run.