diff --git a/AGENT_TODO.md b/AGENT_TODO.md index 6f3a1d7..996db8f 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -40,8 +40,6 @@ summary (newest last). re-run install.sh to refresh `/usr/local/bin`. - **Tier 4: `pos new`** — scaffold a new tool from `templates/pos-tool.sh` (category, name, POS header, exec bit, doc stubs). -- **NOT NOW:** Telegram inbound bot (command handling) — outbound alerting - covers current needs; revisit if remote control is wanted. - **NOT NOW:** per-category `bin/` subdirectories — flat `bin/` + filename dispatch scales fine; revisit only if `bin/` passes ~40 files. - **NOT NOW:** split `lib/entertainment-lib.sh` — fine under 600 lines; revisit @@ -49,6 +47,10 @@ summary (newest last). ## Done (summary, newest last) +- 2026-08-06: Telegram **listener** — `pos communication telegram listener`: + interactive `/command` → bash map editor + owner-only polling daemon as a + systemd user service (map in `~/.config/linux_post_install/telegram_commands.env`, + re-read per message; `/help`, unknown-command reply, 60s timeout, stdout reply). - 2026-08-06: NFS in `pos system` — `pos system nfs-server` (status/share/ unshare/list/reload/enable/disable, idempotent /etc/exports edits, generic default with Tailscale/WireGuard/LAN examples) + `pos system nfs-client` diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 968f312..f68cc7f 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -10,19 +10,19 @@ | ## 1. Project Overview | 28–43 | -| ## 2. Directory Structure | 44–167 | -| ## 3. Installation Flow | 168–219 | -| ## 4. The `pos` CLI System | 220–279 | -| ## 5. Shared Library — `lib/common.sh` | 280–310 | -| ## 6. Docker Compose / ScaleTail | 311–353 | -| ## 7. Optional Apps (`apps/`) | 354–383 | -| ## 8. Entertainment Module | 384–397 | -| ## 9. Systemd Services | 398–408 | -| ## 10. Configuration Files | 409–432 | -| ## 11. Coding Conventions | 433–465 | -| ## 12. Development Workflow | 466–517 | -| ## 13. Key File Quick Reference | 518–562 | -| ## 14. Common Tasks for Agents | 563–587 | +| ## 2. Directory Structure | 44–168 | +| ## 3. Installation Flow | 169–220 | +| ## 4. The `pos` CLI System | 221–281 | +| ## 5. Shared Library — `lib/common.sh` | 282–312 | +| ## 6. Docker Compose / ScaleTail | 313–355 | +| ## 7. Optional Apps (`apps/`) | 356–385 | +| ## 8. Entertainment Module | 386–399 | +| ## 9. Systemd Services | 400–410 | +| ## 10. Configuration Files | 411–434 | +| ## 11. Coding Conventions | 435–467 | +| ## 12. Development Workflow | 468–519 | +| ## 13. Key File Quick Reference | 520–565 | +| ## 14. Common Tasks for Agents | 566–590 | ## 1. Project Overview @@ -57,29 +57,30 @@ Linux_post_install/ ├── bin/ # CLI tools — installed to /usr/local/bin/ │ ├── pos # Main dispatcher — smart arg matching to pos-* scripts -│ ├── pos-communication-telegram # Send Telegram messages/files/links/stickers via Bot API (send, test, config set) -│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config) -│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy) -│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime) -│ ├── pos-docker-vbox # Disposable Docker-based VMs (create/enter/start/stop/rm/ls) -│ ├── pos-entertainment-config # Show or edit the entertainment config (ENABLED auto-trigger list, weather location) -│ ├── pos-entertainment-disable # Disable a plugin's auto-trigger (remove it from ENABLED) -│ ├── pos-entertainment-enable # Enable an auto-trigger for a plugin on a schedule -│ ├── pos-entertainment-send # Run a public-API plugin and send its output via Telegram (default sender) -│ ├── pos-entertainment-status # Show enabled plugins and scheduler state -│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp) -│ ├── pos-media-mp4 # Download video as MP4 (interactive format select) -│ ├── pos-network-checkport # Check TCP port connectivity -│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui -│ ├── pos-network-ip # Show interfaces, routes, public IP + location -│ ├── pos-network-scan # Parallel ping sweep of CIDR -│ ├── pos-ssh-load-keys # Load all SSH keys into the agent -│ ├── pos-system-backup # Encrypted (AES-256) folder snapshots (tar + gpg) -│ ├── pos-system-firewall # Interactive UFW management -│ ├── pos-system-health # Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL -│ ├── pos-system-nfs-client # Mount NFS shares (ephemeral or persistent systemd mount units) -│ ├── pos-system-nfs-server # Manage the NFS kernel server (status, share/unshare exports, enable/disable) -│ ├── pos-usb-server # USB Redirector server control (--ls, --share; prompts when args omitted) +│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash, run them on chat messages +│ ├── pos-communication-telegram # Send Telegram messages/files/links/stickers via Bot API (send, test, config set) +│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config) +│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy) +│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime) +│ ├── pos-docker-vbox # Disposable Docker-based VMs (create/enter/start/stop/rm/ls) +│ ├── pos-entertainment-config # Show or edit the entertainment config (ENABLED auto-trigger list, weather location) +│ ├── pos-entertainment-disable # Disable a plugin's auto-trigger (remove it from ENABLED) +│ ├── pos-entertainment-enable # Enable an auto-trigger for a plugin on a schedule +│ ├── pos-entertainment-send # Run a public-API plugin and send its output via Telegram (default sender) +│ ├── pos-entertainment-status # Show enabled plugins and scheduler state +│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp) +│ ├── pos-media-mp4 # Download video as MP4 (interactive format select) +│ ├── pos-network-checkport # Check TCP port connectivity +│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui +│ ├── pos-network-ip # Show interfaces, routes, public IP + location +│ ├── pos-network-scan # Parallel ping sweep of CIDR +│ ├── pos-ssh-load-keys # Load all SSH keys into the agent +│ ├── pos-system-backup # Encrypted (AES-256) folder snapshots (tar + gpg) +│ ├── pos-system-firewall # Interactive UFW management +│ ├── pos-system-health # Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL +│ ├── pos-system-nfs-client # Mount NFS shares (ephemeral or persistent systemd mount units) +│ ├── pos-system-nfs-server # Manage the NFS kernel server (status, share/unshare exports, enable/disable) +│ ├── pos-usb-server # USB Redirector server control (--ls, --share; prompts when args omitted) │ ├── flag-reader # Inspect feature flags (list/status/--raw) │ ├── flag-set # Set a feature flag (optionally with a value) @@ -236,6 +237,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst | Category | Command | Script | Description | |----------|---------|--------|-------------| +| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages | | communication | telegram | `pos-communication-telegram` | Send Telegram messages/files/links/stickers via Bot API (send, test, config set) | | docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) | @@ -303,7 +305,7 @@ source "$(dirname "$0")/../lib/common.sh" **Scripts that do NOT source common.sh** (self-contained): -`pos`, `pos-communication-telegram`, `pos-network-checkport`, `pos-network-hotspot`, `pos-network-ip`, `pos-network-scan`, `pos-ssh-load-keys`, `pos-system-firewall`. +`pos`, `pos-communication-telegram`, `pos-communication-telegram-listener`, `pos-network-checkport`, `pos-network-hotspot`, `pos-network-ip`, `pos-network-scan`, `pos-ssh-load-keys`, `pos-system-firewall`. --- @@ -530,13 +532,14 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/flag-clear` | 21 | Unset a flag | | `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) | -| `bin/pos` | 216 | CLI dispatcher with smart arg matching + logging + category help | +| `bin/pos` | 219 | CLI dispatcher with smart arg matching + logging + category help | +| `bin/pos-communication-telegram-listener` | 408 | Telegram bot listener: map /command → bash, run them on chat messages | | `bin/pos-communication-telegram` | 274 | Send Telegram messages/files/links/stickers via Bot API (send, test, config set) | | `bin/pos-docker-compose` | 364 | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | `bin/pos-docker-health` | 110 | One-glance container health dashboard (exits 1 if unhealthy) | | `bin/pos-docker-ps` | 128 | Enhanced container overview (health, IPs, ports, uptime) | | `bin/pos-docker-vbox` | 157 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | -| `bin/pos-entertainment-config` | 85 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) | +| `bin/pos-entertainment-config` | 98 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) | | `bin/pos-entertainment-disable` | 32 | Disable a plugin's auto-trigger (remove it from ENABLED) | | `bin/pos-entertainment-enable` | 50 | Enable an auto-trigger for a plugin on a schedule | | `bin/pos-entertainment-send` | 93 | Run a public-API plugin and send its output via Telegram (default sender) | @@ -554,7 +557,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-system-nfs-client` | 138 | Mount NFS shares (ephemeral or persistent systemd mount units) | | `bin/pos-system-nfs-server` | 134 | Manage the NFS kernel server (status, share/unshare exports, enable/disable) | | `bin/pos-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) | -| `completions/pos.bash` | 189 | Dynamic bash completion | +| `completions/pos.bash` | 190 | Dynamic bash completion | | `apps/install.sh` | 171 | App install/uninstall picker/orchestrator | diff --git a/DOC/POS.md b/DOC/POS.md index 774cfda..b7c6fed 100644 --- a/DOC/POS.md +++ b/DOC/POS.md @@ -194,6 +194,7 @@ Subcommands that need input prompt interactively when args are omitted. | Command | File | Purpose | Configuration | |---------|------|---------|---------------| | `pos communication telegram send "text"` | `bin/pos-communication-telegram` | Send a message, link, or media file (auto-detects the type) to a Telegram chat via the Bot API | Token + chat ID from `~/.config/linux_post_install/telegram.env` (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, chmod 600). Precedence: `--token`/`--chat-id` flags > env > config file | +| `pos communication telegram listener` | `bin/pos-communication-telegram-listener` | Telegram bot listener: map `/command` → bash commands and run them from chat; interactive editor for the map | Same `telegram.env` (the bot is the owner, `TELEGRAM_CHAT_ID`). Map lives in `~/.config/linux_post_install/telegram_commands.env` (`/cmd=bash command` lines, chmod 600) | `pos communication telegram` in detail: @@ -216,6 +217,18 @@ Subcommands that need input prompt interactively when args are omitted. The bot token is a secret — it is stored only in `~/.config/linux_post_install/telegram.env` and never in the repo. Requires network access to `api.telegram.org`. +`pos communication telegram listener` in detail: + +| Command | Behavior | +|---------|----------| +| `pos communication telegram listener` | Interactive editor for the `/command` → bash map (`a`dd / `e`dit / `r`emove / `t`est / `q`uit); test-runs run `bash -n` first and may execute the command live | +| `pos communication telegram listener --status` | Shows service state (running/autostart), config + map file paths, and the mapped commands | +| `pos communication telegram listener --enable` | Installs + starts a systemd **user** service (`pos-telegram-listener.service`); the daemon polls `getUpdates` and runs mapped commands | +| `pos communication telegram listener --disable` | Stops, disables, and removes the service | +| `pos communication telegram listener --run` | Run the polling loop in the foreground (what the service executes) | + +The map file is re-read for every message — edits apply without a restart. The listener only reacts to the owner chat (`TELEGRAM_CHAT_ID`); anyone else's message is ignored. `/help` lists mapped commands; an unmapped command replies "Unknown command". Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`), so `sudo` inside them needs a NOPASSWD rule. `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`. + ### entertainment **File:** `bin/pos-entertainment-send` diff --git a/DOC/howto/communication.md b/DOC/howto/communication.md index 376474d..83d82cc 100644 --- a/DOC/howto/communication.md +++ b/DOC/howto/communication.md @@ -5,6 +5,7 @@ Telegram messaging and alerts. Tools: `telegram`, `matrix`. | Tool | What it does | |------|--------------| | `pos communication telegram` | Send messages/files, manage config, webhook state, message logs | +| `pos communication telegram listener` | Bot listener: map `/command` → bash and run it from chat (systemd user daemon) | | `pos communication matrix` | Matrix/Synapse sender (extensible; not yet implemented) | `telegram` is the workhorse: it backs the whole **notify system** — health @@ -71,6 +72,44 @@ pos communication telegram clear-logs # wipe message logs --- +## `pos communication telegram listener` + +Turns your bot into a two-way remote control: map `/command` → bash, then +message the bot from your phone to run it. + +```bash +pos communication telegram listener # edit the /command → bash map +pos communication telegram listener --status # service state + mapped commands +pos communication telegram listener --enable # install the systemd user daemon +pos communication telegram listener --disable # remove it +``` + +- **Map file:** `~/.config/linux_post_install/telegram_commands.env` (chmod 600), + one `/cmd=bash command` per line — re-read on every message, so edits apply + instantly. Example: + ``` + /status=systemctl --user is-active pos-telegram-listener.service + /temp=sensors | grep -i 'Tctl\|package id 0' + /update=cd /path/to/repo && git pull + ``` +- **Owner-only:** the bot only reacts to `TELEGRAM_CHAT_ID` (your own chat); + others are ignored. `/help` lists mapped commands; unknown → "Unknown command". +- **Runs as you:** mapped commands execute as your user with a 60s timeout, + stdout + stderr are replied to the chat (truncated ~3800 chars; empty → `OK`). + `sudo` inside a command needs a NOPASSWD rule. +- **Daemon lifecycle:** the service is a systemd **user** unit; it stops at + logout unless you enable linger: `sudo loginctl enable-linger $(whoami)`. + `--enable` prints this warning if linger is off. + +**Troubleshooting:** +- Bot doesn't answer → send `/help`; if silent, check the service with + `systemctl --user status pos-telegram-listener.service`. +- A webhook on the bot blocks `getUpdates` → delete it with + `pos communication telegram setwebhook ""`. +- Needs `jq` (in preinstall PACKAGES). + +--- + ## The notify system (`notify_send`) Every tool that announces something sends through `lib/notify.sh` instead of diff --git a/bin/pos b/bin/pos index 11e9c35..e01dda2 100755 --- a/bin/pos +++ b/bin/pos @@ -116,6 +116,9 @@ EXAMPLES pos communication telegram --send "Backup done" Send a Telegram message + pos communication telegram listener Edit the /command → bash map + pos communication telegram listener --enable + Install the Telegram bot listener pos entertainment send weather Fetch Open-Meteo weather → Telegram pos entertainment send joke --print Preview a joke locally (no send) @@ -180,7 +183,7 @@ MAIN_LOG="$LOG_DIR/pos.log" log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; } # Commands that read from stdin interactively — only log invocation -INTERACTIVE_CMDS="system-firewall media-mp4 system-backup usb-server" +INTERACTIVE_CMDS="system-firewall media-mp4 system-backup usb-server communication-telegram-listener" for ((i=n-1; i>=0; i--)); do cmd="pos" diff --git a/bin/pos-communication-telegram-listener b/bin/pos-communication-telegram-listener new file mode 100755 index 0000000..66700f1 --- /dev/null +++ b/bin/pos-communication-telegram-listener @@ -0,0 +1,408 @@ +#!/usr/bin/env bash +set -euo pipefail +# POS: communication telegram-listener — Telegram bot listener: map /command → bash, run them on chat messages +# POS_FLAGS: --enable --disable --status --run + +CONFIG_DIR="$HOME/.config/linux_post_install" +CONFIG_FILE="$CONFIG_DIR/telegram.env" +MAP_FILE="$CONFIG_DIR/telegram_commands.env" +API="https://api.telegram.org" +SERVICE="pos-telegram-listener.service" +USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" + +err() { echo "ERROR: $*" >&2; exit 1; } +log() { echo "[+] $*"; } +warn() { echo "[!] $*"; } + +usage() { + cat < "$tmp" + printf '%s=%s\n' "$cmd" "$value" >> "$tmp" + mv "$tmp" "$MAP_FILE" + chmod 600 "$MAP_FILE" +} + +map_del() { + [ -f "$MAP_FILE" ] || return 0 + local cmd="$1" tmp + tmp="$(mktemp)" + awk -v k="$cmd" 'index($0, k "=") != 1 { print }' "$MAP_FILE" > "$tmp" + mv "$tmp" "$MAP_FILE" + chmod 600 "$MAP_FILE" +} + +MAP_CMDS=(); MAP_VALS=(); MAP_N=0 +load_map() { + MAP_CMDS=(); MAP_VALS=(); MAP_N=0 + [ -f "$MAP_FILE" ] || return 0 + local i=0 line cmd value + while IFS='|' read -r cmd value; do + [ -n "$cmd" ] || continue + i=$((i + 1)) + MAP_CMDS[$i]="$cmd"; MAP_VALS[$i]="$value" + done <<< "$(map_entries)" + MAP_N="$i" +} + +map_cmds_list() { + load_map + local out="" i + for ((i=1; i<=MAP_N; i++)); do + [ -n "$out" ] && out+=", " + out+="${MAP_CMDS[$i]}" + done + printf '%s' "${out:-none}" +} + +check_syntax() { + bash -n -c "$1" 2>&1 +} + +# ── interactive editor ────────────────────────────────────────── +ui_pick() { + load_map + if [ "$MAP_N" -eq 0 ]; then + warn "no commands mapped yet — add one first" + return 1 + fi + echo + local i + for ((i=1; i<=MAP_N; i++)); do + printf ' %2d) %-16s -> %s\n' "$i" "${MAP_CMDS[$i]}" "${MAP_VALS[$i]}" + done + local idx + read -rp "Entry number: " idx + if ! [[ "$idx" =~ ^[0-9]+$ ]] || (( idx < 1 || idx > MAP_N )); then + warn "invalid number '$idx'" + return 1 + fi + echo "$idx" +} + +ui_run_command() { + local value="$1" output rc + echo + echo "--- running: $value" + if output="$(timeout 60 bash -c "$value" 2>&1)"; then + rc=0 + else + rc=$? + fi + printf '%s\n' "$output" | head -c 3800 + [ -z "$output" ] || echo + echo "--- exit $rc" +} + +ui_add() { + local cmd value out + read -rp "/command name (e.g. /status): " cmd + [ -n "$cmd" ] || { warn "empty command name"; return; } + case "$cmd" in + /[A-Za-z0-9_.-]*) ;; + *) warn "command must start with '/' and use [A-Za-z0-9_.-]: $cmd"; return ;; + esac + read -rp "bash command: " value + [ -n "$value" ] || { warn "empty bash command"; return; } + if out="$(check_syntax "$value")"; then + map_set "$cmd" "$value" + log "saved $cmd -> $value" + else + warn "syntax error — not saved:" + printf '%s\n' "$out" | sed 's/^/ /' + return + fi + local yn + read -rp "Test-run it now? [y/N] " yn + case "$yn" in + y|Y) ui_run_command "$value" ;; + esac +} + +ui_edit() { + local idx cmd value out + idx="$(ui_pick)" || return + cmd="${MAP_CMDS[$idx]}" + echo "Editing: $cmd -> ${MAP_VALS[$idx]}" + read -rp "bash command: " value + [ -n "$value" ] || { warn "empty bash command"; return; } + if out="$(check_syntax "$value")"; then + map_set "$cmd" "$value" + log "updated $cmd -> $value" + else + warn "syntax error — not saved:" + printf '%s\n' "$out" | sed 's/^/ /' + fi +} + +ui_remove() { + local idx cmd yn + idx="$(ui_pick)" || return + cmd="${MAP_CMDS[$idx]}" + read -rp "Remove '$cmd'? [y/N] " yn + case "$yn" in + y|Y) map_del "$cmd"; log "removed $cmd" ;; + *) warn "canceled" ;; + esac +} + +ui_test() { + local idx + idx="$(ui_pick)" || return + ui_run_command "${MAP_VALS[$idx]}" +} + +ui() { + local choice + while true; do + echo + echo "Telegram listener — /command -> bash map" + echo "-----------------------------------------" + load_map + if [ "$MAP_N" -eq 0 ]; then + echo " (no commands mapped yet)" + else + local i + for ((i=1; i<=MAP_N; i++)); do + printf ' %2d) %-16s -> %s\n' "$i" "${MAP_CMDS[$i]}" "${MAP_VALS[$i]}" + done + fi + echo + read -rp "Menu: [a]dd [e]dit [r]emove [t]est [q]uit > " choice + case "$choice" in + a|A|add) ui_add ;; + e|E|edit) ui_edit ;; + r|R|remove) ui_remove ;; + t|T|test) ui_test ;; + q|Q|quit|exit) echo; log "bye"; return 0 ;; + *) warn "unknown choice '$choice'" ;; + esac + done +} + +# ── systemd user service ──────────────────────────────────────── +enable_service() { + command -v systemctl &>/dev/null || err "systemctl not found — cannot create the listener service" + mkdir -p "$USER_SYSTEMD_DIR" + + local runner + if [ -x /usr/local/bin/pos-communication-telegram-listener ]; then + runner=/usr/local/bin/pos-communication-telegram-listener + else + runner="$(cd "$(dirname "$0")/.." && pwd)/bin/pos-communication-telegram-listener" + warn "using repo path $runner — re-run 'install.sh' so the service survives a deleted repo" + fi + + cat >"$USER_SYSTEMD_DIR/$SERVICE" </dev/null 2>&1; then + if ! loginctl show-user "$(id -un)" 2>/dev/null | grep -q '^Linger=yes'; then + warn "enable linger so the listener survives logout: sudo loginctl enable-linger $(id -un)" + fi + fi +} + +disable_service() { + if [ ! -f "$USER_SYSTEMD_DIR/$SERVICE" ]; then + warn "no listener service installed ($SERVICE)" + exit 0 + fi + systemctl --user disable --now "$SERVICE" 2>/dev/null || true + rm -f "$USER_SYSTEMD_DIR/$SERVICE" + systemctl --user daemon-reload + log "listener service disabled" +} + +status() { + if systemctl --user is-active --quiet "$SERVICE" 2>/dev/null; then + echo "listener: running" + else + echo "listener: not running" + fi + if systemctl --user is-enabled "$SERVICE" >/dev/null 2>&1; then + echo "autostart: enabled (starts on login)" + else + echo "autostart: disabled" + fi + echo "config: $CONFIG_FILE" + echo "map file: $MAP_FILE" + load_map + echo "commands: $MAP_N mapped" + local i + for ((i=1; i<=MAP_N; i++)); do + printf ' %-16s -> %s\n' "${MAP_CMDS[$i]}" "${MAP_VALS[$i]}" + done +} + +# ── polling daemon ────────────────────────────────────────────── +reply() { + local text="$1" msg_id="$2" rc="${3:-}" + text="${text:0:3800}" + local args=(--data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" --data-urlencode "text=${text}") + [ -n "$msg_id" ] && args+=(--data-urlencode "reply_to_message_id=${msg_id}") + curl -fsS -m 60 -X POST "${API}/bot${TELEGRAM_BOT_TOKEN}/sendMessage" "${args[@]}" >/dev/null 2>&1 \ + || warn "reply failed (rc ${rc:-?})" +} + +handle_message() { + local text="$1" msg_id="$2" value output rc + case "$text" in + /help|/start) + reply "Mapped commands: $(map_cmds_list)" "$msg_id" + return ;; + esac + value="$(map_get "$text")" + if [ -z "$value" ]; then + reply "Unknown command: $text (send /help)" "$msg_id" + return + fi + log "exec: $text" + if output="$(timeout 60 bash -c "$value" 2>&1)"; then + rc=0 + else + rc=$? + fi + if [ -z "$output" ]; then + output="OK" + fi + if [ "$rc" -ne 0 ]; then + reply "$(printf 'exit %s\n%s' "$rc" "$output")" "$msg_id" "$rc" + else + reply "$output" "$msg_id" + fi +} + +run_daemon() { + command -v jq &>/dev/null || err "jq not found (install jq — in preinstall PACKAGES)" + load_config + [ -n "${TELEGRAM_BOT_TOKEN:-}" ] || err "No bot token — run 'pos communication telegram config set TELEGRAM_BOT_TOKEN=...'" + [ -n "${TELEGRAM_CHAT_ID:-}" ] || err "No chat id — run 'pos communication telegram config set TELEGRAM_CHAT_ID=...'" + + local offset=0 + log "listener running (owner chat ${TELEGRAM_CHAT_ID}) — Ctrl+C to stop" + while true; do + local resp n i + resp="$(curl -fsS -m 45 "${API}/bot${TELEGRAM_BOT_TOKEN}/getUpdates" \ + --data-urlencode "timeout=30" \ + --data-urlencode "offset=${offset}" \ + --data-urlencode 'allowed_updates=["message"]' 2>/dev/null)" || { sleep 5; continue; } + if [ "$(printf '%s' "$resp" | jq -r '.ok // false')" != "true" ]; then + sleep 5 + continue + fi + n="$(printf '%s' "$resp" | jq -r '.result | length')" + for ((i=0; i