feat: pos communication telegram listener — /command to bash map + owner-only daemon
This commit is contained in:
+4
-2
@@ -40,8 +40,6 @@ summary (newest last).
|
|||||||
re-run install.sh to refresh `/usr/local/bin`.
|
re-run install.sh to refresh `/usr/local/bin`.
|
||||||
- **Tier 4: `pos new`** — scaffold a new tool from `templates/pos-tool.sh`
|
- **Tier 4: `pos new`** — scaffold a new tool from `templates/pos-tool.sh`
|
||||||
(category, name, POS header, exec bit, doc stubs).
|
(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
|
- **NOT NOW:** per-category `bin/` subdirectories — flat `bin/` + filename
|
||||||
dispatch scales fine; revisit only if `bin/` passes ~40 files.
|
dispatch scales fine; revisit only if `bin/` passes ~40 files.
|
||||||
- **NOT NOW:** split `lib/entertainment-lib.sh` — fine under 600 lines; revisit
|
- **NOT NOW:** split `lib/entertainment-lib.sh` — fine under 600 lines; revisit
|
||||||
@@ -49,6 +47,10 @@ summary (newest last).
|
|||||||
|
|
||||||
## Done (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/
|
- 2026-08-06: NFS in `pos system` — `pos system nfs-server` (status/share/
|
||||||
unshare/list/reload/enable/disable, idempotent /etc/exports edits, generic
|
unshare/list/reload/enable/disable, idempotent /etc/exports edits, generic
|
||||||
default with Tailscale/WireGuard/LAN examples) + `pos system nfs-client`
|
default with Tailscale/WireGuard/LAN examples) + `pos system nfs-client`
|
||||||
|
|||||||
@@ -10,19 +10,19 @@
|
|||||||
|
|
||||||
<!-- GEN:START docmap -->
|
<!-- GEN:START docmap -->
|
||||||
| ## 1. Project Overview | 28–43 |
|
| ## 1. Project Overview | 28–43 |
|
||||||
| ## 2. Directory Structure | 44–167 |
|
| ## 2. Directory Structure | 44–168 |
|
||||||
| ## 3. Installation Flow | 168–219 |
|
| ## 3. Installation Flow | 169–220 |
|
||||||
| ## 4. The `pos` CLI System | 220–279 |
|
| ## 4. The `pos` CLI System | 221–281 |
|
||||||
| ## 5. Shared Library — `lib/common.sh` | 280–310 |
|
| ## 5. Shared Library — `lib/common.sh` | 282–312 |
|
||||||
| ## 6. Docker Compose / ScaleTail | 311–353 |
|
| ## 6. Docker Compose / ScaleTail | 313–355 |
|
||||||
| ## 7. Optional Apps (`apps/`) | 354–383 |
|
| ## 7. Optional Apps (`apps/`) | 356–385 |
|
||||||
| ## 8. Entertainment Module | 384–397 |
|
| ## 8. Entertainment Module | 386–399 |
|
||||||
| ## 9. Systemd Services | 398–408 |
|
| ## 9. Systemd Services | 400–410 |
|
||||||
| ## 10. Configuration Files | 409–432 |
|
| ## 10. Configuration Files | 411–434 |
|
||||||
| ## 11. Coding Conventions | 433–465 |
|
| ## 11. Coding Conventions | 435–467 |
|
||||||
| ## 12. Development Workflow | 466–517 |
|
| ## 12. Development Workflow | 468–519 |
|
||||||
| ## 13. Key File Quick Reference | 518–562 |
|
| ## 13. Key File Quick Reference | 520–565 |
|
||||||
| ## 14. Common Tasks for Agents | 563–587 |
|
| ## 14. Common Tasks for Agents | 566–590 |
|
||||||
<!-- GEN:END docmap -->
|
<!-- GEN:END docmap -->
|
||||||
|
|
||||||
## 1. Project Overview
|
## 1. Project Overview
|
||||||
@@ -57,29 +57,30 @@ Linux_post_install/
|
|||||||
├── bin/ # CLI tools — installed to /usr/local/bin/
|
├── bin/ # CLI tools — installed to /usr/local/bin/
|
||||||
│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts
|
│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts
|
||||||
<!-- GEN:START tree -->
|
<!-- GEN:START tree -->
|
||||||
│ ├── pos-communication-telegram # Send Telegram messages/files/links/stickers via Bot API (send, test, config set)
|
│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash, run them on chat messages
|
||||||
│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config)
|
│ ├── pos-communication-telegram # Send Telegram messages/files/links/stickers via Bot API (send, test, config set)
|
||||||
│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy)
|
│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config)
|
||||||
│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime)
|
│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy)
|
||||||
│ ├── pos-docker-vbox # Disposable Docker-based VMs (create/enter/start/stop/rm/ls)
|
│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime)
|
||||||
│ ├── pos-entertainment-config # Show or edit the entertainment config (ENABLED auto-trigger list, weather location)
|
│ ├── pos-docker-vbox # Disposable Docker-based VMs (create/enter/start/stop/rm/ls)
|
||||||
│ ├── pos-entertainment-disable # Disable a plugin's auto-trigger (remove it from ENABLED)
|
│ ├── pos-entertainment-config # Show or edit the entertainment config (ENABLED auto-trigger list, weather location)
|
||||||
│ ├── pos-entertainment-enable # Enable an auto-trigger for a plugin on a schedule
|
│ ├── pos-entertainment-disable # Disable a plugin's auto-trigger (remove it from ENABLED)
|
||||||
│ ├── pos-entertainment-send # Run a public-API plugin and send its output via Telegram (default sender)
|
│ ├── pos-entertainment-enable # Enable an auto-trigger for a plugin on a schedule
|
||||||
│ ├── pos-entertainment-status # Show enabled plugins and scheduler state
|
│ ├── pos-entertainment-send # Run a public-API plugin and send its output via Telegram (default sender)
|
||||||
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
|
│ ├── pos-entertainment-status # Show enabled plugins and scheduler state
|
||||||
│ ├── pos-media-mp4 # Download video as MP4 (interactive format select)
|
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
|
||||||
│ ├── pos-network-checkport # Check TCP port connectivity
|
│ ├── pos-media-mp4 # Download video as MP4 (interactive format select)
|
||||||
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
|
│ ├── pos-network-checkport # Check TCP port connectivity
|
||||||
│ ├── pos-network-ip # Show interfaces, routes, public IP + location
|
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
|
||||||
│ ├── pos-network-scan # Parallel ping sweep of CIDR
|
│ ├── pos-network-ip # Show interfaces, routes, public IP + location
|
||||||
│ ├── pos-ssh-load-keys # Load all SSH keys into the agent
|
│ ├── pos-network-scan # Parallel ping sweep of CIDR
|
||||||
│ ├── pos-system-backup # Encrypted (AES-256) folder snapshots (tar + gpg)
|
│ ├── pos-ssh-load-keys # Load all SSH keys into the agent
|
||||||
│ ├── pos-system-firewall # Interactive UFW management
|
│ ├── pos-system-backup # Encrypted (AES-256) folder snapshots (tar + gpg)
|
||||||
│ ├── pos-system-health # Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL
|
│ ├── pos-system-firewall # Interactive UFW management
|
||||||
│ ├── pos-system-nfs-client # Mount NFS shares (ephemeral or persistent systemd mount units)
|
│ ├── pos-system-health # Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL
|
||||||
│ ├── pos-system-nfs-server # Manage the NFS kernel server (status, share/unshare exports, enable/disable)
|
│ ├── pos-system-nfs-client # Mount NFS shares (ephemeral or persistent systemd mount units)
|
||||||
│ ├── pos-usb-server # USB Redirector server control (--ls, --share; prompts when args omitted)
|
│ ├── 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)
|
||||||
<!-- GEN:END tree -->
|
<!-- GEN:END tree -->
|
||||||
│ ├── flag-reader # Inspect feature flags (list/status/--raw)
|
│ ├── flag-reader # Inspect feature flags (list/status/--raw)
|
||||||
│ ├── flag-set # Set a feature flag (optionally with a value)
|
│ ├── 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 |
|
| Category | Command | Script | Description |
|
||||||
|----------|---------|--------|-------------|
|
|----------|---------|--------|-------------|
|
||||||
<!-- GEN:START dispatch -->
|
<!-- GEN:START dispatch -->
|
||||||
|
| 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) |
|
| 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 | 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) |
|
| 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):
|
**Scripts that do NOT source common.sh** (self-contained):
|
||||||
<!-- GEN:START selfcontained -->
|
<!-- GEN:START selfcontained -->
|
||||||
`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`.
|
||||||
<!-- GEN:END selfcontained -->
|
<!-- GEN:END selfcontained -->
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -530,13 +532,14 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
|||||||
| `bin/flag-clear` | 21 | Unset a flag |
|
| `bin/flag-clear` | 21 | Unset a flag |
|
||||||
| `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) |
|
| `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) |
|
||||||
<!-- GEN:START filetable -->
|
<!-- GEN:START filetable -->
|
||||||
| `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-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-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-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-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-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-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-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) |
|
| `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-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-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) |
|
| `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 |
|
||||||
<!-- GEN:END filetable -->
|
<!-- GEN:END filetable -->
|
||||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||||
|
|
||||||
|
|||||||
+13
@@ -194,6 +194,7 @@ Subcommands that need input prompt interactively when args are omitted.
|
|||||||
| Command | File | Purpose | Configuration |
|
| 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 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:
|
`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`.
|
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
|
### entertainment
|
||||||
|
|
||||||
**File:** `bin/pos-entertainment-send`
|
**File:** `bin/pos-entertainment-send`
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Telegram messaging and alerts. Tools: `telegram`, `matrix`.
|
|||||||
| Tool | What it does |
|
| Tool | What it does |
|
||||||
|------|--------------|
|
|------|--------------|
|
||||||
| `pos communication telegram` | Send messages/files, manage config, webhook state, message logs |
|
| `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) |
|
| `pos communication matrix` | Matrix/Synapse sender (extensible; not yet implemented) |
|
||||||
|
|
||||||
`telegram` is the workhorse: it backs the whole **notify system** — health
|
`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`)
|
## The notify system (`notify_send`)
|
||||||
|
|
||||||
Every tool that announces something sends through `lib/notify.sh` instead of
|
Every tool that announces something sends through `lib/notify.sh` instead of
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ EXAMPLES
|
|||||||
|
|
||||||
pos communication telegram --send "Backup done"
|
pos communication telegram --send "Backup done"
|
||||||
Send a Telegram message
|
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 weather Fetch Open-Meteo weather → Telegram
|
||||||
pos entertainment send joke --print Preview a joke locally (no send)
|
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"; }
|
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
||||||
|
|
||||||
# Commands that read from stdin interactively — only log invocation
|
# 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
|
for ((i=n-1; i>=0; i--)); do
|
||||||
cmd="pos"
|
cmd="pos"
|
||||||
|
|||||||
Executable
+408
@@ -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 <<EOF
|
||||||
|
Usage: pos communication telegram listener [command]
|
||||||
|
|
||||||
|
Telegram bot listener: map /command → bash commands and run them from chat.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
(none) Interactive editor for the /command → bash map
|
||||||
|
--enable Install + start the systemd user service (autostarts on login)
|
||||||
|
--disable Stop + disable + remove the service
|
||||||
|
--status Show service state and the command map
|
||||||
|
--run Run the polling loop in the foreground (used by the service)
|
||||||
|
|
||||||
|
Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID)
|
||||||
|
Map: $MAP_FILE — one '/cmd=bash command' per line
|
||||||
|
|
||||||
|
The listener only reacts to the owner chat (TELEGRAM_CHAT_ID). Commands run
|
||||||
|
as your user, so 'sudo' inside them needs a NOPASSWD rule. The interactive
|
||||||
|
editor runs 'bash -n' to syntax-check commands before saving.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos communication telegram listener
|
||||||
|
pos communication telegram listener --enable
|
||||||
|
pos communication telegram listener --status
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── telegram.env (same pattern as pos-communication-telegram) ────
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── command map (MAP_FILE) ──────────────────────────────────────
|
||||||
|
# Lines: /cmd=bash command. Keys keep the leading slash; read via awk so
|
||||||
|
# values may contain '='. The map is re-read per message — edits apply
|
||||||
|
# without restarting the listener.
|
||||||
|
|
||||||
|
map_entries() {
|
||||||
|
[ -f "$MAP_FILE" ] || return 0
|
||||||
|
grep -E '^/[A-Za-z0-9_.-]+=' "$MAP_FILE" | while IFS= read -r line; do
|
||||||
|
printf '%s|%s\n' "${line%%=*}" "${line#*=}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
map_has() {
|
||||||
|
[ -f "$MAP_FILE" ] && awk -F= -v k="$1" '$1==k{exit 0} END{exit 1}' "$MAP_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
map_get() {
|
||||||
|
[ -f "$MAP_FILE" ] || return 0
|
||||||
|
awk -F= -v k="$1" '$1==k{sub(/^[^=]*=/,""); print}' "$MAP_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
map_set() {
|
||||||
|
local cmd="$1" value="$2"
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
touch "$MAP_FILE"
|
||||||
|
chmod 600 "$MAP_FILE"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
awk -v k="$cmd" 'index($0, k "=") != 1 { print }' "$MAP_FILE" > "$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" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=pos Telegram listener (/command to bash)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=$runner --run
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=HOME=$HOME
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
EOF
|
||||||
|
chmod 644 "$USER_SYSTEMD_DIR/$SERVICE"
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now "$SERVICE"
|
||||||
|
log "listener service enabled: $SERVICE"
|
||||||
|
warn "commands run as $(id -un) — 'sudo' inside them needs a NOPASSWD rule"
|
||||||
|
if command -v loginctl >/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<n; i++)); do
|
||||||
|
local u text chat from_id msg_id
|
||||||
|
u="$(printf '%s' "$resp" | jq -r ".result[$i].update_id")"
|
||||||
|
text="$(printf '%s' "$resp" | jq -r ".result[$i].message.text // empty")"
|
||||||
|
chat="$(printf '%s' "$resp" | jq -r ".result[$i].message.chat.id // empty")"
|
||||||
|
from_id="$(printf '%s' "$resp" | jq -r ".result[$i].message.from.id // empty")"
|
||||||
|
msg_id="$(printf '%s' "$resp" | jq -r ".result[$i].message.message_id // empty")"
|
||||||
|
offset=$((u + 1))
|
||||||
|
[ -n "$text" ] || continue
|
||||||
|
if [ -n "$chat" ] && [ "$chat" != "$TELEGRAM_CHAT_ID" ] && [ "$from_id" != "$TELEGRAM_CHAT_ID" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
handle_message "$text" "$msg_id"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
--enable) enable_service ;;
|
||||||
|
--disable) disable_service ;;
|
||||||
|
--status) status ;;
|
||||||
|
--run) run_daemon ;;
|
||||||
|
"") ui ;;
|
||||||
|
*) err "Unknown option '$1' (see --help)" ;;
|
||||||
|
esac
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
# Install: source this file in ~/.bashrc or place in /etc/bash_completion.d/
|
# Install: source this file in ~/.bashrc or place in /etc/bash_completion.d/
|
||||||
# GEN:START posflags
|
# GEN:START posflags
|
||||||
declare -A _pos_flags
|
declare -A _pos_flags
|
||||||
|
_pos_flags[communication-telegram-listener]="--enable --disable --status --run"
|
||||||
_pos_flags[communication-telegram]="--send --type --caption --parse-mode --no-preview --token --chat-id"
|
_pos_flags[communication-telegram]="--send --type --caption --parse-mode --no-preview --token --chat-id"
|
||||||
_pos_flags[entertainment-send]="--print --markdown"
|
_pos_flags[entertainment-send]="--print --markdown"
|
||||||
_pos_flags[network-hotspot]="--foreground"
|
_pos_flags[network-hotspot]="--foreground"
|
||||||
|
|||||||
Reference in New Issue
Block a user