Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 137 additions & 105 deletions lib/pasarguard-backup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,27 @@ pg_manifest_encode() {
printf '%s\t%s\t%s\t%s\t%s' "$dbname" "$owner" "$has_ts" "$filename" "$ts_version"
}

escape_markdown_v2() {
local text="$1"
local chars='_*[]()~`>#+-=|{}.!\'
local escaped=""
local i char
for (( i=0; i<${#text}; i++ )); do
char="${text:$i:1}"
if [[ "$chars" == *"$char"* ]]; then
escaped+="\\$char"
else
escaped+="$char"
fi
done
printf '%s' "$escaped"
}

send_backup_to_telegram() {
if [ -f "$ENV_FILE" ]; then
if grep -q "^BACKUP_TELEGRAM_CHAT_ID=" "$ENV_FILE"; then
sed -i 's/^BACKUP_TELEGRAM_CHAT_ID=/BACKUP_TELEGRAM_CHAT_IDS=/' "$ENV_FILE"
fi
while IFS='=' read -r key value; do
if [[ -z "$key" || "$key" =~ ^# ]]; then
continue
Expand Down Expand Up @@ -175,8 +194,8 @@ send_backup_to_telegram() {
return 1
fi

if [ -z "$BACKUP_TELEGRAM_CHAT_ID" ]; then
colorized_echo red "Error: BACKUP_TELEGRAM_CHAT_ID is not set in .env file"
if [ -z "$BACKUP_TELEGRAM_CHAT_IDS" ]; then
colorized_echo red "Error: BACKUP_TELEGRAM_CHAT_IDS is not set in .env file"
return 1
fi

Expand Down Expand Up @@ -272,78 +291,82 @@ send_backup_to_telegram() {

local backup_time=$(date "+%Y-%m-%d %H:%M:%S %Z")

local files_list=""
for file_path in "${backup_paths[@]}"; do
files_list+="- $(basename "$file_path")"$'\n'
done
files_list="${files_list%$'\n'}"

local info_message=$'📤 Backup Upload Summary\n'
info_message+=$'──────────────────────\n'
info_message+="⏳ Time: $backup_time"$'\n'
info_message+=$'\n✅ Files Uploaded:\n'
info_message+="$files_list"$'\n'
info_message+=$'\n📂 Extraction Guide:\n'
if [ "$extraction_mode" = "part_zip" ]; then
info_message+=$'🪟 Windows: Rebuild the archive first, e.g. copy /b backup_xxx.part01.zip+backup_xxx.part02.zip+... backup_xxx.zip, then extract backup_xxx.zip with 7-Zip.\n'
info_message+=$'🐧 Linux: Run cat backup_xxx.part*.zip > backup_xxx.zip && unzip backup_xxx.zip.\n'
info_message+=$'🍎 macOS: Run cat backup_xxx.part*.zip > backup_xxx.zip && unzip backup_xxx.zip.\n'
info_message+=$'⚠️ Always download every .partNN.zip file before rebuilding the archive.'
elif [ "$extraction_mode" = "split_zip" ]; then
info_message+=$'🪟 Windows: Install and use 7-Zip. Place the .zip and every .zXX part together, then start extraction from the .zip file.\n'
info_message+=$'🐧 Linux: Run unzip (e.g., unzip backup_xxx.zip) with all .zXX parts in the same directory.\n'
info_message+=$'🍎 macOS: Use Archive Utility or run unzip backup_xxx.zip from Terminal with the .zXX parts beside the .zip file.\n'
info_message+=$'⚠️ Always download the .zip and every .zXX part before extracting.'
else
info_message+=$'🪟 Windows: Extract the uploaded archive with 7-Zip.\n'
info_message+=$'🐧 Linux: Run unzip backup_xxx.zip.\n'
info_message+=$'🍎 macOS: Open the archive with Archive Utility or run unzip backup_xxx.zip.\n'
info_message+=$'⚠️ Download the complete archive before extracting.'
fi

local escaped_info_message=$(escape_markdown_v2 "$info_message")

IFS=',' read -ra chat_ids <<< "$BACKUP_TELEGRAM_CHAT_IDS"
for part in "${backup_paths[@]}"; do
local part_name=$(basename "$part")
local custom_filename="$part_name"

local escaped_server_ip=$(printf '%s' "$server_ip" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g')
local escaped_filename=$(printf '%s' "$custom_filename" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g')
local escaped_time=$(printf '%s' "$backup_time" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g')
local caption="📦 *Backup Information*\n🌐 *Server IP*: \`$escaped_server_ip\`\n📁 *Backup File*: \`$escaped_filename\`\n⏰ *Backup Time*: \`$escaped_time\`"

local response=$(curl "${curl_proxy_args[@]}" -s -w "\n%{http_code}" -F chat_id="$BACKUP_TELEGRAM_CHAT_ID" \
-F document=@"$part;filename=$custom_filename" \
-F caption="$(printf '%b' "$caption")" \
-F parse_mode="MarkdownV2" \
"https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument" 2>&1)

local http_code=$(echo "$response" | tail -n1)
local response_body=$(echo "$response" | sed '$d')

if [ "$http_code" == "200" ]; then
# Check if response contains "ok":true
if echo "$response_body" | grep -q '"ok":true'; then
uploaded_files+=("$custom_filename")
colorized_echo green "Backup part $custom_filename successfully sent to Telegram."
local escaped_server_ip=$(escape_markdown_v2 "$server_ip")
local escaped_filename=$(escape_markdown_v2 "$custom_filename")
local escaped_time=$(escape_markdown_v2 "$backup_time")
local caption=$'📦 *Backup Information*\n'
caption+="🌐 *Server IP*: \`$escaped_server_ip\`"$'\n'
caption+="📁 *Backup File*: \`$escaped_filename\`"$'\n'
caption+="⏰ *Backup Time*: \`$escaped_time\`"$'\n\n'
caption+="$escaped_info_message"
Comment on lines +300 to +337

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching backup script:"
fd -a 'pasarguard-backup\.sh$' . || true

script="$(fd 'pasarguard-backup\.sh$' . | head -n1 || true)"
if [ -n "$script" ]; then
  echo
  echo "Line count:"
  wc -l "$script"
  echo
  echo "Relevant section lines 250-370:"
  sed -n '250,370p' "$script" | cat -n -v
  echo
  echo "Definitions/usages around files_list and backup_paths:"
  rg -n "files_list|backup_paths|part_zip|split_zip|escape_markdown_v2|capture_errors|Telegram|sendDocument|send.*Files|caption" "$script"
fi

echo
echo "Telegram API references in repo:"
rg -n "api\.telegram|sendDocument|1024|400 Bad Request" .

Repository: PasarGuard/scripts

Length of output: 15023


🌐 Web query:

Telegram Bot API sendDocument caption max 1024 characters MarkdownV2 caption limit

💡 Result:

The caption for a Telegram Bot API sendDocument request is strictly limited to a maximum of 1024 characters [1][2]. This limit applies to the caption text after any entities have been parsed [1][3][2]. Key considerations regarding this limit: - Parse Mode Impact: When using MarkdownV2 (or HTML), the 1024-character limit applies to the final, rendered text that appears in the message, not the raw input string containing the markup syntax [1][3][2]. - Exceeding the Limit: If the provided caption exceeds 1024 characters, the API will return a 400 Bad Request error [1]. - Comparison to Standard Messages: This limit is specific to media captions; standard text messages sent via the sendMessage method have a higher limit of 4096 characters [4]. - Workarounds: If your content exceeds 1024 characters, the recommended practice is to send the document first and then follow up with a separate text message containing the remaining content [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re

text = open("lib/pasarguard-backup.sh", errors="utf-8-sig").read()
caption_block = text[text.index("""caption=$'""");text.index('caption+="', text.index("caption=$'"))]
# Extract literal-ish assignment from raw echo, but easier: locate source assignment as string.
m = re.search(r"local caption=.+?(?=local escaped_server_ip)", text, re.S)
print("caption source presence:", bool(m))
print(m.group(0)[:500])

# Build same source text without interpreting markdown as entities, only emoji/lines/file lines,
# to approximate raw char count.
# Use the visible snippet from sed as approximate bytes.
PY

python3 - <<'PY'
import textwrap
sample_names = [f"backup_xxx-{i:03d}.zip" for i in range(1, 21)]
files_list = "\n".join("- " + name for name in sample_names)
backup_time = "2026-07-01 12:00:00 UTC"
excerpt = f"- Backup Upload Summary\n──────────────────────\nTime: {backup_time}\n\n✅ Files Uploaded:\n{files_list}\n\n📂 Extraction Guide:\n🪟 Windows: Rebuild the archive first, e.g. copy /b backup_xxx.part01.zip+backup_xxx.part02.zip+... backup_xxx.zip, then extract backup_xxx.zip with 7-Zip.\n🐧 Linux: Run cat backup_xxx.part*.zip > backup_xxx.zip && unzip backup_xxx.zip.\n🍎 macOS: Run cat backup_xxx.part*.zip > backup_xxx.zip && unzip backup_xxx.zip.\n⚠️ Always download every .partNN.zip file before rebuilding the archive."
caption = "📦 *Backup Information*\n🌐 *Server IP*: `192.0.2.1`\n📁 *Backup File*: `backup_zzz.zip`\n⏰ *Backup Time*: `{backup_time}`\n\n{excerpt}"
# We need rendered count: markdown code ticks may render as code blocks but plain ASCII mostly rendered same; emojis count 1 char after rendering.
count = len(caption)
print(f"raw_len={count}")
print(f"exceeds_1024={count > 1024}")
print(f"excerpt_lines={caption.count(chr(10)) + 1}; file_lines={caption.count(chr(10))}")
PY

Repository: PasarGuard/scripts

Length of output: 370


🌐 Web query:

Telegram Bot API sendDocument caption limit 1024 rendered entities not raw characters

💡 Result:

The caption length limit for the sendDocument method (and other media methods) in the Telegram Bot API is 1024 characters [1][2][3]. Crucially, this limit is defined based on the text after entity parsing or, more specifically, the number of UTF-16 code units [4]. According to the official Telegram Bot API documentation, the length is evaluated "after entities parsing" [1][2][3]. Key technical considerations regarding this limit include: 1. Measurement unit: Telegram computes the length of strings and entities using UTF-16 code units, not raw characters or bytes [4]. This means characters outside the Basic Multilingual Plane (such as certain emojis or special symbols) count as two UTF-16 code units [4]. 2. Parsing and Serialization: Because the limit applies after parsing, developers must be aware that using Markdown or HTML formatting (e.g., HTML entities like < or serialization) can cause a string that appears to be under 1024 characters in its raw form to exceed the limit once rendered by the Telegram server [5]. 3. Best Practice: If your intended caption exceeds the 1024-character limit after formatting, the recommended approach is to send the document first and then send the remaining text as a separate text message [1]. You should ensure your application calculates the length of the string based on UTF-16 code units and accounts for any expansion that occurs during the parsing process if using parse_mode (HTML or MarkdownV2) [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "MarkdownV2 escape implementation:"
sed -n '130,185p' lib/pasarguard-backup.sh | cat -n -v

echo
echo "Build approximation of caption length by echo:"
cat >/tmp/telegram_caption_helper.sh <<'BASH'
#!/bin/bash
extraction_mode="part_zip"
backup_paths=(
  "/srv/parts/backup.part01.zip"
  "/srv/parts/backup.part02.zip"
  "/srv/parts/backup.part03.zip"
  "/srv/parts/backup.part04.zip"
  "/srv/parts/backup.part05.zip"
  "/srv/parts/backup.part06.zip"
  "/srv/parts/backup.part07.zip"
  "/srv/parts/backup.part08.zip"
  "/srv/parts/backup.part09.zip"
  "/srv/parts/backup.part10.zip"
  "/srv/parts/backup.part11.zip"
  "/srv/parts/backup.part12.zip"
  "/srv/parts/backup.part13.zip"
  "/srv/parts/backup.part14.zip"
  "/srv/parts/backup.part15.zip"
  "/srv/parts/backup.part16.zip"
  "/srv/parts/backup.part17.zip"
  "/srv/parts/backup.part18.zip"
  "/srv/parts/backup.part19.zip"
  "/srv/parts/backup.part20.zip"
)
escape_markdown_v2() {
  sed -e 's#_#\\_#g' -e 's#\*#\\*`#g`' -e 's#\[#\\[`#g`' -e 's#\]#\\]`#g`' -e 's#~#\\~`#g`' -e 's#>#\\>`#g`' -e 's#<#\\<`#g`' -e 's#`#\\`#g' -e 's#!#\\!`#g`' -e 's#|#\\|`#g`' \
      -e ':a; s#\\\(#\\(\\)`#g`; s#\\\)([^]|[:space:]]|[[:space:]])#$#; s#\\\)#\(\)`#g`' "$1"
}
files_list=""
for file_path in "${backup_paths[@]}"; do
  files_list+="- $(basename "$file_path")"$'\n'
done
files_list="${files_list%$'\n'}"
backup_time="2026-07-01 12:00:00 UTC"
info_message=$'📤 Backup Upload Summary\n'
info_message+=$'──────────────────────\n'
info_message+="⏳ Time: $backup_time"$'\n'
info_message+=$'\n✅ Files Uploaded:\n'
info_message+="$files_list"$'\n'
info_message+=$'\n📂 Extraction Guide:\n'
if [ "$extraction_mode" = "part_zip" ]; then
  info_message+=$'🪟 Windows: Rebuild the archive first, e.g. copy /b backup_xxx.part01.zip+backup_xxx.part02.zip+... backup_xxx.zip, then extract backup_xxx.zip with 7-Zip.\n'
  info_message+=$'🐧 Linux: Run cat backup_xxx.part*.zip > backup_xxx.zip && unzip backup_xxx.zip.\n'
  info_message+=$'🍎 macOS: Run cat backup_xxx.part*.zip > backup_xxx.zip && unzip backup_xxx.zip.\n'
  info_message+=$'⚠️ Always download every .partNN.zip file before rebuilding the archive.'
elif [ "$extraction_mode" = "split_zip" ]; then
  info_message+=$'🪟 Windows: Install and use 7-Zip. Place the .zip and every .zXX part together, then start extraction from the .zip file.\n'
  info_message+=$'🐧 Linux: Run unzip (e.g., unzip backup_xxx.zip) with all .zXX parts in the same directory.\n'
  info_message+=$'🍎 macOS: Use Archive Utility or run unzip backup_xxx.zip from Terminal with the .zXX parts beside the .zip file.\n'
  info_message+=$'⚠️ Always download the .zip and every .zXX part before extracting.'
else
  info_message+=$'🪟 Windows: Extract the uploaded archive with 7-Zip.\n'
  info_message+=$'🐧 Linux: Run unzip backup_xxx.zip.\n'
  info_message+=$'🍎 macOS: Open the archive with Archive Utility or run unzip backup_xxx.zip.\n'
  info_message+=$'⚠️ Download the complete archive before extracting.'
fi

escaped_info_message=$(escape_markdown_v2 "$info_message")

escaped_server_ip=$(escape_markdown_v2 "192.0.2.1")
custom_filename="backup.part20.zip"
escaped_filename=$(escape_markdown_v2 "$custom_filename")
escaped_time=$(escape_markdown_v2 "$backup_time")
caption=$'📦 *Backup Information*\n'
caption+="🌐 *Server IP*: \`$escaped_server_ip\`"$'\n'
caption+="📁 *Backup File*: \`$escaped_filename\`"$'\n'
caption+="⏰ *Backup Time*: \`$escaped_time\`"$'\n\n'
caption+="$escaped_info_message"

tmpfile="/tmp/pasarguard_caption_temp"
echo -n "$caption" > "$tmpfile"
python3 - "$tmpfile" <<'PY'
import sys
p=sys.argv[1]
raw=open(p,'rb').read()
print("file", p)
print("file_size_bytes", len(raw))
print("chars", len(raw.decode("utf-8")))
print("utf16_code_units", len(raw.decode("utf-8").encode("utf-16-le")) // 2)
PY
BASH
bash /tmp/telegram_caption_helper.sh

Repository: PasarGuard/scripts

Length of output: 2810


🌐 Web query:

Telegram Bot API sendDocument caption limit 1024 characters documented

💡 Result:

The Telegram Bot API documentation for the sendDocument method specifies that the caption parameter has a limit of 1024 characters [1][2][3]. While there have been community requests and discussions regarding increasing this limit to match the 4096-character limit used for standard text messages, the documented limit for media captions remains 1024 characters [4][5]. Exceeding this limit when calling the API typically results in a 400 Bad Request error [1][5].

Citations:


🌐 Web query:

Telegram Bot API sendMediaGroup sent as separate text messages caption workaround

💡 Result:

In the Telegram Bot API, the sendMediaGroup method does not support a separate text message for captions; it only supports captions attached directly to the media items within the group [1][2]. If your goal is to have a "global" caption that appears below the entire album, the established workaround is to set the caption on exactly one of the media items in the group (typically the first one) [3][4][5]. Key points regarding this behavior: - Single Caption Placement: When you provide a caption for only one item in the media array, the Telegram client interprets this as the "global" caption for the entire album and displays it beneath the media group [3][6]. - Multiple Captions: If you assign a caption to more than one media item, the Telegram client will not display a global caption [3][7]. Instead, each individual media item will retain its own caption, which is typically only visible when a user opens that specific media item in full screen [3][6]. - No Separate Message Workaround: The Telegram Bot API does not provide a native way to send a media group and a separate text message as a single atomic unit [1][2]. If you require a separate text message, you must send it as a distinct API call (e.g., using sendMessage) either before or after sending the media group, though this will result in two separate message bubbles in the chat [3]. In summary, there is no workaround to force a "separate text message" to appear as a linked caption for a media group. The only way to display a caption "below" the group is to include it within the metadata of one of the media items in the sendMediaGroup request [6][8].

Citations:


Keep the per-document caption under Telegram’s media caption limit.

sendDocument captions are limited to 1024 UTF-16 code units after entity parsing. With 20+ part_zip/split_zip files, the metadata plus files list and extraction guide can exceed that limit, causing the document upload API calls to fail while iterating over parts and chat IDs.

Send the verbose summary once per chat as a separate sendMessage, then keep the per-document caption short (server IP, filename, time only).

🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 323-323: Declare and assign separately to avoid masking return values.

(SC2155)


[warning] 327-327: Declare and assign separately to avoid masking return values.

(SC2155)


[warning] 330-330: Declare and assign separately to avoid masking return values.

(SC2155)


[warning] 331-331: Declare and assign separately to avoid masking return values.

(SC2155)


[warning] 332-332: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pasarguard-backup.sh` around lines 300 - 337, Update the backup summary
flow around the info_message construction and per-part sendDocument logic: send
the escaped verbose summary once per chat via sendMessage, before iterating over
backup_paths, then remove it from each document caption. Keep each per-document
caption limited to the server IP, filename, and backup time so it remains within
Telegram’s caption limit.


for chat_id in "${chat_ids[@]}"; do
chat_id=$(echo "$chat_id" | xargs)
[ -z "$chat_id" ] && continue

local response=$(curl "${curl_proxy_args[@]}" -s -w "\n%{http_code}" -F chat_id="$chat_id" \
-F document=@"$part;filename=$custom_filename" \
-F caption="$caption" \
-F parse_mode="MarkdownV2" \
"https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument" 2>&1)
Comment on lines +343 to +347

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No timeout on the Telegram document upload.

Unlike the ifconfig.me lookup a few lines above (--max-time 5), this sendDocument call has no --max-time/--connect-timeout. If Telegram is unreachable or slow, this can hang indefinitely inside a cron-triggered backup run.

🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 343-343: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pasarguard-backup.sh` around lines 343 - 347, Add explicit connection and
total-operation timeouts to the curl invocation in the Telegram document upload
within the backup function, matching the existing timeout pattern used by the
nearby ifconfig.me lookup. Keep the current upload fields, response capture, and
Telegram endpoint unchanged.


local http_code=$(echo "$response" | tail -n1)
local response_body=$(echo "$response" | sed '$d')

if [ "$http_code" == "200" ]; then
# Check if response contains "ok":true
if echo "$response_body" | grep -q '"ok":true'; then
colorized_echo green "Backup part $custom_filename successfully sent to Telegram chat $chat_id."
else
# Extract error message from Telegram response
local error_msg=$(echo "$response_body" | grep -o '"description":"[^"]*"' | cut -d'"' -f4 || echo "Unknown error")
colorized_echo red "Failed to send backup part $custom_filename to Telegram chat $chat_id: $error_msg"
echo "Telegram API status: $http_code" >&2
echo "Telegram API Response: $response_body" >&2
fi
else
# Extract error message from Telegram response
local error_msg=$(echo "$response_body" | grep -o '"description":"[^"]*"' | cut -d'"' -f4 || echo "Unknown error")
colorized_echo red "Failed to send backup part $custom_filename to Telegram: $error_msg"
echo "Telegram API status: $http_code" >&2
local error_msg=$(echo "$response_body" | grep -o '"description":"[^"]*"' | cut -d'"' -f4 || echo "HTTP $http_code")
colorized_echo red "Failed to send backup part $custom_filename to Telegram chat $chat_id: $error_msg"
echo "Telegram API Response: $response_body" >&2
fi
else
local error_msg=$(echo "$response_body" | grep -o '"description":"[^"]*"' | cut -d'"' -f4 || echo "HTTP $http_code")
colorized_echo red "Failed to send backup part $custom_filename to Telegram: $error_msg"
echo "Telegram API Response: $response_body" >&2
fi
done

if [ ${#uploaded_files[@]} -gt 0 ]; then
local files_list=""
for file in "${uploaded_files[@]}"; do
files_list+="- $file"$'\n'
done
files_list="${files_list%$'\n'}"

local info_message=$'📦 Backup Upload Summary\n'
info_message+=$'──────────────────────\n'
info_message+="🌐 Server IP: $server_ip"$'\n'
info_message+="⏰ Time: $backup_time"$'\n'
info_message+=$'\n✅ Files Uploaded:\n'
info_message+="$files_list"$'\n'
info_message+=$'\n📂 Extraction Guide:\n'
if [ "$extraction_mode" = "part_zip" ]; then
info_message+=$'🪟 Windows: Rebuild the archive first, e.g. copy /b backup_xxx.part01.zip+backup_xxx.part02.zip+... backup_xxx.zip, then extract backup_xxx.zip with 7-Zip.\n'
info_message+=$'🐧 Linux: Run cat backup_xxx.part*.zip > backup_xxx.zip && unzip backup_xxx.zip.\n'
info_message+=$'🍎 macOS: Run cat backup_xxx.part*.zip > backup_xxx.zip && unzip backup_xxx.zip.\n'
info_message+=$'⚠️ Always download every .partNN.zip file before rebuilding the archive.'
elif [ "$extraction_mode" = "split_zip" ]; then
info_message+=$'🪟 Windows: Install and use 7-Zip. Place the .zip and every .zXX part together, then start extraction from the .zip file.\n'
info_message+=$'🐧 Linux: Run unzip (e.g., unzip backup_xxx.zip) with all .zXX parts in the same directory.\n'
info_message+=$'🍎 macOS: Use Archive Utility or run unzip backup_xxx.zip from Terminal with the .zXX parts beside the .zip file.\n'
info_message+=$'⚠️ Always download the .zip and every .zXX part before extracting.'
else
info_message+=$'🪟 Windows: Extract the uploaded archive with 7-Zip.\n'
info_message+=$'🐧 Linux: Run unzip backup_xxx.zip.\n'
info_message+=$'🍎 macOS: Open the archive with Archive Utility or run unzip backup_xxx.zip.\n'
info_message+=$'⚠️ Download the complete archive before extracting.'
fi

curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \
-d chat_id="$BACKUP_TELEGRAM_CHAT_ID" \
-d text="$info_message" >/dev/null 2>&1 || true
fi
done

if [ -n "$cleanup_dir" ]; then
rm -rf "$cleanup_dir"
Expand Down Expand Up @@ -377,35 +400,44 @@ send_backup_error_to_telegram() {
[Message truncated]"
fi

curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \
-d chat_id="$BACKUP_TELEGRAM_CHAT_ID" \
-d text="$message" >/dev/null 2>&1 &&
colorized_echo green "Backup error notification sent to Telegram." ||
colorized_echo red "Failed to send error notification to Telegram."

if [ -f "$log_file" ]; then
IFS=',' read -ra chat_ids <<< "$BACKUP_TELEGRAM_CHAT_IDS"
for chat_id in "${chat_ids[@]}"; do
chat_id=$(echo "$chat_id" | xargs)
[ -z "$chat_id" ] && continue

response=$(curl "${curl_proxy_args[@]}" -s -w "%{http_code}" -o /tmp/tg_response.json \
-F chat_id="$BACKUP_TELEGRAM_CHAT_ID" \
-F document=@"$log_file;filename=backup_error.log" \
-F caption="📜 Backup Error Log - $error_time" \
"https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument")

http_code="${response:(-3)}"
if [ "$http_code" -eq 200 ]; then
colorized_echo green "Backup error log sent to Telegram."
curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \
-d chat_id="$chat_id" \
-d text="$message" >/dev/null 2>&1 &&
colorized_echo green "Backup error notification sent to Telegram chat $chat_id." ||
colorized_echo red "Failed to send error notification to Telegram chat $chat_id."
Comment on lines +408 to +412

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Error notification success is not actually verified.

Success here is only "curl exited 0," not "Telegram accepted the message." A few lines below, the document-upload path correctly checks the response body for "ok":true before reporting success — this text message doesn't, so an invalid chat_id or a Telegram-side rejection will still print "sent successfully," defeating the purpose of the error-alerting path. Also missing --max-time like the sibling call at Line 343.

🔧 Suggested fix
-        curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \
-            -d chat_id="$chat_id" \
-            -d text="$message" >/dev/null 2>&1 &&
-            colorized_echo green "Backup error notification sent to Telegram chat $chat_id." ||
-            colorized_echo red "Failed to send error notification to Telegram chat $chat_id."
+        local msg_response
+        msg_response=$(curl "${curl_proxy_args[@]}" -s --max-time 15 -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \
+            -d chat_id="$chat_id" -d text="$message" 2>&1)
+        if echo "$msg_response" | grep -q '"ok":true'; then
+            colorized_echo green "Backup error notification sent to Telegram chat $chat_id."
+        else
+            colorized_echo red "Failed to send error notification to Telegram chat $chat_id: $msg_response"
+        fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \
-d chat_id="$chat_id" \
-d text="$message" >/dev/null 2>&1 &&
colorized_echo green "Backup error notification sent to Telegram chat $chat_id." ||
colorized_echo red "Failed to send error notification to Telegram chat $chat_id."
local msg_response
msg_response=$(curl "${curl_proxy_args[@]}" -s --max-time 15 -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \
-d chat_id="$chat_id" -d text="$message" 2>&1)
if echo "$msg_response" | grep -q '"ok":true'; then
colorized_echo green "Backup error notification sent to Telegram chat $chat_id."
else
colorized_echo red "Failed to send error notification to Telegram chat $chat_id: $msg_response"
fi
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 410-410: Note that A && B || C is not if-then-else. C may run when A is true.

(SC2015)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pasarguard-backup.sh` around lines 408 - 412, Update the Telegram text
notification flow around the curl POST to include the same --max-time safeguard
as the document-upload call and capture the response body instead of discarding
it. Report success only when the response confirms "ok":true; otherwise use the
existing failure message, including curl failures, timeouts, invalid chat IDs,
and Telegram-side rejections.


if [ -f "$log_file" ]; then

response=$(curl "${curl_proxy_args[@]}" -s -w "%{http_code}" -o /tmp/tg_response.json \
-F chat_id="$chat_id" \
-F document=@"$log_file;filename=backup_error.log" \
-F caption="📜 Backup Error Log - $error_time" \
"https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument")

http_code="${response:(-3)}"
if [ "$http_code" -eq 200 ]; then
colorized_echo green "Backup error log sent to Telegram chat $chat_id."
else
colorized_echo red "Failed to send backup error log to Telegram chat $chat_id. HTTP code: $http_code"
cat /tmp/tg_response.json
fi
else
colorized_echo red "Failed to send backup error log to Telegram. HTTP code: $http_code"
cat /tmp/tg_response.json
colorized_echo red "Log file not found: $log_file"
fi
Comment on lines +414 to 431

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Missing --max-time and missing local.

Same missing-timeout gap as the other Telegram calls in this file. Additionally, response and http_code here aren't declared local (unlike local response=... at Line 343), so they leak into global scope — inconsistent with the rest of the file and a latent source of accidental cross-function clobbering.

🔧 Suggested fix
-            response=$(curl "${curl_proxy_args[@]}" -s -w "%{http_code}" -o /tmp/tg_response.json \
+            local response http_code
+            response=$(curl "${curl_proxy_args[@]}" -s --max-time 30 -w "%{http_code}" -o /tmp/tg_response.json \
                 -F chat_id="$chat_id" \
                 -F document=@"$log_file;filename=backup_error.log" \
                 -F caption="📜 Backup Error Log - $error_time" \
                 "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument")
 
-            http_code="${response:(-3)}"
+            http_code="${response:(-3)}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [ -f "$log_file" ]; then
response=$(curl "${curl_proxy_args[@]}" -s -w "%{http_code}" -o /tmp/tg_response.json \
-F chat_id="$chat_id" \
-F document=@"$log_file;filename=backup_error.log" \
-F caption="📜 Backup Error Log - $error_time" \
"https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument")
http_code="${response:(-3)}"
if [ "$http_code" -eq 200 ]; then
colorized_echo green "Backup error log sent to Telegram chat $chat_id."
else
colorized_echo red "Failed to send backup error log to Telegram chat $chat_id. HTTP code: $http_code"
cat /tmp/tg_response.json
fi
else
colorized_echo red "Failed to send backup error log to Telegram. HTTP code: $http_code"
cat /tmp/tg_response.json
colorized_echo red "Log file not found: $log_file"
fi
if [ -f "$log_file" ]; then
local response http_code
response=$(curl "${curl_proxy_args[@]}" -s --max-time 30 -w "%{http_code}" -o /tmp/tg_response.json \
-F chat_id="$chat_id" \
-F document=@"$log_file;filename=backup_error.log" \
-F caption="📜 Backup Error Log - $error_time" \
"https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument")
http_code="${response:(-3)}"
if [ "$http_code" -eq 200 ]; then
colorized_echo green "Backup error log sent to Telegram chat $chat_id."
else
colorized_echo red "Failed to send backup error log to Telegram chat $chat_id. HTTP code: $http_code"
cat /tmp/tg_response.json
fi
else
colorized_echo red "Log file not found: $log_file"
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pasarguard-backup.sh` around lines 414 - 431, Update the Telegram
sendDocument curl call in the log-file branch to include the file’s established
--max-time timeout option. Declare response and http_code as local variables in
this function, preserving the existing response parsing and success/failure
handling.

else
colorized_echo red "Log file not found: $log_file"
fi
done
}

backup_service() {
if [ -f "$ENV_FILE" ] && grep -q "^BACKUP_TELEGRAM_CHAT_ID=" "$ENV_FILE"; then
sed -i 's/^BACKUP_TELEGRAM_CHAT_ID=/BACKUP_TELEGRAM_CHAT_IDS=/' "$ENV_FILE"
fi
local telegram_bot_key=""
local telegram_chat_id=""
local telegram_chat_ids=""
local cron_schedule=""
local interval_minutes=""
local backup_proxy_enabled="false"
Expand All @@ -418,7 +450,7 @@ backup_service() {
if grep -q "BACKUP_SERVICE_ENABLED=true" "$ENV_FILE"; then
while true; do
telegram_bot_key=$(awk -F'=' '/^BACKUP_TELEGRAM_BOT_KEY=/ {print $2}' "$ENV_FILE")
telegram_chat_id=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_ID=/ {print $2}' "$ENV_FILE")
telegram_chat_ids=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_IDS=/ {print $2}' "$ENV_FILE")
cron_schedule=$(awk -F'=' '/^BACKUP_CRON_SCHEDULE=/ {print $2}' "$ENV_FILE" | tr -d '"')
backup_proxy_enabled=$(awk -F'=' '/^BACKUP_PROXY_ENABLED=/ {print $2}' "$ENV_FILE")
backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE")
Expand All @@ -433,7 +465,7 @@ backup_service() {
colorized_echo green "====================================="
colorized_echo green "Current Backup Configuration:"
colorized_echo cyan "Telegram Bot API Key: $masked_telegram_bot_key"
colorized_echo cyan "Telegram Chat ID: $telegram_chat_id"
colorized_echo cyan "Telegram Chat ID(s): $telegram_chat_ids"
colorized_echo cyan "Backup Interval: $(format_backup_interval "$interval_minutes" "$cron_schedule")"
if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then
colorized_echo cyan "Proxy: Enabled ($backup_proxy_url)"
Expand Down Expand Up @@ -500,9 +532,9 @@ backup_service() {
done

while true; do
printf "Enter your Telegram chat ID: "
read telegram_chat_id
if [[ -n "$telegram_chat_id" ]]; then
printf "Enter Telegram chat ID(s) (comma-separated for multiple, e.g. 1234567,9876543): "
read telegram_chat_ids
if [[ -n "$telegram_chat_ids" ]]; then
break
else
colorized_echo red "Chat ID cannot be empty. Please try again."
Expand Down Expand Up @@ -558,7 +590,7 @@ backup_service() {

sed -i '/^BACKUP_SERVICE_ENABLED/d' "$ENV_FILE"
sed -i '/^BACKUP_TELEGRAM_BOT_KEY/d' "$ENV_FILE"
sed -i '/^BACKUP_TELEGRAM_CHAT_ID/d' "$ENV_FILE"
sed -i '/^BACKUP_TELEGRAM_CHAT_IDS/d' "$ENV_FILE"
sed -i '/^BACKUP_CRON_SCHEDULE/d' "$ENV_FILE"
sed -i '/^BACKUP_PROXY_ENABLED/d' "$ENV_FILE"
sed -i '/^BACKUP_PROXY_URL/d' "$ENV_FILE"
Expand All @@ -568,7 +600,7 @@ backup_service() {
echo "# Backup service configuration"
echo "BACKUP_SERVICE_ENABLED=true"
echo "BACKUP_TELEGRAM_BOT_KEY=$telegram_bot_key"
echo "BACKUP_TELEGRAM_CHAT_ID=$telegram_chat_id"
echo "BACKUP_TELEGRAM_CHAT_IDS=$telegram_chat_ids"
echo "BACKUP_CRON_SCHEDULE=\"$cron_schedule\""
echo "BACKUP_PROXY_ENABLED=$backup_proxy_enabled"
echo "BACKUP_PROXY_URL=\"$backup_proxy_url\""
Expand Down Expand Up @@ -625,7 +657,7 @@ view_backup_service() {
fi

local telegram_bot_key=$(awk -F'=' '/^BACKUP_TELEGRAM_BOT_KEY=/ {print $2}' "$ENV_FILE")
local telegram_chat_id=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_ID=/ {print $2}' "$ENV_FILE")
local telegram_chat_ids=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_IDS=/ {print $2}' "$ENV_FILE")
local cron_schedule=$(awk -F'=' '/^BACKUP_CRON_SCHEDULE=/ {print $2}' "$ENV_FILE" | tr -d '"')
local backup_proxy_enabled=$(awk -F'=' '/^BACKUP_PROXY_ENABLED=/ {print $2}' "$ENV_FILE")
local backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE")
Expand All @@ -643,7 +675,7 @@ view_backup_service() {
colorized_echo blue "====================================="
colorized_echo green "Status: Enabled"
colorized_echo cyan "Telegram Bot API Key: $masked_telegram_bot_key"
colorized_echo cyan "Telegram Chat ID: $telegram_chat_id"
colorized_echo cyan "Telegram Chat ID(s): $telegram_chat_ids"
colorized_echo cyan "Cron Schedule: $cron_schedule"
colorized_echo cyan "Backup Interval: $(format_backup_interval "$interval_minutes" "$cron_schedule")"
if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then
Expand All @@ -663,7 +695,7 @@ edit_backup_service() {
fi

local telegram_bot_key=$(awk -F'=' '/^BACKUP_TELEGRAM_BOT_KEY=/ {print $2}' "$ENV_FILE")
local telegram_chat_id=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_ID=/ {print $2}' "$ENV_FILE")
local telegram_chat_ids=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_IDS=/ {print $2}' "$ENV_FILE")
local cron_schedule=$(awk -F'=' '/^BACKUP_CRON_SCHEDULE=/ {print $2}' "$ENV_FILE" | tr -d '"')
local backup_proxy_enabled=$(awk -F'=' '/^BACKUP_PROXY_ENABLED=/ {print $2}' "$ENV_FILE")
local backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE")
Expand All @@ -685,7 +717,7 @@ edit_backup_service() {
proxy_display="Enabled ($backup_proxy_url)"
fi
colorized_echo cyan "1. Telegram Bot API Key: $masked_telegram_bot_key"
colorized_echo cyan "2. Telegram Chat ID: $telegram_chat_id"
colorized_echo cyan "2. Telegram Chat ID(s): $telegram_chat_ids"
colorized_echo cyan "3. Backup Interval: $(format_backup_interval "$interval_minutes" "$cron_schedule")"
colorized_echo cyan "4. Proxy: $proxy_display"
colorized_echo yellow "5. Cancel"
Expand All @@ -708,11 +740,11 @@ edit_backup_service() {
;;
2)
while true; do
printf "Enter new Telegram chat ID [current: $telegram_chat_id]: "
read new_chat_id
if [[ -n "$new_chat_id" ]]; then
sed -i "s|^BACKUP_TELEGRAM_CHAT_ID=.*|BACKUP_TELEGRAM_CHAT_ID=$new_chat_id|" "$ENV_FILE"
colorized_echo green "Telegram Chat ID updated successfully."
printf "Enter new Telegram chat ID(s) (comma-separated for multiple, e.g. 1234567,9876543) [current: $telegram_chat_ids]: "
read new_chat_ids
if [[ -n "$new_chat_ids" ]]; then
sed -i "s|^BACKUP_TELEGRAM_CHAT_IDS=.*|BACKUP_TELEGRAM_CHAT_IDS=$new_chat_ids|" "$ENV_FILE"
colorized_echo green "Telegram Chat ID(s) updated successfully."
break
else
colorized_echo red "Chat ID cannot be empty. Please try again."
Expand Down Expand Up @@ -823,7 +855,7 @@ remove_backup_service() {
sed -i '/^# Backup service configuration/d' "$ENV_FILE"
sed -i '/BACKUP_SERVICE_ENABLED/d' "$ENV_FILE"
sed -i '/BACKUP_TELEGRAM_BOT_KEY/d' "$ENV_FILE"
sed -i '/BACKUP_TELEGRAM_CHAT_ID/d' "$ENV_FILE"
sed -i '/BACKUP_TELEGRAM_CHAT_IDS/d' "$ENV_FILE"
sed -i '/BACKUP_CRON_SCHEDULE/d' "$ENV_FILE"
sed -i '/BACKUP_PROXY_ENABLED/d' "$ENV_FILE"
sed -i '/BACKUP_PROXY_URL/d' "$ENV_FILE"
Expand Down