add Telegram notification support (#246)

Thanks to @smkrv in https://github.com/sdr-enthusiasts/docker-planefence/issues/244 for the base code that made the implementation process much easier! 

---------

Co-authored-by: @smkrv
This commit is contained in:
kx1t
2025-06-22 10:11:25 -04:00
committed by GitHub
parent ca9d9d31bc
commit dd18d0cfa9
12 changed files with 294 additions and 14 deletions

BIN
.img/telegram-new-bot.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

View File

@@ -47,7 +47,7 @@ RUN \
#
# Install Planefence (it was copied in with /rootfs, so this is
# mainly moving files to the correct location and creating symlinks):
chmod a+x /usr/share/planefence/*.sh /usr/share/planefence/*.py /usr/share/planefence/*.pl && \
chmod a+x /usr/share/planefence/*.sh /usr/share/planefence/*.py /usr/share/planefence/*.pl /scripts/post2telegram.sh && \
ln -s /usr/share/socket30003/socket30003.cfg /usr/share/planefence/socket30003.cfg && \
ln -s /usr/share/planefence/config_tweeting.sh /root/config_tweeting.sh && \
if curl --compressed --fail -sSL https://raw.githubusercontent.com/kx1t/planefence-airlinecodes/main/airlinecodes.txt > /tmp/airlinecodes.txt; then mv -f /tmp/airlinecodes.txt /usr/share/planefence/airlinecodes.txt; fi && \

71
README-telegram.md Normal file
View File

@@ -0,0 +1,71 @@
# Configure Planefence / Plane-Alert to send notifications to a Telegram Channel
- [Configure Planefence / Plane-Alert to send notifications to a Telegram Channel](#configure-planefence--plane-alert-to-send-notifications-to-a-telegram-channel)
- [Prerequisites](#prerequisites)
- [Step 1: Create and configure a Telegram bot](#step-1-create-and-configure-a-telegram-bot)
- [Step 2: Create a Telegram channel you will post to](#step-2-create-a-telegram-channel-you-will-post-to)
- [Posting to a newly created (private) channel for which you are an Administrator](#posting-to-a-newly-created-private-channel-for-which-you-are-an-administrator)
- [Posting to an existing public channel](#posting-to-an-existing-public-channel)
- [Step 3: Configure Planefence to send notifications to Telegram](#step-3-configure-planefence-to-send-notifications-to-telegram)
- [Summary of License Terms](#summary-of-license-terms)
This README describes how to configure Planefence to send notifications for Planefence and/or Plane-Alert messages to Telegram.
The examples described here use the Telegram web interface. You can do most of this also using one of the Telegram Apps, but please note that some apps (for example, the iPhone app) don't allow you to do steps 2.4 and 2.5. We therefore recommend you use a browser from your computer to set this up. Telegram in the browser interface can be reached [at this link](https://web.telegram.org/a/).
## Prerequisites
This is part of the [sdr-enthusiasts/docker-planefence] docker container. Nothing in this document will make sense outside the context of this container. We assume that Planefence has been set up correctly and is working fine, and all you want to do is add Telegram notifications to your existing setup.
You should also already have a Telegram account. If you don't, please go to [Telegram](https://web.telegram.org/a/) on the web and sign up first.
## Step 1: Create and configure a Telegram bot
1. Start a conversation with [@BotFather](https://web.telegram.org/a/#93372553) on Telegram
2. Say: `/newbot`. Give it a name and a username that is unique
![Create a bot](.img/telegram-new-bot.png)
3. Copy the HTTP API Token. This will be the value for the`TELEGRAM_BOT_TOKEN` parameter that we will discuss below.
## Step 2: Create a Telegram channel you will post to
### Posting to a newly created (private) channel for which you are an Administrator
1. From the screen that lists all of your Chats, click the square-with-pencil icon and create a "New Channel".
2. Add **yourself** and **the bot you just created** to the channel. The bot must have **Admin privileges**, but the only attribute it needs is to "Post Messages". The other permissions can safely be switched off
3. Send a test message to the channel
4. Right-click the test message and select "Copy Message Link" *)
5. Paste the link somewhere to see the URL, which will look like this: <https://t.me/c/123456789/2>. The numbers in `c/...../` are your CHAT_ID (in our case: `123456789`). Please note this as you will use it below.
The "Copy Message Link" option appears to be available only when you are using a web client for Telegram. Go to [https://web.telegram.org/a](Telegram on the web) and log in there to see this.
### Posting to an existing public channel
1. You must add the bot as a subscriber to the channel. You can only do this if you are an Administrator of the Channel
2. For the CHAT_ID, feel free to use the `@ChannelName` value, or follow steps 4 and 5 above to get the numeric ID.
## Step 3: Configure Planefence to send notifications to Telegram
Edit your `planefence.config` file, and add or modify the following parameters:
```config
TELEGRAM_BOT_TOKEN="" # set this parameter to the HTTP API Token you got in Step 1
PF_TELEGRAM_CHAT_ID="" # Planefence Chat (channel) ID - set this parameter to the CHAT_ID you got in Step 2
PA_TELEGRAM_CHAT_ID="" # Plane-Alert Chat (channel) ID - set this parameter to the CHAT_ID you got in Step 2
PF_TELEGRAM_ENABLED=false # Set this to "on"/"enabled"/"1"/"yes"/"true" to start sending Planefence notifications
PA_TELEGRAM_ENABLED=false # Set this to "on"/"enabled"/"1"/"yes"/"true" to start sending Plane-Alert notifications
```
## Summary of License Terms
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@@ -19,6 +19,7 @@
- [Plane-Alert Query parameters](#plane-alert-query-parameters)
- [Troubleshooting](#troubleshooting)
- [Getting help](#getting-help)
- [License and Data Use/Privacy](#license-and-data-useprivacy)
## What is it?
@@ -26,7 +27,7 @@ This repository contains Planefence, which is an add-on to `ultrafeeder`, `reads
Planefence will create a log of aircraft heard by your Feeder Station that are within a "fence", that is, less than a certain distance and lower than a certain
altitude from your station. This log is displayed on a website and is also made available in daily CSV files.
Furthermore, Planefence can send a notification for every plane in the fence to BlueSky, Mastodon, Discord, and/or MQTT. We used to support Twitter/X as well but this was deprecated when they started charging for API use. With some add-on software/hardware, you will be able to collect audio noise figures to see how loud the aircraft are that fly above your Feeder Station.
Furthermore, Planefence can send a notification for every plane in the fence to BlueSky, Mastodon, Discord, Telegram, RSS, and/or MQTT. We used to support Twitter/X as well but this was deprecated when they started charging for API use. With some add-on software/hardware, you will be able to collect audio noise figures to see how loud the aircraft are that fly above your Feeder Station.
Planefence is deployed as a Docker container and is pre-built for the following architectures:
@@ -128,6 +129,8 @@ Also note that after adding exclusions, any pre-existing entries for those exclu
- Planefence deployment example: <https://planefence.com/planefence>
- Plane-Alert deployment example: <https://planefence.com/plane-alert>
- BlueSky notifications: <https://bsky.app/profile/aboveboston.bsky.social>
- Telegram notifications: <https://t.me/+B_r-DgeNEQ4yMTUx>
- RSS feed: <https://kx1t.com/planefence-dev/planefence.rss>
## API access to your data

154
rootfs/scripts/post2telegram.sh Executable file
View File

@@ -0,0 +1,154 @@
#!/command/with-contenv bash
#shellcheck shell=bash disable=SC1091,SC2174,SC2015,SC2154
# -----------------------------------------------------------------------------------
# Copyright 2025 Ramon F. Kolb - licensed under the terms and conditions
# of GPLv3. The terms and conditions of this license are included with the Github
# distribution of this package, and are also available here:
# https://github.com/sdr-enthusiasts/docker-planefence/
#
# This package may incorporate other software and license terms.
# -----------------------------------------------------------------------------------
source /scripts/common
source /usr/share/planefence/persist/planefence.config
if (( ${#@} < 1 )); then
"${s6wrap[@]}" echo "Usage: $0 <PF|PA> <text> [image1] [image2] ..."
exit 1
fi
# Set the default values
TELEGRAM_API="${TELEGRAM_API:-https://api.telegram.org/bot}"
TELEGRAM_MAX_LENGTH="${TELEGRAM_MAX_LENGTH:-4096}"
if [[ "${1,,}" == "pf" ]]; then
if [[ -z "${PF_TELEGRAM_CHAT_ID}" ]]; then
"${s6wrap[@]}" echo "Fatal: the PF_TELEGRAM_CHAT_ID environment variable must be set"
exit 1
fi
TELEGRAM_CHAT_ID="${PF_TELEGRAM_CHAT_ID}"
elif [[ "${1,,}" == "pa" ]]; then
# shellcheck disable=SC2153
TELEGRAM_CHAT_ID="${PA_TELEGRAM_CHAT_ID}"
if [[ -z "$TELEGRAM_CHAT_ID" ]]; then
"${s6wrap[@]}" echo "Fatal: the PA_TELEGRAM_CHAT_ID environment variable must be set"
exit 1
fi
else
"${s6wrap[@]}" echo "Fatal: you must specify either 'PF' or 'PA' as the first argument to $0"
"${s6wrap[@]}" echo "Usage: $0 <PF|PA> <text> [image1] [image2] ..."
exit 1
fi
# Check if the required variables are set
if [[ -z "$TELEGRAM_BOT_TOKEN" ]]; then
"${s6wrap[@]}" echo "Fatal: the TELEGRAM_BOT_TOKEN environment variable must be set"
exit 1
fi
if [[ -z "$TELEGRAM_CHAT_ID" ]]; then
"${s6wrap[@]}" echo "Fatal: the TELEGRAM_CHAT_ID environment variable must be set"
exit 1
fi
if [[ "${TELEGRAM_CHAT_ID:0:4}" != "-100" ]]; then TELEGRAM_CHAT_ID="-100${TELEGRAM_CHAT_ID}"; fi
# Extract info from the command line arguments
args=("$@")
TEXT="${args[1]}"
IMAGES=("${args[2]}" "${args[3]}" "${args[4]}" "${args[5]}") # up to 4 images
if [[ -z "$TEXT" ]]; then
"${s6wrap[@]}" echo "Fatal: a message text must be included in the request to $0"
"${s6wrap[@]}" echo "Usage: $0 <PF|PA> <text> [image1] [image2] ..."
exit 1
fi
# "${s6wrap[@]}" echo "DEBUG: Invoking: $0 $1 $TEXT ${IMAGES[*]}"
# Clean up the text
TEXT="${TEXT:0:$TELEGRAM_MAX_LENGTH}" # limit to max characters
TEXT="${TEXT//[[:cntrl:]]/$'\n'}" # Replace control characters with newlines
# Send images to Telegram if available
image_count=0
for image in "${IMAGES[@]}"; do
if [[ -n "$image" ]]; then
image_count="$((image_count + 1))" # only count non-empty image paths
fi
done
image_counter=1
# shellcheck disable=SC2001
ICAO="$(sed -n 's/.*ICAO: #\?\([A-Fa-f0-9]\{6\}\).*/\1/p' <<< "${TEXT//[[:cntrl:]]/ }")"
TAIL="$(sed -n 's/.*Tail: #\?\([A-Za-z0-9-]\+\).*/\1/p' <<< "${TEXT//[[:cntrl:]]/ }")"
FLIGHT="$(sed -n 's/.*Flt: #\?\([A-Za-z0-9-]\+\).*/\1/p' <<< "${TEXT//[[:cntrl:]]/ }")"
if [[ -n "$ICAO" ]]; then image_header="ICAO $ICAO"; else image_header=""; fi
if [[ -n "$TAIL" ]]; then image_header+="${image_header:+ - }Tail $TAIL"; fi
if [[ -n "$FLIGHT" ]]; then image_header+="${image_header:+ - }Flight $FLIGHT"; fi
image_header="${image_header:+$image_header - }"
for image in "${IMAGES[@]}"; do
# Skip if the image is not a file that exists
if [[ -z "$image" ]] || [[ ! -f "$image" ]]; then
continue
fi
if (( image_count > 1 )); then
if ((image_counter == 1 )); then
image_text="Image $image_counter of $image_count"
else
image_text="${image_header}Image $image_counter of $image_count"
fi
else
image_text=""
fi
# Send the photo with the message
if (( image_counter == 1 )); then
response="$(curl --max-time 30 -sSL -X POST "${TELEGRAM_API}${TELEGRAM_BOT_TOKEN}/sendPhoto" \
-F "chat_id=${TELEGRAM_CHAT_ID}" \
-F "photo=@${image}" \
-F "caption=${image_text}${image_text:+$'\n'}${TEXT}" \
-F "parse_mode=HTML")"
message_id="$(jq -r '.result.message_id' <<< "$response" 2>/dev/null)"
else
response="$(curl --max-time 30 -sSL -X POST "${TELEGRAM_API}${TELEGRAM_BOT_TOKEN}/sendPhoto" \
-F "chat_id=${TELEGRAM_CHAT_ID}" \
-F "photo=@${image}" \
-F "caption=${image_text}" \
-F "parse_mode=HTML")"
fi
if (( image_counter == 1)); then
if [[ -z "$message_id" ]] || [[ "$message_id" == "null" ]]; then
"${s6wrap[@]}" echo "Error sending photo to Telegram: $response"
{ echo "{ \"title\": \"Telegram Photo Send Error\","
echo " \"response\": $response }"
} >> /tmp/telegram.json
else
echo "https://t.me/c/${TELEGRAM_CHAT_ID}/${message_id}" > /tmp/telegram.link
"${s6wrap[@]}" echo "Photo message sent successfully to Telegram; link: $(</tmp/telegram.link)"
fi
fi
image_counter=$((image_counter + 1))
done
# If no images or image sending failed, send text only
if (( image_count == 0 )); then
response="$(curl --max-time 30 -sSL -X POST "${TELEGRAM_API}${TELEGRAM_BOT_TOKEN}/sendMessage" \
-F "chat_id=${TELEGRAM_CHAT_ID}" \
-F "text=${TEXT}" \
-F "parse_mode=HTML")"
message_id="$(jq -r '.result.message_id' <<< "$response" 2>/dev/null)"
if [[ -z "$message_id" ]] || [[ "$message_id" == "null" ]]; then
"${s6wrap[@]}" echo "Error sending message to Telegram: $response"
{ echo "{ \"title\": \"Telegram Message Send Error\","
echo " \"response\": $response }"
} >> /tmp/telegram.json
exit 1
else
echo "https://t.me/c/${TELEGRAM_CHAT_ID}/${message_id}" > /tmp/telegram.link
"${s6wrap[@]}" echo "Text message sent successfully to Telegram; link: $(</tmp/telegram.link)"
fi
fi

View File

@@ -207,6 +207,10 @@ set +a
BLUESKY_API=""
#
# ---------------------------------------------------------------------
# These are the parameters to switch Telegram notifications on or off.
TELEGRAM_ENABLED=false
#
# ---------------------------------------------------------------------
# These are the parameters for PA web page dark mode:
DARKMODE=false

View File

@@ -290,7 +290,7 @@ touch /tmp/pa-diff.csv
# compare the new csv file to the old one and only print the added entries
comm -23 <(sort < "$OUTFILE") <(sort < /tmp/pa-old.csv ) >/tmp/pa-diff.csv
[[ "$(wc -l < /tmp/pa-diff.csv)" -gt "0" ]] && [[ "$LOGLEVEL" != "ERROR" ]] && echo "[planefence/plane-alert][$(date)] Plane-Alert DIFF file has $(cat /tmp/pa-diff.csv | wc -l) lines and contains:" && cat /tmp/pa-diff.csv || true
[[ "$(wc -l < /tmp/pa-diff.csv)" -gt "0" ]] && [[ "$LOGLEVEL" != "ERROR" ]] && "${s6wrap[@]}" echo "Plane-Alert DIFF file has $(cat /tmp/pa-diff.csv | wc -l) lines and contains:" && cat /tmp/pa-diff.csv || true
# -----------------------------------------------------------------------------------
# Next, let's do some stuff with the newly acquired aircraft of interest
# but only if there are actually newly acquired records
@@ -533,16 +533,26 @@ then
fi
fi
# Inject BlueSky integration here:
if [[ -n "$BLUESKY_HANDLE" ]] && [[ -n "$BLUESKY_APP_PASSWORD" ]]; then
# get a list of images to upload
# shellcheck disable=SC2206
if [[ "$GOTSNAP" == "true" ]]; then images=("$snapfile" ${images[@]}); fi
# now send the BlueSky message:
echo "DEBUG: posting to BlueSky: /scripts/post2bsky.sh \"$(sed -e 's|\\/|/|g' -e 's|\\n|\n|g' -e 's|%0A|\n|g' <<< "${TWITTEXT}")\" ${images[*]::4}"
# echo "DEBUG: posting to BlueSky: /scripts/post2bsky.sh \"$(sed -e 's|\\/|/|g' -e 's|\\n|\n|g' -e 's|%0A|\n|g' <<< "${TWITTEXT}")\" ${images[*]::4}"
# shellcheck disable=SC2206
if [[ "$GOTSNAP" == "true" ]]; then bsky_images=("$snapfile" ${images[@]}); else bsky_images=(${images[@]}); fi
# shellcheck disable=SC2068
/scripts/post2bsky.sh "$(sed -e 's|\\/|/|g' -e 's|\\n|\n|g' -e 's|%0A|\n|g' <<< "${TWITTEXT}")" ${images[@]::4} || true
/scripts/post2bsky.sh "$(sed -e 's|\\/|/|g' -e 's|\\n|\n|g' -e 's|%0A|\n|g' <<< "${TWITTEXT}")" ${bsky_images[@]::4} || true
fi
# Inject Telegram integration here:
if chk_enabled "$TELEGRAM_ENABLED"; then
# now send the Telegram message:
# echo "DEBUG: posting to Telegram: /scripts/post2telegram.sh \"$(sed -e 's|\\/|/|g' -e 's|\\n|\n|g' -e 's|%0A|\n|g' <<< "${TWITTEXT}")\" ${images[*]::4}"
# shellcheck disable=SC2206
if [[ "$GOTSNAP" == "true" ]]; then telegram_images=("$snapfile" ${images[@]}); else telegram_images=(${images[@]}); fi
# shellcheck disable=SC2068
/scripts/post2telegram.sh PA "$(sed -e 's|\\/|/|g' -e 's|\\n|\n|g' -e 's|%0A|\n|g' <<< "${TWITTEXT}")" ${telegram_images[@]::4} || true
fi
# Inject Mastodon integration here:

View File

@@ -404,6 +404,10 @@ set +a
BLUESKY_API=""
#
# ---------------------------------------------------------------------
# These are the parameters to switch Telegram notifications on or off.
TELEGRAM_ENABLED=false
#
# ---------------------------------------------------------------------
# This is the parameter for PF web page dark mode:
DARKMODE=false
#

View File

@@ -492,6 +492,7 @@ WRITEHTMLTABLE () {
records[$index:notif_service]="MQTT"
records[$index:notif_link]=""
elif [[ "${records[$index:notif_link]:0:17}" == "https://bsky.app/" ]]; then records[$index:notif_service]="BlueSky"
elif [[ "${records[$index:notif_link]:0:13}" == "https://t.me/" ]]; then records[$index:notif_service]="Telegram"
elif grep -qo "$MASTODON_SERVER" <<< "${records[$index:notif_link]}"; then records[$index:notif_service]="Mastodon"
fi
fi
@@ -887,11 +888,12 @@ fi
# see if we need to invoke PlaneTweet:
[[ "$BASETIME" != "" ]] && echo "7. $(bc -l <<< "$(date +%s.%2N) - $BASETIME")s -- done applying filters, invoking PlaneTweet" || true
if [[ -n "$PLANETWEET" ]] \
if chk_enabled "$PLANETWEET" \
|| chk_enabled "${PF_DISCORD}" \
|| [[ -n "$MASTODON_SERVER" ]] \
|| chk_enabled "$PF_MASTODON" \
|| [[ -n "$BLUESKY_HANDLE" ]] \
|| [[ -n "$RSS_SITELINK" ]] \
|| chk_enabled "$PF_TELEGRAM_ENABLED" \
|| [[ -n "$MQTT_URL" ]]; then
LOG "Invoking planefence_notify.sh for notifications"
$PLANEFENCEDIR/planefence_notify.sh today "$DISTUNIT" "$ALTUNIT"

View File

@@ -466,6 +466,14 @@ if [ -f "$CSVFILE" ]; then
fi
# Insert Telegram notifications here:
if chk_enabled "$TELEGRAM_ENABLED"; then
/scripts/post2telegram.sh PF "#Planefence $(sed -e 's|\\/|/|g' -e 's|\\n|\n|g' -e 's|%0A|\n|g' <<<"${TWEET}")" "$(if $GOTSNAP; then echo "$snapfile"; fi)" "$(if $GOTIMG; then echo "$imgfile"; fi)" || true
if [[ -f /tmp/telegram.link ]]; then
LINK="$(</tmp/telegram.link)"
rm -f /tmp/telegram.link
fi
fi
# Insert BlueSky notifications here:
if [[ -n "$BLUESKY_HANDLE" ]] && [[ -n "$BLUESKY_APP_PASSWORD" ]]; then
/scripts/post2bsky.sh "#Planefence $(sed -e 's|\\/|/|g' -e 's|\\n|\n|g' -e 's|%0A|\n|g' <<<"${TWEET}")" "$(if $GOTSNAP; then echo "$snapfile"; fi)" "$(if $GOTIMG; then echo "$imgfile"; fi)" || true

View File

@@ -322,6 +322,10 @@ if [[ -n "$MASTODON_SERVER" ]] && [[ -n "$MASTODON_ACCESS_TOKEN" ]]; then
fi
fi
# Configure Telegram parameters:
configure_planefence "TELEGRAM_ENABLED" "$PF_TELEGRAM_ENABLED"
configure_planealert "TELEGRAM_ENABLED" "$PA_TELEGRAM_ENABLED"
configure_planealert "NAME" "${PF_NAME:-My}"
configure_planealert "ADSBLINK" "$PF_MAPURL"
configure_planealert "RANGE" "${PF_PARANGE:-999999}"

View File

@@ -522,6 +522,26 @@ PA_BLUESKY_ENABLED=""
# BLUESKY_API="" # do not change unless you are sure that you know what you are doing!
#
# ---------------------------------------------------------------------
# These are optional parameters related to Telegram notifications
# See https://sdr-e.com/docker-planefence/main/README-telegram.md for instructions on
# how to create a Telegram Bot and a Telegram Channel to post to.
#
# TELEGRAM_BOT_TOKEN should look like "123456789:ABCDefGhIJKlmNoPQRsTUVwxyZ"
# PF_TELEGRAM_CHAT_ID / PA_TELEGRAM_CHAT_ID can be a numeric ID (for private channels) or a public channel name like "@yourchannel"
#
# IMPORTANT: you must add your newly created Bot as an Administrator of your channel. You can only do this if you
# yourself are an Administrator of the channel.
#
# The parameters "PF_TELEGRAM_ENABLED" and "PA_TELEGRAM_ENABLED" must be set to "on"/"enabled"/"1"/"yes"/"true"
# to start notifications about Planefence and Plane-Alert respectively.
#
TELEGRAM_BOT_TOKEN=""
PF_TELEGRAM_CHAT_ID=""
PA_TELEGRAM_CHAT_ID=""
PF_TELEGRAM_ENABLED=false
PA_TELEGRAM_ENABLED=false
#
# ---------------------------------------------------------------------
# These are optional parameters related to downloading the OpenSky database.
# By default, the container will check at start if the latest OpenSky aircraft database is available
# If it's not already downloaded, or if there is a new version of this database, it will download the latest version.