From 2aaf5cff62f7472ab1649d8018c37b9a5ada89b7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 5 Aug 2026 14:45:51 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20add=20entertainment=20module=20?= =?UTF-8?q?=E2=80=94=20public-API=20plugins=20(weather,=20joke)=20sent=20v?= =?UTF-8?q?ia=20Telegram=20by=20default=20(pos=20entertainment=20send)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DOC/AGENT_Context_Project.md | 13 +++- DOC/DEV.md | 36 +++++++++++ DOC/POS.md | 26 ++++++++ README.md | 2 +- bin/pos | 7 +- bin/pos-entertainment-send | 122 +++++++++++++++++++++++++++++++++++ completions/pos.bash | 4 ++ entertainment/joke.sh | 32 +++++++++ entertainment/weather.sh | 83 ++++++++++++++++++++++++ install.sh | 10 +++ scripts/gen-docs.sh | 2 +- 11 files changed, 330 insertions(+), 7 deletions(-) create mode 100755 bin/pos-entertainment-send create mode 100755 entertainment/joke.sh create mode 100755 entertainment/weather.sh diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 55d957f..3f3dc5f 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -40,6 +40,7 @@ Linux_post_install/ │ ├── 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-send # Run a public-API plugin and send its output via Telegram (default sender) │ ├── 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 @@ -61,6 +62,10 @@ Linux_post_install/ ├── features/ # User-customizable scripts (installed via --feature) │ └── autostart.sh # Boot-time script (via systemd, flag-gated) │ +├── entertainment/ # Public-API plugins for pos entertainment send (→ /usr/local/share/linux_post_install/entertainment) +│ ├── weather.sh # Current weather via Open-Meteo (no API key) +│ └── joke.sh # Random dad joke via icanhazdadjoke (no API key) +│ ├── templates/ # Dev-only scaffolds — NOT installed by install.sh │ ├── pos-tool.sh # New `pos` CLI tool (→ bin/pos--) │ ├── app.sh # New optional app installer (→ apps//.sh) @@ -205,6 +210,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst | docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) | | docker | ps | `pos-docker-ps` | Enhanced container overview (health, IPs, ports, uptime) | | docker | vbox | `pos-docker-vbox` | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | +| entertainment | send | `pos-entertainment-send` | Run a public-API plugin and send its output via Telegram (default sender) | | media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) | | media | mp4 | `pos-media-mp4` | Download video as MP4 (interactive format select) | | network | checkport | `pos-network-checkport` | Check TCP port connectivity | @@ -470,12 +476,13 @@ 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` | 208 | CLI dispatcher with smart arg matching + logging + category help | -| `bin/pos-communication-telegram` | 140 | Send Telegram messages via Bot API (--send, test, config set) | +| `bin/pos` | 211 | CLI dispatcher with smart arg matching + logging + category help | +| `bin/pos-communication-telegram` | 150 | Send Telegram messages 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-send` | 122 | Run a public-API plugin and send its output via Telegram (default sender) | | `bin/pos-media-mp3` | 35 | Download audio as MP3 (yt-dlp) | | `bin/pos-media-mp4` | 38 | Download video as MP4 (interactive format select) | | `bin/pos-network-checkport` | 45 | Check TCP port connectivity | @@ -486,7 +493,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-system-backup` | 117 | Encrypted (AES-256) folder snapshots (tar + gpg) | | `bin/pos-system-firewall` | 285 | Interactive UFW management | | `bin/pos-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) | -| `completions/pos.bash` | 146 | Dynamic bash completion | +| `completions/pos.bash` | 150 | Dynamic bash completion | | `apps/install.sh` | 171 | App install/uninstall picker/orchestrator | diff --git a/DOC/DEV.md b/DOC/DEV.md index 46d192e..5eed107 100644 --- a/DOC/DEV.md +++ b/DOC/DEV.md @@ -33,6 +33,7 @@ Each phase is independent and runs only if the corresponding script exists. | `apps//` | Optional desktop app installers | run on demand | | `lib/` | Shared library (`common.sh`) | sourced at build time | | `config/` | Gitignored user config files | `~/.config//` (via postinstall) | +| `entertainment/` | Public-API plugins for the entertainment module | `/usr/local/share/linux_post_install/entertainment` (via install.sh Phase 2) | | `compose/` | ScaleTail templates (git submodule) | `/usr/local/share/linux_post_install/scale-tail` | | `systemd/` | Systemd unit files | `/etc/systemd/system/` (via postinstall) | @@ -176,6 +177,41 @@ make check # full self-consistency gate (syntax, exec bits, d --- +## Adding an Entertainment Plugin + +The `entertainment` module routes public-API data to Telegram via the single runner `pos entertainment send ` (`bin/pos-entertainment-send`). + +### 1. Create the plugin + +Drop an executable script in `entertainment/.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail +err() { echo "ERROR: $*" >&2; exit 1; } + +command -v curl &>/dev/null || err "curl not found" + +data="$(curl -fsS --max-time 20 https://api.example.com/foo)" +printf 'Title: %s\n' "$data" +``` + +**Contract:** plugins are **self-contained** — do **not** source `lib/common.sh`. Its `log`/`warn`/`ok` helpers print to **stdout**, and the runner captures stdout as the message to send (helper chatter would be sent to Telegram). All stdout is the message; errors go to stderr and exit nonzero. Plugins must be non-interactive (no prompts) — the module is designed for cron/systemd timers. + +### 2. Config (if needed) + +Read runtime values from `~/.config/linux_post_install/entertainment.env` (chmod 600, env precedence) — same pattern as `telegram.env`. Example: `weather.sh` uses `WEATHER_LAT`/`WEATHER_LON`. + +### 3. Deps + +`curl` and `jq` are already in `preinstall.sh` PACKAGES. Anything else: guard with `command -v … || err "…"` and, if apt-available, add to PACKAGES. + +### 4. Done + +Plugins are not `pos-*` tools, so `make gen`/`make check` don't scan them — verify with `bash -n entertainment/.sh` and a live `pos entertainment send --print` run. Document the plugin in `DOC/POS.md`'s entertainment plugin table. + +--- + ## Adding an Optional App Start from the template: `cp templates/app.sh apps//.sh`. diff --git a/DOC/POS.md b/DOC/POS.md index 60ad0d7..76ff2da 100644 --- a/DOC/POS.md +++ b/DOC/POS.md @@ -12,6 +12,7 @@ - [ssh](#ssh) - [usb](#usb) - [communication](#communication) + - [entertainment](#entertainment) - [flags](#flags) - [Legacy wrappers](#legacy-wrappers) @@ -203,6 +204,31 @@ 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`. +### entertainment + +**File:** `bin/pos-entertainment-send` +**Purpose:** run a public-API plugin and send its output to Telegram by default. Plugins are standalone scripts in `entertainment/` that fetch a public API and **print the message to stdout** — that stdout is what gets sent. + +| Command | Behavior | +|---------|----------| +| `pos entertainment send` | List available plugins + usage | +| `pos entertainment send [--print] [--markdown] [args…]` | Run the plugin, send its output to Telegram (silent) | +| `pos entertainment send --print` | Print the output locally; do not send | +| `pos entertainment send --markdown` | Send with `--parse-mode markdown` (via `pos communication telegram`) | + +**Plugin lookup order:** `$ENTERTAINMENT_DIR` → repo `entertainment/` → `/usr/local/share/linux_post_install/entertainment/` (installed by `install.sh` Phase 2). A plugin name matches the file name with or without the `.sh` suffix. + +Plugins: + +| Plugin | Source API | Config | +|--------|-----------|--------| +| `weather` | Open-Meteo (no API key) | `~/.config/linux_post_install/entertainment.env`: `WEATHER_LAT`, `WEATHER_LON` (required), `WEATHER_CITY` (optional label) | +| `joke` | icanhazdadjoke.com (no API key) | None | + +**Adding a plugin:** drop an executable script in `entertainment/` (e.g. `myfeed.sh`). It must be non-interactive and print the message to stdout; errors go to stderr (exit nonzero). If it needs coordinates/tokens, read them from `~/.config/linux_post_install/entertainment.env` (chmod 600, env precedence). No registration needed. Dependencies beyond `curl`/`jq` (both in `preinstall.sh` PACKAGES) should be guarded with `command -v … || exit 1`. + +**Automation:** the runner is headless/timer-friendly — no TTY prompts, exit 0 on success / 1 on failure. A systemd timer (e.g. hourly) can call `pos entertainment send weather` directly. Note the config files live under the user's `$HOME`, so the timer must run as that user (a systemd **user** unit, or a system unit with `Environment=HOME=/home/`). + ### flags Feature-flag management CLIs (see [SCRIPTS.md → lib/flags.sh](SCRIPTS.md#libflagssh--feature-flags)): diff --git a/README.md b/README.md index 5bbc4d4..31ad39f 100755 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It is a **personal toolkit** — a bootstrap script, a unified `pos` CLI for eve **What you get:** - 25+ system packages installed automatically -- The `pos` CLI: network, Docker (compose + vbox VMs), media, system, SSH, USB, and communication (Telegram) tools +- The `pos` CLI: network, Docker (compose + vbox VMs), media, system, SSH, USB, communication (Telegram), and entertainment (public-API plugins → Telegram) tools - Wi-Fi hotspot tools (`create_ap`, `wihotspot-gui`) via `pos network hotspot` - 15 optional desktop apps (VS Code, Brave, OBS, Tailscale, …) — pick what you want - 119+ self-hosted services with Tailscale access (Jellyfin, Home Assistant, …) diff --git a/bin/pos b/bin/pos index 2eff6dd..441bf6c 100755 --- a/bin/pos +++ b/bin/pos @@ -81,8 +81,8 @@ usage() { cat < [args] @@ -114,6 +114,9 @@ EXAMPLES pos communication telegram --send "Backup done" Send a Telegram message + pos entertainment send weather Fetch Open-Meteo weather → Telegram + pos entertainment send joke --print Preview a joke locally (no send) + pos docker vbox create lab1 Create disposable Docker VM pos docker vbox create lab1 --dir . Create VM using current directory pos docker vbox enter lab1 Shell into a Docker VM diff --git a/bin/pos-entertainment-send b/bin/pos-entertainment-send new file mode 100755 index 0000000..bccc0be --- /dev/null +++ b/bin/pos-entertainment-send @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail +# POS: entertainment send — Run a public-API plugin and send its output via Telegram (default sender) +# POS_FLAGS: --print --markdown + +source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" + +# ── Plugin directory lookup ───────────────────────────────────── +plugin_dir() { + if [ -n "${ENTERTAINMENT_DIR:-}" ]; then + echo "$ENTERTAINMENT_DIR" + elif [ -d "$(dirname "$0")/../entertainment" ]; then + echo "$(cd "$(dirname "$0")/../entertainment" && pwd)" + elif [ -d "/usr/local/share/linux_post_install/entertainment" ]; then + echo "/usr/local/share/linux_post_install/entertainment" + else + err "entertainment plugin dir not found (set ENTERTAINMENT_DIR)" + fi +} + +list_plugins() { + local dir="$1" f + for f in "$dir"/*; do + [ -f "$f" ] && [ -x "$f" ] && echo "$(basename "$f")" + done +} + +resolve_plugin() { + local dir="$1" name="$2" f + [ -n "$name" ] || err "No plugin given" + for f in "$dir/$name" "$dir/$name.sh"; do + if [ -f "$f" ] && [ -x "$f" ]; then + echo "$f" + return 0 + fi + done + err "Plugin '$name' not found in $dir — try one of: $(list_plugins "$dir" | tr '\n' ' ')" +} + +usage() { + local dir plugins + dir="$(plugin_dir 2>/dev/null)" || dir="" + if [ -n "$dir" ]; then + plugins="$(list_plugins "$dir" | sed 's/^/ /')" + else + plugins=" (plugin directory not found — set ENTERTAINMENT_DIR)" + fi + cat < [--print] [--markdown] [plugin args...] + +Run an entertainment plugin, capture its output, and send it via Telegram. + +Plugins are scripts that fetch a public API and print the message to stdout — +that stdout is what gets sent. + +Modes: + (default) Send the plugin output to Telegram (silent) + --print Print the output locally instead of sending + --markdown Send with Markdown parse_mode (via pos communication telegram) + +Available plugins: +$plugins + +Environment: + ENTERTAINMENT_DIR Override the plugin directory + +Config (per plugin): ~/.config/linux_post_install/entertainment.env + +Examples: + pos entertainment send weather + pos entertainment send weather --print + pos entertainment send joke --markdown +EOF + exit 0 +} + +case "${1:-}" in + -h|--help|"") usage ;; +esac + +# ── Parse runner flags ────────────────────────────────────────── +PRINT=0 +MARKDOWN=0 +plugin_args=() +while [ $# -gt 0 ]; do + case "$1" in + --print) PRINT=1; shift ;; + --markdown) MARKDOWN=1; shift ;; + -h|--help) usage ;; + *) plugin_args+=("$1"); shift ;; + esac +done + +[ ${#plugin_args[@]} -ge 1 ] || usage +plugin="${plugin_args[0]}" +unset 'plugin_args[0]' + +# ── Run the plugin ────────────────────────────────────────────── +dir="$(plugin_dir)" +script="$(resolve_plugin "$dir" "$plugin")" + +if ! output="$("$script" "$@")"; then + err "Plugin '$plugin' failed (exit $?)" +fi + +[ -n "$output" ] || { warn "Plugin '$plugin' produced no output — nothing to send"; exit 0; } + +# ── Route the output ──────────────────────────────────────────── +if [ "$PRINT" -eq 1 ]; then + printf '%s\n' "$output" + exit 0 +fi + +telegram="$(dirname "$0")/pos-communication-telegram" +[ -x "$telegram" ] || telegram="$(command -v pos-communication-telegram 2>/dev/null || true)" +[ -n "$telegram" ] && [ -x "$telegram" ] || err "pos-communication-telegram not found next to this script" + +if [ "$MARKDOWN" -eq 1 ]; then + exec "$telegram" --send "$output" --parse-mode markdown +else + exec "$telegram" --send "$output" +fi diff --git a/completions/pos.bash b/completions/pos.bash index 8b9cfde..81393f3 100644 --- a/completions/pos.bash +++ b/completions/pos.bash @@ -4,6 +4,7 @@ # GEN:START posflags declare -A _pos_flags _pos_flags[communication-telegram]="--send" +_pos_flags[entertainment-send]="--print --markdown" _pos_flags[network-hotspot]="--foreground" _pos_flags[system-backup]="--service" _pos_flags[usb-server]="--ls --ls-shared --share --unshare --auto-share --callback --close-callback --auto-connect --disconnect --nickname --timeout --port --info --version" @@ -114,6 +115,9 @@ _pos() { communication-telegram) _pos_complete_flags communication-telegram ;; + entertainment-send) + _pos_complete_flags entertainment-send + ;; network-hotspot) _pos_complete_flags network-hotspot ;; diff --git a/entertainment/joke.sh b/entertainment/joke.sh new file mode 100755 index 0000000..6b2ad92 --- /dev/null +++ b/entertainment/joke.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Entertainment plugin: random dad joke via icanhazdadjoke.com (no API key). +# Contract: stdout is the message sent by 'pos entertainment send joke'. + +err() { echo "ERROR: $*" >&2; exit 1; } + +case "${1:-}" in + -h|--help) + cat </dev/null || err "curl not found" +command -v jq &>/dev/null || err "jq not found" + +if ! joke="$(curl -fsS --max-time 20 -H 'Accept: application/json' \ + https://icanhazdadjoke.com/ | jq -r '.joke')"; then + err "Failed to fetch a joke from icanhazdadjoke.com" +fi + +[ -n "$joke" ] || err "No joke received" +printf '%s\n' "$joke" diff --git a/entertainment/weather.sh b/entertainment/weather.sh new file mode 100755 index 0000000..54e3a11 --- /dev/null +++ b/entertainment/weather.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Entertainment plugin: current weather via Open-Meteo (no API key). +# Contract: stdout is the message sent by 'pos entertainment send weather'. + +err() { echo "ERROR: $*" >&2; exit 1; } + +CONFIG_FILE="$HOME/.config/linux_post_install/entertainment.env" + +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) +} + +wmo_desc() { + case "$1" in + 0) echo "clear sky" ;; + 1) echo "mainly clear" ;; + 2) echo "partly cloudy" ;; + 3) echo "overcast" ;; + 45|48) echo "fog" ;; + 51|53|55) echo "drizzle" ;; + 61|63|65) echo "rain" ;; + 71|73|75) echo "snow" ;; + 80|81|82) echo "rain showers" ;; + 95|96|99) echo "thunderstorm" ;; + *) echo "weather code $1" ;; + esac +} + +case "${1:-}" in + -h|--help) + cat </dev/null || err "curl not found" +command -v jq &>/dev/null || err "jq not found" + +load_config + +[ -n "${WEATHER_LAT:-}" ] || err "WEATHER_LAT not set — add it to $CONFIG_FILE" +[ -n "${WEATHER_LON:-}" ] || err "WEATHER_LON not set — add it to $CONFIG_FILE" +label="${WEATHER_CITY:-$WEATHER_LAT,$WEATHER_LON}" + +if ! json="$(curl -fsS --max-time 20 \ + "https://api.open-meteo.com/v1/forecast?latitude=${WEATHER_LAT}&longitude=${WEATHER_LON}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m")"; then + err "Failed to fetch weather from Open-Meteo" +fi + +temp="$(jq -r '.current.temperature_2m' <<<"$json")" +feels="$(jq -r '.current.apparent_temperature' <<<"$json")" +humidity="$(jq -r '.current.relative_humidity_2m' <<<"$json")" +code="$(jq -r '.current.weather_code' <<<"$json")" +wind="$(jq -r '.current.wind_speed_10m' <<<"$json")" +unit_temp="$(jq -r '.current_units.temperature_2m' <<<"$json")" +unit_wind="$(jq -r '.current_units.wind_speed_10m' <<<"$json")" + +printf 'Weather in %s: %s, %s%s (feels like %s%s), humidity %s%%, wind %s%s\n' \ + "$label" "$(wmo_desc "$code")" "$temp" "$unit_temp" "$feels" "$unit_temp" "$humidity" "$wind" "$unit_wind" diff --git a/install.sh b/install.sh index 1b671fc..c3623f8 100755 --- a/install.sh +++ b/install.sh @@ -114,6 +114,16 @@ if should_run 2 scripts; then run sudo install -m 644 lib/common.sh /usr/local/bin/common.sh run sudo install -m 644 lib/flags.sh /usr/local/bin/flags.sh + # ── Entertainment plugins ──────────────────────────────── + run sudo mkdir -p /usr/local/share/linux_post_install/entertainment + pcount=0 + for f in entertainment/*.sh; do + [ -f "$f" ] || continue + run sudo install -m 755 "$f" /usr/local/share/linux_post_install/entertainment/ + pcount=$((pcount + 1)) + done + ok "$pcount entertainment plugins -> /usr/local/share/linux_post_install/entertainment" + # ── Precompiled architecture binaries ───────────────────── # Manually-compiled binaries (not available on the internet), # copied straight into /usr/local/bin for the matching arch. diff --git a/scripts/gen-docs.sh b/scripts/gen-docs.sh index 47050ab..e220380 100755 --- a/scripts/gen-docs.sh +++ b/scripts/gen-docs.sh @@ -132,7 +132,7 @@ regen_block() { regen_block "$ctx" tree regen_block "$ctx" dispatch regen_block "$ctx" selfcontained -regen_block "$ctx" filetable regen_block "$comp" posflags +regen_block "$ctx" filetable echo "gen-docs: $mode OK"