#!/usr/bin/env bash set -euo pipefail # POS: communication telegram — Send Telegram messages/files/links/stickers via Bot API (send, test, config set) # POS_FLAGS: --send --type --caption --parse-mode --no-preview --token --chat-id CONFIG_DIR="$HOME/.config/linux_post_install" CONFIG_FILE="$CONFIG_DIR/telegram.env" API="https://api.telegram.org" usage() { cat < [options] Send a message, link, or media (auto-detects the type) test Send a test message using the current config config Show current config (token masked) config set KEY=VALUE Set TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID Types (auto-detected when --type is omitted): message Plain text (default) link Clickable link (value starts with http:// https:// www.) file Any document (any other existing file) photo .jpg .jpeg .png .bmp video .mp4 .mkv .webm .mov .avi audio .mp3 .wav .flac .m4a .aac voice .ogg .opus .oga animation .gif sticker .webp Options: --type Force a type: message|file|link|sticker|photo|video|audio|voice|animation --caption Caption for file/photo/video/audio/voice/animation --parse-mode Format mode: plain (default), markdown, html (message/link/caption) --markdown Alias for --parse-mode markdown (uniform notify_send contract) --no-preview Disable the link's web page preview (message/link only) --token Override token for one send --chat-id Override chat id for one send Config file: $CONFIG_FILE Keys: TELEGRAM_BOT_TOKEN | TELEGRAM_CHAT_ID Precedence: CLI flags > environment > config file. Examples: pos communication telegram send "Backup finished" pos communication telegram send "/path/to/report.pdf" --caption "Daily report" pos communication telegram send "/path/to/photo.jpg" --caption "Sunset" pos communication telegram send "/path/to/sticker.webp" pos communication telegram send "https://example.com" --no-preview pos communication telegram send "/path/to/video.mp4" --type video pos communication telegram --send "Old style text message" pos communication telegram test EOF exit 0 } err() { echo "ERROR: $*" >&2; exit 1; } load_config() { [ -f "$CONFIG_FILE" ] || return 0 local k v while IFS='=' read -r k v; do [ -n "$k" ] || continue case "$k" in \#*) continue ;; esac v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}" if [ -z "${!k:-}" ]; then export "$k"="$v" fi done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true) } mask_token() { local t="$1" [ -z "$t" ] && { echo "(not set)"; return; } if [ "${#t}" -le 12 ]; then echo "${t:0:4}... (${#t} chars)" else echo "${t:0:6}...${t: -4} (${#t} chars)" fi } send_request() { local endpoint="$1"; shift [ -z "${TELEGRAM_BOT_TOKEN:-}" ] && err "No bot token — run 'pos communication telegram config set TELEGRAM_BOT_TOKEN=...'" [ -z "${TELEGRAM_CHAT_ID:-}" ] && err "No chat id — run 'pos communication telegram config set TELEGRAM_CHAT_ID=...'" curl -fsS -m 60 -X POST "$API/bot${TELEGRAM_BOT_TOKEN}/${endpoint}" "$@" >/dev/null } send_message() { local text="$1" parse_mode="${2:-}" no_preview="${3:-}" local args=(--data-urlencode "chat_id=${TELEGRAM_CHAT_ID:-}" --data-urlencode "text=${text}") [ -n "$parse_mode" ] && args+=(--data-urlencode "parse_mode=${parse_mode}") [ "$no_preview" = "1" ] && args+=(--data-urlencode "disable_web_page_preview=true") send_request sendMessage "${args[@]}" } send_multipart() { local endpoint="$1" file_field="$2" file_path="$3" caption="${4:-}" parse_mode="${5:-}" [ -f "$file_path" ] && [ -r "$file_path" ] || err "File not found or unreadable: $file_path" local args=(-F "chat_id=${TELEGRAM_CHAT_ID:-}" -F "${file_field}=@${file_path}") [ -n "$caption" ] && args+=(-F "caption=${caption}") [ -n "$parse_mode" ] && args+=(-F "parse_mode=${parse_mode}") send_request "$endpoint" "${args[@]}" } detect_type() { local value="$1" ext if [ -f "$value" ] && [ -r "$value" ]; then ext="${value##*.}" ext="${ext,,}" case "$ext" in webp) echo sticker ;; gif) echo animation ;; jpg|jpeg|png|bmp) echo photo ;; mp4|mkv|webm|mov|avi|m4v) echo video ;; ogg|opus|oga) echo voice ;; mp3|wav|flac|m4a|aac) echo audio ;; *) echo file ;; esac elif [[ "$value" =~ ^(https?://|www\.) ]]; then echo link else echo message fi } cmd_send() { local value="" type="" caption="" parse_mode="" no_preview=0 type_explicit=0 auto="" while [ $# -gt 0 ]; do case "$1" in --type) [ $# -ge 2 ] || err "--type needs a value" case "$2" in message|file|link|sticker|photo|video|audio|voice|animation) type="$2" ;; *) err "Unknown type '$2' (allowed: message, file, link, sticker, photo, video, audio, voice, animation)" ;; esac type_explicit=1 shift 2 ;; --caption) [ $# -ge 2 ] || err "--caption needs a value" caption="$2"; shift 2 ;; --markdown) # Alias for --parse-mode markdown (uniform notify_send contract) parse_mode="markdown"; shift ;; --parse-mode) [ $# -ge 2 ] || err "--parse-mode needs a value" case "$2" in plain|markdown|html) ;; *) err "Unknown parse mode '$2' (allowed: plain, markdown, html)" ;; esac parse_mode="$2"; shift 2 ;; --no-preview) no_preview=1; shift ;; --token) [ $# -ge 2 ] || err "--token needs a value" TELEGRAM_BOT_TOKEN="$2"; shift 2 ;; --chat-id) [ $# -ge 2 ] || err "--chat-id needs a value" TELEGRAM_CHAT_ID="$2"; shift 2 ;; --) shift [ $# -ge 1 ] || err "No value given for send" value="$1"; shift [ $# -eq 0 ] || err "Unexpected argument '$1'" ;; -*) err "Unknown option '$1'" ;; *) [ -z "$value" ] || err "Unexpected extra argument '$1'" value="$1"; shift ;; esac done [ -n "$value" ] || err "No value given for send" if [ "$type_explicit" -eq 0 ]; then type="$(detect_type "$value")" auto=1 fi case "$type" in message|link) [ -z "$caption" ] || err "--caption only applies to file/photo/video/audio/voice/animation" ;; sticker) [ -z "$caption" ] || err "--caption is not supported for stickers" ;; esac case "$type" in message|link) ;; *) [ "$no_preview" -eq 0 ] || err "--no-preview only applies to --type message or link" ;; esac [ "$parse_mode" = "plain" ] && parse_mode="" load_config case "$type" in message) send_message "$value" "$parse_mode" "$no_preview" ;; link) send_message "$value" "$parse_mode" "$no_preview" ;; file) send_multipart sendDocument document "$value" "$caption" "$parse_mode" ;; sticker) send_multipart sendSticker sticker "$value" "" "" ;; photo) send_multipart sendPhoto photo "$value" "$caption" "$parse_mode" ;; video) send_multipart sendVideo video "$value" "$caption" "$parse_mode" ;; audio) send_multipart sendAudio audio "$value" "$caption" "$parse_mode" ;; voice) send_multipart sendVoice voice "$value" "$caption" "$parse_mode" ;; animation) send_multipart sendAnimation animation "$value" "$caption" "$parse_mode" ;; esac echo "[+] $type${auto:+ (auto-detected)} sent to chat ${TELEGRAM_CHAT_ID}" } cmd_config() { load_config echo "Config: $CONFIG_FILE" echo " TELEGRAM_BOT_TOKEN = $(mask_token "${TELEGRAM_BOT_TOKEN:-}")" echo " TELEGRAM_CHAT_ID = ${TELEGRAM_CHAT_ID:-}" } cmd_config_set() { local kv="$1" local key="${kv%%=*}" local val="${kv#*=}" case "$key" in TELEGRAM_BOT_TOKEN|TELEGRAM_CHAT_ID) ;; *) err "Unknown key '$key' (allowed: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID)" ;; esac [ -z "$val" ] && err "No value given (expected $key=...)" mkdir -p "$CONFIG_DIR" if [ -f "$CONFIG_FILE" ]; then if grep -q "^${key}=" "$CONFIG_FILE"; then sed -i "s|^${key}=.*|${key}=${val}|" "$CONFIG_FILE" else echo "${key}=${val}" >>"$CONFIG_FILE" fi else echo "${key}=${val}" >"$CONFIG_FILE" fi chmod 600 "$CONFIG_FILE" echo "[+] $key saved to $CONFIG_FILE" } cmd="${1:-}" case "$cmd" in -h|--help) usage ;; send) shift cmd_send "$@" ;; --send) shift [ $# -ge 1 ] || usage cmd_send "$@" ;; test) load_config send_message "Test message from pos $(date '+%Y-%m-%d %H:%M:%S')" echo "[+] test message sent to chat ${TELEGRAM_CHAT_ID}" ;; config) shift case "${1:-}" in set) shift; [ $# -eq 1 ] || usage; cmd_config_set "$1" ;; *) cmd_config ;; esac ;; *) echo "ERROR: Unknown telegram command '$cmd'" echo "Run 'pos communication telegram --help' for usage." exit 1 ;; esac