I was running some unattended tasks on my computer and wanted a quick and easy way to monitor the state of the tasks and other parameters while away from my local network. I have the option of tunneling to my local network via a VPN and then using a SSH session to view my computer remotely, however, I wanted the notifications to be convenient oon the go. I was at first thinking about using something such as a self-hosted ntfy server or even integrate with a home-assistant instance I have running to push notifications to my phone, but why make it so complicated when I already have messaging apps installed?

I decided to use Telegram as my messaging app of choice, as I had a little experience with it in the past using Python and the python-telegram-bot library. After looking at the Telegram API documentation, I decided to use a simple bash script and curl (which I install by default on any machine and container I own) and not worry about installing Python and other dependencies—using the HTTP API directly. I can do this since I’m only sending notifications from the bot to known users and ignoring any messages that are sent to the bot. Using bash also makes the script very portable, and I don’t need to worry about all the shenanigans of the Python library version changes and the async issues that come with it.

I started by creating a new bot using Telegram’s @BotFather. It’s as simple as sending the command /newbot and giving it a name and a username (the username must end with bot; follow the prompts). Once the bot is created, BotFather will provide an access token for the HTTP API.

Next, I had to figure out how to send a message to myself using the bot. Telegram bots cannot start a conversation with a user; the user needs to start the conversation, and the bot can reply from there to that chat. Since I could reply to anyone that starts a chat with my bot, there was a risk that anyone could get updates on my tasks, potentially leading to sensitive information leaks. The solution is simple: start a conversation with the bot, get my own user ID, and then only send messages to my user ID. This way, no matter who interacts with the bot, only I would receive updates.

I searched for the bot in Telegram using the username and sent it a test message. Then, using the URL https://api.telegram.org/bot<API-ACCESS-TOKEN>/getUpdates, I was able to view the message I sent and find my user ID.

{
  "ok": true,
  "result": [
    {
      "update_id": XXXXXXXX,
      "message": {
        "message_id": XXXXXXXX,
        "from": {
          "id": XXXXXXXX, // <- Here is my user ID
          "is_bot": false,
          "first_name": "XXXXXXXX",
          "username": "XXXXXXXX",
          "language_code": "XXXXXXXX"
        },
        "chat": {
          "id": XXXXXXXX,
          "first_name": "XXXXXXXX",
          "username": "XXXXXXXX",
          "type": "XXXXXXXX"
        },
        "date": XXXXXXXX,
        "text": "Test" // <- Here is the message I sent
      }
    }
  ]
}

Once I had the access token and my user ID, I tested to see if I could send myself messages. To send a message, a POST request needs to be sent to https://api.telegram.org/bot<API-ACCESS-TOKEN>/sendMessage. There are two parameters that need to be added. The first is chat_id, which we found earlier—this is the user to whom the message will be sent. The second is text, which is the content of the message. There is also a third parameter that I tested—disable_notification=<true/false>. When this parameter is set to true, the Telegram notification on the phone will not cause the vibrate/ring to trigger, which is more convenient for non-critical informational messages.

Here is the full curl command to send a silent message:

curl -s -X POST "https://api.telegram.org/bot<API-ACCESS-TOKEN>/sendMessage" -d "chat_id=XXXXXXXX" -d "text=Hello World" -d disable_notification=true

The -s is to silence curl itself, -X POST is to set the HTTP request type to POST, and -d is used to set the request parameters.

Once I had it all figured out, I decided to also add some severity levels for my messages, so info and success messages would not distract me. I also added a small emoji so the message severity is easily identifiable. To ensure that I can view messages that failed to send—possibly due to Telegram downtime, an internet connection error, or other issues—I also log the message (without the emoji) to the local journal. In the end, putting this all together, I created the following crude script that has been working just fine, the script takes 3 parameters, a severity level, a title for the first line and the message itself (the title is useful to note the source of the message):

#!/bin/bash
set -Eeuo pipefail

# Telegram bot token
TOKEN="XXXXXXXX" 
# Chat ID where you want to send notifications
CHAT_ID="XXXXXXXX"

send_telegram_message() {
    local level=$1
    local title=$2
    local body=$3

    # Define emoji circle colors based on level
    case $level in
        info)
            emoji="🔵"
            ;;
        success)
            emoji="🟢"
            ;;
        warning)
            emoji="🟡"
            ;;
        error)
            emoji="🔴"
            ;;
        *)
            emoji="❔"
            ;;
    esac

    local message="$emoji $title
$body"

    local silent=""
    if [[ "$level" == "info" || "$level" == "sucess" ]]; then
        silent="-d disable_notification=true"
    fi

    # Send message to Telegram
    curl -s -X POST "https://api.telegram.org/bot$TOKEN/sendMessage" -d "chat_id=$CHAT_ID" -d "text=$message" $silent > /dev/null

    # Log the message without emoji
    logger -t telesend "$level: $title - $body"
}

send_telegram_message "$1" "$2" "$3"

I put the script as telesend under /usr/local/bin, so I can call it from anywhere. For example, I added it to my script that runs when smartmontools detects an anomaly. I simply added the following line at the end of the script called by smartd - /usr/local/bin/telesend error "smartd: $SMARTD_FAILTYPE" "$SMARTD_FULLMESSAGE"

smartd notification on telegram

I also added a script under /etc/zfs/zed.d/ that executes /usr/local/bin/telesend.sh "info" "Scrub finished" "$(zpool status $ZEVENT_POOL)" This sends me an informational message with the ZFS scrub results (this can be done better to properly use the severity levels depending on the results).

ZFS scrub notification on telegram
ZFS scrub notification in the journal

Yes, this script can be improved, but for a few minutes work, (shorter than writing this post) I can have low disk space notifications, backup failures and statuses, SSH logins, temperature warnings, fail2ban status, and who knows what else sent directly to my phone, all with a tiny bash script or curl call.