feat: Matrix/Synapse sender + listener — second notify platform (telegram,matrix)
pos communication matrix sender: send (plain/--markdown/--room), login (password->access token via m.login.password), test. Implements the lib/notify.sh sender contract, so NOTIFY_PLATFORM=telegram,matrix now fans out for real; matrix.env config scope registered for pos config. pos communication matrix listener: systemd user daemon long-polling /sync (since token, compact m.room.message filter); reacts to own user's messages (/ and ! both resolve), threaded m.in_reply_to replies, @quiet marker, ai bridge with per-room session, interactive editor. Added to INTERACTIVE_CMDS. Docs: POS.md, howto/communication.md, HOWTO.md, usage EXAMPLES. Verified against a mock homeserver (send shape, login, owner filter, replies, exit codes, editor). make gen && make check green.
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ summary (newest last).
|
|||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
- **2026-08-09** — New `pos system event-trigger` — state-based threshold rule monitors (eventer). Each line of `~/.config/linux_post_install/event.env` is an independent rule: `["<msg>" if ] <check-command> <op> <threshold>` (op `> < >= <= == !=`, unit suffix ok `60c`/`80%`). The check command is run on every pass and its **first numeric output** compared float-safe; operator detected as the rightmost `op threshold` pair so checks containing their own `>`/`<` (awk, redirection) parse fine. Alerts once on false→true plus one recovery message on true→false (no repeats while a condition holds); per-rule state in `~/.local/share/linux_post_install/eventer/state/` keyed by rule-line hash (editing a rule resets its state). Subcommands: `run` (timer entrypoint), `config` (interactive add/remove/edit with validation by test-running the check), `list` (rules + live values), `enable [interval]` (systemd **user** timer `pos-event-trigger.timer` + oneshot service; `5m…weekly` or `OnCalendar=…`; graceful warnings when no user systemd manager, `loginctl enable-linger` attempt), `disable`, `status`. `--dry-run` honors the DEV.md dry-run convention. Alerts via `lib/notify.sh` (Telegram default; other platforms via `NOTIFY_PLATFORM`). New: `bin/pos-system-event-trigger`, `lib/eventer-lib.sh`, `config/event.env` template (installed no-clobber by postinstall), `lib/eventer-lib.sh` installed by install.sh, `system-event-trigger` added to `INTERACTIVE_CMDS`, usage EXAMPLES row. Docs: POS.md system row, HOWTO.md index row, howto/event-trigger.md; `make gen && make check` green; functional tests covered trigger/recovery/no-repeat, float + unit parsing, editor add/remove/edit + validation + dry-run, timer enable/disable/status (graceful), dispatcher routing.
|
- **2026-08-09** — Matrix/Synapse `communication` tools — `pos communication matrix sender` + `listener`, completing the second notify platform `lib/notify.sh` was designed for (`NOTIFY_PLATFORM=telegram,matrix` fan-out; the sender implements the `send <value> [--markdown]` contract via `notify_sender_name()`'s default key→tool mapping, no lib changes). **Sender** (`bin/pos-communication-matrix-sender`): `send <value> [--markdown] [--room <id|alias>]` PUTs `m.room.message` (`m.text`) to the client-server API v3 — room ids/aliases URL-encoded (`#pos:example.org` → `%23pos%3A…`), unique per-message txn id, `--markdown` sends `org.matrix.custom.html` via a best-effort markdown→HTML converter (bold/italic/code/fences/strike/links/headers/lists, escapes HTML, never fails the send); `login --user <@id>` (masked password prompt → `m.login.password` → saves `access_token`+`user_id`); `test`. Config scope `matrix` (`~/.config/linux_post_install/matrix.env`, `MATRIX_HOMESERVER`/`MATRIX_ACCESS_TOKEN`/`MATRIX_USER_ID`/`MATRIX_ROOM_ID`, secret masked) registered via `# POS_CONFIG:` → `pos config matrix` + tab-completion scope. **Listener** (`bin/pos-communication-matrix-listener`): systemd **user** daemon (`pos-matrix-listener.service`) long-polling `/sync` (30s timeout, per-sync `since` token, compact filter dropping presence/account_data/device noise, `m.room.message` only); reacts to `MATRIX_USER_ID`'s own messages (resolved via `/account/whoami` if unset), `MATRIX_ROOM_ID` restricts to one room; `/` and `!` both resolve; replies threaded `m.in_reply_to`; `@quiet` no-reply marker; `/cmd::desc=…` map descriptions; `ai …` bridge (`pos ai gemini ask`, per-room session `matrix-<room>`, `ai /reset` clears, markdown stripped); interactive editor (`--status`/`--enable`/`--disable`/`--run`), 60s command timeout, exit-code prefix, ~3800-char truncation. `communication-matrix-listener` added to `INTERACTIVE_CMDS` (stdin editor + forever-loop daemon). Docs: POS.md rows + "in detail" sections + ai bridge note, howto/communication.md rewritten Matrix sections, HOWTO.md index + config table + platform note, `bin/pos` usage EXAMPLES; `make gen && make check` green. Verified against a mock homeserver: send plain/markdown/`--room`/test request shape (URL-encoding, Bearer auth, JSON body), login token save, listener owner-filter + `/status` reply + `/help` + `@quiet` silence + non-zero exit reply + interactive editor add. — state-based threshold rule monitors (eventer). Each line of `~/.config/linux_post_install/event.env` is an independent rule: `["<msg>" if ] <check-command> <op> <threshold>` (op `> < >= <= == !=`, unit suffix ok `60c`/`80%`). The check command is run on every pass and its **first numeric output** compared float-safe; operator detected as the rightmost `op threshold` pair so checks containing their own `>`/`<` (awk, redirection) parse fine. Alerts once on false→true plus one recovery message on true→false (no repeats while a condition holds); per-rule state in `~/.local/share/linux_post_install/eventer/state/` keyed by rule-line hash (editing a rule resets its state). Subcommands: `run` (timer entrypoint), `config` (interactive add/remove/edit with validation by test-running the check), `list` (rules + live values), `enable [interval]` (systemd **user** timer `pos-event-trigger.timer` + oneshot service; `5m…weekly` or `OnCalendar=…`; graceful warnings when no user systemd manager, `loginctl enable-linger` attempt), `disable`, `status`. `--dry-run` honors the DEV.md dry-run convention. Alerts via `lib/notify.sh` (Telegram default; other platforms via `NOTIFY_PLATFORM`). New: `bin/pos-system-event-trigger`, `lib/eventer-lib.sh`, `config/event.env` template (installed no-clobber by postinstall), `lib/eventer-lib.sh` installed by install.sh, `system-event-trigger` added to `INTERACTIVE_CMDS`, usage EXAMPLES row. Docs: POS.md system row, HOWTO.md index row, howto/event-trigger.md; `make gen && make check` green; functional tests covered trigger/recovery/no-repeat, float + unit parsing, editor add/remove/edit + validation + dry-run, timer enable/disable/status (graceful), dispatcher routing.
|
||||||
|
|
||||||
- **2026-08-09** — `pos media mp3`/`mp4` hardened + smart format selection. Both tools: yt-dlp calls go through `spawn` (honor `$DRY_RUN`; `--dry-run` prints the exact command and skips dep checks), `-o/--output`, `--no-playlist`, `--cookies` (file existence check), clean ffmpeg/yt-dlp guards, `# POS_FLAGS:` for completion, full metadata (`--embed-metadata --embed-chapters --embed-thumbnail --no-overwrites`, mp3 also `--convert-thumbnails jpg` + `--parse-metadata "%(artist,uploader)s:%(artist)s"` so the uploader fills the artist tag). mp3 gains `--by-artist` (`~/Music/<artist>/<title>.mp3`). mp4 gains `-f <id>` / `--best` / `--worst` (no prompt), conflict validation, and an interactive picker that shows a **curated** `-F` table (`[audio]`/`[video]`/`[combo]` grouping, raw clutter dropped) on stderr — stdout carries only the chosen id (ui_pick lesson) — with id validation against the real table and empty/best default. Docs: howto/media.md rewritten (flags, metadata, by-artist, troubleshooting); `make gen && make check` green.
|
- **2026-08-09** — `pos media mp3`/`mp4` hardened + smart format selection. Both tools: yt-dlp calls go through `spawn` (honor `$DRY_RUN`; `--dry-run` prints the exact command and skips dep checks), `-o/--output`, `--no-playlist`, `--cookies` (file existence check), clean ffmpeg/yt-dlp guards, `# POS_FLAGS:` for completion, full metadata (`--embed-metadata --embed-chapters --embed-thumbnail --no-overwrites`, mp3 also `--convert-thumbnails jpg` + `--parse-metadata "%(artist,uploader)s:%(artist)s"` so the uploader fills the artist tag). mp3 gains `--by-artist` (`~/Music/<artist>/<title>.mp3`). mp4 gains `-f <id>` / `--best` / `--worst` (no prompt), conflict validation, and an interactive picker that shows a **curated** `-F` table (`[audio]`/`[video]`/`[combo]` grouping, raw clutter dropped) on stderr — stdout carries only the chosen id (ui_pick lesson) — with id validation against the real table and empty/best default. Docs: howto/media.md rewritten (flags, metadata, by-artist, troubleshooting); `make gen && make check` green.
|
||||||
|
|
||||||
|
|||||||
@@ -10,19 +10,19 @@
|
|||||||
|
|
||||||
<!-- GEN:START docmap -->
|
<!-- GEN:START docmap -->
|
||||||
| ## 1. Project Overview | 28–43 |
|
| ## 1. Project Overview | 28–43 |
|
||||||
| ## 2. Directory Structure | 44–184 |
|
| ## 2. Directory Structure | 44–186 |
|
||||||
| ## 3. Installation Flow | 185–237 |
|
| ## 3. Installation Flow | 187–239 |
|
||||||
| ## 4. The `pos` CLI System | 238–302 |
|
| ## 4. The `pos` CLI System | 240–306 |
|
||||||
| ## 5. Shared Library — `lib/common.sh` | 303–334 |
|
| ## 5. Shared Library — `lib/common.sh` | 307–338 |
|
||||||
| ## 6. Docker Compose / ScaleTail | 335–377 |
|
| ## 6. Docker Compose / ScaleTail | 339–381 |
|
||||||
| ## 7. Optional Apps (`apps/`) | 378–407 |
|
| ## 7. Optional Apps (`apps/`) | 382–411 |
|
||||||
| ## 8. Entertainment Module | 408–421 |
|
| ## 8. Entertainment Module | 412–425 |
|
||||||
| ## 9. Systemd Services | 422–434 |
|
| ## 9. Systemd Services | 426–438 |
|
||||||
| ## 10. Configuration Files | 435–461 |
|
| ## 10. Configuration Files | 439–465 |
|
||||||
| ## 11. Coding Conventions | 462–494 |
|
| ## 11. Coding Conventions | 466–498 |
|
||||||
| ## 12. Development Workflow | 495–547 |
|
| ## 12. Development Workflow | 499–551 |
|
||||||
| ## 13. Key File Quick Reference | 548–599 |
|
| ## 13. Key File Quick Reference | 552–605 |
|
||||||
| ## 14. Common Tasks for Agents | 600–625 |
|
| ## 14. Common Tasks for Agents | 606–631 |
|
||||||
<!-- GEN:END docmap -->
|
<!-- GEN:END docmap -->
|
||||||
|
|
||||||
## 1. Project Overview
|
## 1. Project Overview
|
||||||
@@ -59,6 +59,8 @@ Linux_post_install/
|
|||||||
│ ├── 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-ai-gemini # Chat with Google Gemini (ask, chat, models, sessions)
|
│ ├── pos-ai-gemini # Chat with Google Gemini (ask, chat, models, sessions)
|
||||||
|
│ ├── pos-communication-matrix-listener # Matrix listener: map /command → bash, run them on room messages
|
||||||
|
│ ├── pos-communication-matrix-sender # Send messages to a Matrix room via the client-server API (send, test, login)
|
||||||
│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash, run them on chat messages
|
│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash, run them on chat messages
|
||||||
│ ├── pos-communication-telegram-sender # Send Telegram messages/files/links/stickers via Bot API (send, test)
|
│ ├── pos-communication-telegram-sender # Send Telegram messages/files/links/stickers via Bot API (send, test)
|
||||||
│ ├── pos-config # Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
|
│ ├── pos-config # Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
|
||||||
@@ -255,6 +257,8 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
|||||||
|----------|---------|--------|-------------|
|
|----------|---------|--------|-------------|
|
||||||
<!-- GEN:START dispatch -->
|
<!-- GEN:START dispatch -->
|
||||||
| ai | gemini | `pos-ai-gemini` | Chat with Google Gemini (ask, chat, models, sessions) |
|
| ai | gemini | `pos-ai-gemini` | Chat with Google Gemini (ask, chat, models, sessions) |
|
||||||
|
| communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages |
|
||||||
|
| communication | matrix-sender | `pos-communication-matrix-sender` | Send messages to a Matrix room via the client-server API (send, test, login) |
|
||||||
| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages |
|
| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages |
|
||||||
| communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) |
|
| communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) |
|
||||||
| | config | `pos-config` | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
| | config | `pos-config` | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||||
@@ -327,7 +331,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-listener`, `pos-communication-telegram-sender`, `pos-network-checkport`, `pos-network-hotspot`, `pos-network-ip`, `pos-network-scan`, `pos-ssh-load-keys`, `pos-system-firewall`.
|
`pos`, `pos-communication-matrix-listener`, `pos-communication-matrix-sender`, `pos-communication-telegram-listener`, `pos-communication-telegram-sender`, `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 -->
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -562,8 +566,10 @@ 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` | 280 | CLI dispatcher with smart arg matching + logging + category help |
|
| `bin/pos` | 286 | CLI dispatcher with smart arg matching + logging + category help |
|
||||||
| `bin/pos-ai-gemini` | 311 | Chat with Google Gemini (ask, chat, models, sessions) |
|
| `bin/pos-ai-gemini` | 311 | Chat with Google Gemini (ask, chat, models, sessions) |
|
||||||
|
| `bin/pos-communication-matrix-listener` | 565 | Matrix listener: map /command → bash, run them on room messages |
|
||||||
|
| `bin/pos-communication-matrix-sender` | 214 | Send messages to a Matrix room via the client-server API (send, test, login) |
|
||||||
| `bin/pos-communication-telegram-listener` | 563 | Telegram bot listener: map /command → bash, run them on chat messages |
|
| `bin/pos-communication-telegram-listener` | 563 | Telegram bot listener: map /command → bash, run them on chat messages |
|
||||||
| `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) |
|
| `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) |
|
||||||
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||||
@@ -591,7 +597,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
|||||||
| `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-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands |
|
| `bin/pos-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||||
| `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` | 286 | Dynamic bash completion |
|
| `completions/pos.bash` | 288 | 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 |
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -17,7 +17,7 @@ authoritative one-line reference (every command + flag), see
|
|||||||
| `pos system event-trigger` | Threshold-rule monitors that alert on crossing | [event-trigger](howto/event-trigger.md) |
|
| `pos system event-trigger` | Threshold-rule monitors that alert on crossing | [event-trigger](howto/event-trigger.md) |
|
||||||
| `pos ssh` | Load keys into the agent | [ssh](howto/ssh.md) |
|
| `pos ssh` | Load keys into the agent | [ssh](howto/ssh.md) |
|
||||||
| `pos usb` | Share USB devices over the network | [usb](howto/usb.md) |
|
| `pos usb` | Share USB devices over the network | [usb](howto/usb.md) |
|
||||||
| `pos communication` | Send Telegram messages/files/alerts | [communication](howto/communication.md) |
|
| `pos communication` | Send Telegram/Matrix messages & alerts, /command listeners | [communication](howto/communication.md) |
|
||||||
| `pos entertainment` | Scheduled auto-messages from public APIs | [entertainment](howto/entertainment.md) |
|
| `pos entertainment` | Scheduled auto-messages from public APIs | [entertainment](howto/entertainment.md) |
|
||||||
|
|
||||||
Every tool is `bin/pos-<category>-<command>`; run `pos <category> --help` to
|
Every tool is `bin/pos-<category>-<command>`; run `pos <category> --help` to
|
||||||
@@ -36,6 +36,7 @@ templates (without overwriting an existing file):
|
|||||||
| File | Used by | Keys |
|
| File | Used by | Keys |
|
||||||
|------|---------|------|
|
|------|---------|------|
|
||||||
| `telegram.env` | `pos communication telegram sender` / `listener`, everything that alerts | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` |
|
| `telegram.env` | `pos communication telegram sender` / `listener`, everything that alerts | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` |
|
||||||
|
| `matrix.env` | `pos communication matrix sender` / `listener` | `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, `MATRIX_USER_ID`, `MATRIX_ROOM_ID` |
|
||||||
| `notify.env` | `lib/notify.sh` (all alerting) | `NOTIFY_PLATFORM` (e.g. `telegram,matrix`) |
|
| `notify.env` | `lib/notify.sh` (all alerting) | `NOTIFY_PLATFORM` (e.g. `telegram,matrix`) |
|
||||||
| `system.env` | `pos system health`, `pos system backup` | `BACKUP_SERVICE_ROOTS`, `HEALTH_BACKUP_MAX_AGE_DAYS` |
|
| `system.env` | `pos system health`, `pos system backup` | `BACKUP_SERVICE_ROOTS`, `HEALTH_BACKUP_MAX_AGE_DAYS` |
|
||||||
| `compose.env` | `pos docker compose` | `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE` |
|
| `compose.env` | `pos docker compose` | `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE` |
|
||||||
@@ -44,6 +45,8 @@ templates (without overwriting an existing file):
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pos config telegram # set TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID
|
pos config telegram # set TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID
|
||||||
|
pos config matrix # set MATRIX_HOMESERVER / MATRIX_ROOM_ID, then:
|
||||||
|
pos communication matrix sender login --user @you:example.org # fetch an access token
|
||||||
pos entertainment config set WEATHER_LAT=36.51 WEATHER_LON=40.75
|
pos entertainment config set WEATHER_LAT=36.51 WEATHER_LON=40.75
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -58,8 +61,10 @@ platform is configured it warns and never breaks the calling tool.
|
|||||||
NOTIFY_PLATFORM=telegram # comma-separated to send to all
|
NOTIFY_PLATFORM=telegram # comma-separated to send to all
|
||||||
```
|
```
|
||||||
|
|
||||||
Adding a platform later (e.g. Matrix/Synapse) = create `bin/pos-communication-<p>`
|
Ship with `telegram` and `matrix` — add both to `NOTIFY_PLATFORM` to fan out
|
||||||
implementing `send <value> [--markdown]` and list it. See
|
alerts (Matrix needs `pos config matrix` + a `login`-fetched token first).
|
||||||
|
Adding another platform = create `bin/pos-communication-<p>` implementing
|
||||||
|
`send <value> [--markdown]` and list it. See
|
||||||
[DOC/DEV.md → Alerting](DEV.md) for the contract.
|
[DOC/DEV.md → Alerting](DEV.md) for the contract.
|
||||||
|
|
||||||
### Scheduling
|
### Scheduling
|
||||||
|
|||||||
+27
-1
@@ -76,7 +76,7 @@ Category-less tools (`config`, `tree`) live outside any category and are documen
|
|||||||
|
|
||||||
Precedence: `--model` flag > `AI_GEMINI_MODEL` env > config file > `gemini-2.5-flash`. `postinstall.sh` copies the repo's `config/ai.env` template to `~/.config/linux_post_install/ai.env` on install (no clobber). Dependencies: `curl` + `jq` (both in `preinstall.sh` PACKAGES).
|
Precedence: `--model` flag > `AI_GEMINI_MODEL` env > config file > `gemini-2.5-flash`. `postinstall.sh` copies the repo's `config/ai.env` template to `~/.config/linux_post_install/ai.env` on install (no clobber). Dependencies: `curl` + `jq` (both in `preinstall.sh` PACKAGES).
|
||||||
|
|
||||||
**Telegram bridge:** the Telegram listener forwards non-command messages starting with `ai ` (case-insensitive) to `pos ai gemini ask` and replies with the model's answer — see [communication → listener](#communication).
|
**Messaging bridges:** the Telegram and Matrix listeners forward non-command messages starting with `ai ` (case-insensitive) to `pos ai gemini ask` and reply with the model's answer — see [communication → listener](#communication). The Telegram bridge uses one session per chat (`telegram-<chat id>`), the Matrix bridge one per room (`matrix-<room>`).
|
||||||
|
|
||||||
### network
|
### network
|
||||||
|
|
||||||
@@ -226,6 +226,8 @@ Subcommands that need input prompt interactively when args are omitted.
|
|||||||
|---------|------|---------|---------------|
|
|---------|------|---------|---------------|
|
||||||
| `pos communication telegram sender send "text"` | `bin/pos-communication-telegram-sender` | 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 sender send "text"` | `bin/pos-communication-telegram-sender` | 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 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 matrix sender send "text"` | `bin/pos-communication-matrix-sender` | Send a text message (plain or `--markdown`) to a Matrix room via the client-server API; also `login` (password → access token) and `test` | Homeserver + room from `~/.config/linux_post_install/matrix.env` (`MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, `MATRIX_USER_ID`, `MATRIX_ROOM_ID`, chmod 600, secrets masked by `pos config matrix`). Precedence: `--room` flag > env > config file |
|
||||||
|
| `pos communication matrix listener` | `bin/pos-communication-matrix-listener` | Matrix listener: map `/command` → bash commands and run them from room messages; interactive editor for the map | Same `matrix.env` (reacts to `MATRIX_USER_ID`'s own messages; watches `MATRIX_ROOM_ID` or all joined rooms). Map lives in `~/.config/linux_post_install/matrix_commands.env` (`/cmd=bash command` lines, chmod 600) |
|
||||||
|
|
||||||
`pos communication telegram sender` in detail:
|
`pos communication telegram sender` in detail:
|
||||||
|
|
||||||
@@ -259,6 +261,30 @@ The map file is re-read for every message — edits apply without a restart. The
|
|||||||
|
|
||||||
Map entries may carry an optional **description** shown in the bot's `/` menu: `/cmd::short description=bash command` (the description falls back to the bash command, truncated to ~40 chars, when omitted). After every add/edit/remove the command list is pushed to the bot via `setMyCommands`, so the menu stays in sync; an empty map clears the menu. Telegram only registers lowercase `[a-z0-9_]` names (1–32 chars) — commands like `/Status` or `/my-cmd` are skipped from the menu with a warning but still resolve when typed.
|
Map entries may carry an optional **description** shown in the bot's `/` menu: `/cmd::short description=bash command` (the description falls back to the bash command, truncated to ~40 chars, when omitted). After every add/edit/remove the command list is pushed to the bot via `setMyCommands`, so the menu stays in sync; an empty map clears the menu. Telegram only registers lowercase `[a-z0-9_]` names (1–32 chars) — commands like `/Status` or `/my-cmd` are skipped from the menu with a warning but still resolve when typed.
|
||||||
|
|
||||||
|
`pos communication matrix sender` in detail:
|
||||||
|
|
||||||
|
| Command | Behavior |
|
||||||
|
|---------|----------|
|
||||||
|
| `pos communication matrix sender send "text"` | PUTs an `m.room.message` (`m.text`) to the homeserver's client-server API v3 (60s timeout); prints `[+] m.text sent to room <room>` or fails with a nonzero exit. Room id/alias is URL-encoded automatically; a unique transaction id (`<timestamp>ns`) is generated per message |
|
||||||
|
| `pos communication matrix sender send "text" --markdown` | Sends with `format: org.matrix.custom.html` — a best-effort markdown → HTML conversion (`**bold**`, `__bold__`, `*em*`, `_em_`, `` `code` ``, ``` ```fences``` ``, `~~strike~~`, `[link](url)`, headers, list items). Deliberately simple; it never fails the send |
|
||||||
|
| `pos communication matrix sender send "text" --room <id\|alias>` | One-shot override of the room for this send only (e.g. `--room '#ops:example.org'`) |
|
||||||
|
| `pos communication matrix sender login --user <@id>` | Prompts (masked) for the account password, POSTs `m.login.password` to `/login`, and saves the returned `access_token` + `user_id` to `matrix.env` |
|
||||||
|
| `pos communication matrix sender test` | Sends a canned test message (`Test message from pos <timestamp>`) using the current config |
|
||||||
|
|
||||||
|
The access token is a secret — it is stored only in `~/.config/linux_post_install/matrix.env` and never in the repo. `pos config matrix` edits `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN` (masked), `MATRIX_USER_ID`, `MATRIX_ROOM_ID`. Requires network access to your homeserver. The sender implements the `lib/notify.sh` sender contract, so `matrix` can be added to `NOTIFY_PLATFORM` for multi-platform alerting.
|
||||||
|
|
||||||
|
`pos communication matrix listener` in detail:
|
||||||
|
|
||||||
|
| Command | Behavior |
|
||||||
|
|---------|----------|
|
||||||
|
| `pos communication matrix 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 matrix listener --status` | Shows service state (running/autostart), config + map file paths, and the mapped commands |
|
||||||
|
| `pos communication matrix listener --enable` | Installs + starts a systemd **user** service (`pos-matrix-listener.service`); the daemon long-polls `/sync` and runs mapped commands |
|
||||||
|
| `pos communication matrix listener --disable` | Stops, disables, and removes the service |
|
||||||
|
| `pos communication matrix listener --run` | Run the polling loop in the foreground (what the service executes) |
|
||||||
|
|
||||||
|
The daemon long-polls `/sync` (30s timeout, per-sync `since` token, compact filter that drops presence/account_data/device noise and only requests `m.room.message` timeline events). It reacts only to messages **from `MATRIX_USER_ID`** (your own account — resolved via `/account/whoami` if unset); a `MATRIX_ROOM_ID` restricts it to one room, otherwise every joined room is watched. `/` and `!` prefixes both resolve (`!status` = `/status`). `/help` lists mapped commands; an unmapped command replies "Unknown command". Non-command text starting with `ai ` (case-insensitive, e.g. `ai what is Nvidia`) is forwarded to Gemini via `pos ai gemini ask` with a per-room session (`matrix-<room>`; `ai /reset` clears it) and the answer is replied verbatim with markdown stripped. Replies are sent as `m.text` threaded with `m.in_reply_to` on your message. Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`; non-zero exit is prefixed with `exit <rc>`), so `sudo` inside them needs a NOPASSWD rule. A map value prefixed with `@quiet ` runs the command but does NOT reply — for commands that already send their own notification (e.g. `/status=@quiet pos system health --send`). Map lines may carry a `/cmd::description=…` description. `--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`
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
# How-To: `pos communication`
|
# How-To: `pos communication`
|
||||||
|
|
||||||
Telegram messaging and alerts. Tools: `telegram-sender`, `telegram-listener`,
|
Messaging and alerts over Telegram and Matrix. Tools: `telegram-sender`,
|
||||||
`matrix`.
|
`telegram-listener`, `matrix-sender`, `matrix-listener`.
|
||||||
|
|
||||||
| Tool | What it does |
|
| Tool | What it does |
|
||||||
|------|--------------|
|
|------|--------------|
|
||||||
| `pos communication telegram sender` | Send messages/files/links/stickers, test, config (token + chat id) |
|
| `pos communication telegram sender` | Send messages/files/links/stickers, test, config (token + chat id) |
|
||||||
| `pos communication telegram listener` | Bot listener: map `/command` → bash and run it from chat (systemd user daemon) |
|
| `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 sender` | Send messages to a Matrix room via the client-server API (send, test, login) |
|
||||||
|
| `pos communication matrix listener` | Matrix listener: map `/command` → bash and run it from room messages (systemd user daemon) |
|
||||||
|
|
||||||
`telegram-sender` is the workhorse: it backs the whole **notify system** —
|
`telegram-sender` is the workhorse: it backs the whole **notify system** —
|
||||||
health digests, backup alerts, firewall changes — and can be used directly.
|
health digests, backup alerts, firewall changes — and can be used directly.
|
||||||
|
`matrix-sender` plugs into the same system as a second platform.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -121,20 +123,107 @@ NOTIFY_PLATFORM=telegram # default; comma-separated to fan out to all
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `notify_send "msg"` → delivers to every platform in `NOTIFY_PLATFORM`
|
- `notify_send "msg"` → delivers to every platform in `NOTIFY_PLATFORM`
|
||||||
(current senders: `telegram`).
|
(current senders: `telegram`, `matrix`).
|
||||||
- **Silent-fail:** no platform configured → one WARN line, exit 0, never
|
- **Silent-fail:** no platform configured → one WARN line, exit 0, never
|
||||||
breaks the calling tool.
|
breaks the calling tool.
|
||||||
- New platform (e.g. Matrix): implement `bin/pos-communication-<p> send
|
- New platform: implement `bin/pos-communication-<p> send
|
||||||
<value> [--markdown]`, then list it in `NOTIFY_PLATFORM`. Details:
|
<value> [--markdown]`, then list it in `NOTIFY_PLATFORM`. Details:
|
||||||
[DOC/DEV.md → Alerting](../DEV.md).
|
[DOC/DEV.md → Alerting](../DEV.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## `pos communication matrix`
|
## `pos communication matrix sender`
|
||||||
|
|
||||||
Sender contract exists (`send <value> [--markdown]`) and the dispatcher routes
|
Send plain-text (or markdown-formatted) messages to a Matrix room via the
|
||||||
to it, but no implementation ships yet. When present, add `matrix` to
|
homeserver's client-server API (v3). No bot is needed — it uses a regular
|
||||||
`NOTIFY_PLATFORM` and configure it via `pos communication matrix config set …`.
|
account's access token.
|
||||||
|
|
||||||
|
### One-time setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pos config matrix
|
||||||
|
# set MATRIX_HOMESERVER (https://matrix.example.org) and MATRIX_ROOM_ID
|
||||||
|
# (room id or alias like #ops:example.org), then grab a token:
|
||||||
|
pos communication matrix sender login --user @you:example.org
|
||||||
|
# password is prompted (masked) — access token + user id are saved
|
||||||
|
pos communication matrix sender test
|
||||||
|
# config lives in ~/.config/linux_post_install/matrix.env (chmod 600)
|
||||||
|
```
|
||||||
|
|
||||||
|
`login` calls the homeserver's password endpoint and stores the resulting
|
||||||
|
access token in `matrix.env` (masked by `pos config matrix`). Tokens from any
|
||||||
|
Matrix client (Element, `synctl`…) work too — set `MATRIX_ACCESS_TOKEN`
|
||||||
|
directly. Requires network access to your homeserver.
|
||||||
|
|
||||||
|
### Send
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pos communication matrix sender send "Backup finished" # plain text
|
||||||
|
pos communication matrix sender send --markdown "**bold** ok" # org.matrix.custom.html
|
||||||
|
pos communication matrix sender send "hi" --room '#ops:example.org' # one-shot override
|
||||||
|
pos communication matrix sender test # canned test message
|
||||||
|
```
|
||||||
|
|
||||||
|
`--markdown` sends with `org.matrix.custom.html` (best-effort markdown →
|
||||||
|
HTML: `**bold**`, `` `code` ``, links, headers, lists). Room ids and aliases
|
||||||
|
are URL-encoded automatically.
|
||||||
|
|
||||||
|
**Troubleshooting:**
|
||||||
|
- "No access token" → run `pos communication matrix sender login` first.
|
||||||
|
- Login fails → is the homeserver URL right (`pos config matrix`) and is the
|
||||||
|
account password correct? Homeservers may require the full
|
||||||
|
`@user:example.org` id.
|
||||||
|
- Nothing arrives → the room id/alias must exist and the account must be a
|
||||||
|
member. Public aliases like `#pos:example.org` work when the account has
|
||||||
|
joined.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `pos communication matrix listener`
|
||||||
|
|
||||||
|
Turns any room you're in into a remote control for your server: message your
|
||||||
|
own account a `/command` and the mapped bash runs.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pos communication matrix listener # edit the /command → bash map
|
||||||
|
pos communication matrix listener --status # service state + mapped commands
|
||||||
|
pos communication matrix listener --enable # install the systemd user daemon
|
||||||
|
pos communication matrix listener --disable # remove it
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Map file:** `~/.config/linux_post_install/matrix_commands.env` (chmod 600),
|
||||||
|
one `/cmd=bash command` per line — re-read on every message, so edits apply
|
||||||
|
instantly. Example:
|
||||||
|
```
|
||||||
|
/status=@quiet pos system health --send
|
||||||
|
/temp=sensors | grep -i 'Tctl\|package id 0'
|
||||||
|
/update=cd /path/to/repo && git pull
|
||||||
|
```
|
||||||
|
- **Self-messaging:** the listener reacts to messages **from your own user id**
|
||||||
|
(`MATRIX_USER_ID`) — in practice that means a second device (or another
|
||||||
|
account) sending the commands. If `MATRIX_ROOM_ID` is set it only watches
|
||||||
|
that room, otherwise every room you've joined. `/` and `!` both work
|
||||||
|
(`!status` = `/status`). `/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 room as a thread reply to your message
|
||||||
|
(truncated ~3800 chars; empty → `OK`). `sudo` inside a command needs a
|
||||||
|
NOPASSWD rule.
|
||||||
|
- **`@quiet` prefix:** a map value starting with `@quiet ` runs the command but
|
||||||
|
does NOT reply — for commands that already send their own notification.
|
||||||
|
- **`ai …` bridge:** non-command messages starting with `ai ` are answered by
|
||||||
|
`pos ai gemini` (per-room memory session; `ai /reset` clears it) — replying
|
||||||
|
with the model's answer, markdown stripped.
|
||||||
|
- **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:**
|
||||||
|
- Doesn't answer → check the service with
|
||||||
|
`systemctl --user status pos-matrix-listener.service`; the daemon logs to
|
||||||
|
`~/.local/share/linux_post_install/logs/pos.log`.
|
||||||
|
- "Unknown command" → send `/help` for the mapped list.
|
||||||
|
- Needs `jq` (in preinstall PACKAGES).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,12 @@ EXAMPLES
|
|||||||
pos communication telegram listener --enable
|
pos communication telegram listener --enable
|
||||||
Install the Telegram bot listener
|
Install the Telegram bot listener
|
||||||
|
|
||||||
|
pos communication matrix sender send "Backup done"
|
||||||
|
Send a Matrix room message
|
||||||
|
pos communication matrix sender login --user @me:example.org
|
||||||
|
Fetch a Matrix access token
|
||||||
|
pos communication matrix listener Edit the /command → bash map
|
||||||
|
|
||||||
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)
|
||||||
pos entertainment enable weather 5m Auto-send weather to Telegram every 5 min
|
pos entertainment enable weather 5m Auto-send weather to Telegram every 5 min
|
||||||
@@ -244,7 +250,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 communication-telegram-listener ai-gemini system-event-trigger config"
|
INTERACTIVE_CMDS="system-firewall media-mp4 system-backup usb-server communication-telegram-listener communication-matrix-listener ai-gemini system-event-trigger config"
|
||||||
|
|
||||||
for ((i=n-1; i>=0; i--)); do
|
for ((i=n-1; i>=0; i--)); do
|
||||||
cmd="pos"
|
cmd="pos"
|
||||||
|
|||||||
Executable
+565
@@ -0,0 +1,565 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# POS: communication matrix-listener — Matrix listener: map /command → bash, run them on room messages
|
||||||
|
# POS_FLAGS: --enable --disable --status --run
|
||||||
|
|
||||||
|
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/matrix.env"
|
||||||
|
MAP_FILE="$CONFIG_DIR/matrix_commands.env"
|
||||||
|
SERVICE="pos-matrix-listener.service"
|
||||||
|
USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||||
|
|
||||||
|
# System prompt for the "ai " bridge: replies are posted straight into the
|
||||||
|
# room, so ask for concise, emoji-friendly Matrix-style answers.
|
||||||
|
AI_SYSTEM="You are a friendly assistant chatting in a Matrix room. Keep replies concise, use emojis and light formatting to make them lively, and never claim to send messages yourself."
|
||||||
|
|
||||||
|
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
log() { echo "[+] $*"; }
|
||||||
|
warn() { echo "[!] $*" >&2; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos communication matrix listener [command]
|
||||||
|
|
||||||
|
Matrix listener: map /command → bash commands and run them from room messages.
|
||||||
|
|
||||||
|
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 (MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN,
|
||||||
|
MATRIX_USER_ID — edit with 'pos config matrix')
|
||||||
|
Map: $MAP_FILE — '/cmd=bash command' per line (optional
|
||||||
|
'/cmd::short description=bash command')
|
||||||
|
|
||||||
|
Prefix a map value with '@quiet ' to run the command without replying —
|
||||||
|
for commands that already send their own notification, so you don't get it
|
||||||
|
twice (e.g. '/backup=@quiet pos system backup --send').
|
||||||
|
|
||||||
|
To reply with a command's stdout (e.g. the health dashboard), just map it
|
||||||
|
without '@quiet' and the listener forwards the output:
|
||||||
|
'/status=pos system health'
|
||||||
|
|
||||||
|
The listener only reacts to messages from your own Matrix user
|
||||||
|
(MATRIX_USER_ID). If MATRIX_ROOM_ID is set it only watches that room,
|
||||||
|
otherwise it watches every room you've joined. 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.
|
||||||
|
|
||||||
|
Commands are matched with a leading '/' or '!' — '/status' and '!status'
|
||||||
|
both resolve. There is no equivalent of Telegram's bot "/" menu on Matrix,
|
||||||
|
so command discovery is via /help.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos communication matrix listener
|
||||||
|
pos communication matrix listener --enable
|
||||||
|
pos communication matrix listener --status
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── matrix.env (same pattern as pos-communication-matrix-sender) ──
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
api_base() {
|
||||||
|
local base="${MATRIX_HOMESERVER:-}"
|
||||||
|
[ -n "$base" ] || err "No homeserver — run 'pos config matrix' (MATRIX_HOMESERVER)"
|
||||||
|
printf '%s' "${base%/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
urlencode() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s//#/%23}"
|
||||||
|
s="${s//\"/%22}"
|
||||||
|
s="${s// /%20}"
|
||||||
|
printf '%s' "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── command map (MAP_FILE) ──────────────────────────────────────
|
||||||
|
# Lines: /cmd=bash command, or /cmd::description=bash command. Keys keep the
|
||||||
|
# leading slash; read via awk so values may contain '='. Entries are emitted
|
||||||
|
# with a \x1f (unit separator) delimiter so bash commands may contain pipes.
|
||||||
|
# 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
|
||||||
|
local left="${line%%=*}" cmd desc
|
||||||
|
if [[ "$left" == *::* ]]; then
|
||||||
|
cmd="${left%%::*}"
|
||||||
|
desc="${left#*::}"
|
||||||
|
else
|
||||||
|
cmd="$left"
|
||||||
|
desc=""
|
||||||
|
fi
|
||||||
|
printf '%s\x1f%s\x1f%s\n' "$cmd" "$desc" "${line#*=}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
map_has() {
|
||||||
|
[ -f "$MAP_FILE" ] || return 1
|
||||||
|
awk -F= -v k="$1" '$1==k || index($1, k "::")==1 {found=1} END{exit !found}' "$MAP_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
map_get() {
|
||||||
|
[ -f "$MAP_FILE" ] || return 0
|
||||||
|
awk -F= -v k="$1" '$1==k || index($1, k "::")==1 {sub(/^[^=]*=/,""); print}' "$MAP_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
map_set() {
|
||||||
|
local cmd="$1" value="$2" desc="${3:-}"
|
||||||
|
local key="$cmd"
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
touch "$MAP_FILE"
|
||||||
|
chmod 600 "$MAP_FILE"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
[ -n "$desc" ] && key="${cmd}::${desc}"
|
||||||
|
awk -v k="$cmd" 'index($0, k "=") != 1 && index($0, k "::") != 1 { print }' "$MAP_FILE" > "$tmp"
|
||||||
|
printf '%s=%s\n' "$key" "$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 && index($0, k "::") != 1 { print }' "$MAP_FILE" > "$tmp"
|
||||||
|
mv "$tmp" "$MAP_FILE"
|
||||||
|
chmod 600 "$MAP_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
MAP_CMDS=(); MAP_VALS=(); MAP_DESCS=(); MAP_N=0
|
||||||
|
load_map() {
|
||||||
|
MAP_CMDS=(); MAP_VALS=(); MAP_DESCS=(); MAP_N=0
|
||||||
|
[ -f "$MAP_FILE" ] || return 0
|
||||||
|
local i=0 cmd desc value
|
||||||
|
while IFS=$'\x1f' read -r cmd desc value; do
|
||||||
|
[ -n "$cmd" ] || continue
|
||||||
|
i=$((i + 1))
|
||||||
|
MAP_CMDS[$i]="$cmd"; MAP_DESCS[$i]="$desc"; 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 "$(strip_quiet "$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 >&2
|
||||||
|
local i
|
||||||
|
for ((i=1; i<=MAP_N; i++)); do
|
||||||
|
printf ' %2d) %-16s -> %s%s\n' "$i" "${MAP_CMDS[$i]}" "${MAP_VALS[$i]}" "${MAP_DESCS[$i]:+ (${MAP_DESCS[$i]})}" >&2
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
QUIET_PREFIX="@quiet"
|
||||||
|
|
||||||
|
strip_quiet() {
|
||||||
|
local value="$1"
|
||||||
|
if [ "${value#"$QUIET_PREFIX "}" != "$value" ]; then
|
||||||
|
printf '%s' "${value#"$QUIET_PREFIX "}"
|
||||||
|
else
|
||||||
|
printf '%s' "$value"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_run_command() {
|
||||||
|
local value="$1" output rc
|
||||||
|
value="$(strip_quiet "$value")"
|
||||||
|
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 desc 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; }
|
||||||
|
read -rp "description (optional, shown by /help): " desc
|
||||||
|
if out="$(check_syntax "$value")"; then
|
||||||
|
map_set "$cmd" "$value" "$desc"
|
||||||
|
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 desc 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; }
|
||||||
|
read -rp "description (current: ${MAP_DESCS[$idx]:-none}): " desc
|
||||||
|
if out="$(check_syntax "$value")"; then
|
||||||
|
map_set "$cmd" "$value" "$desc"
|
||||||
|
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 "Matrix 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%s\n' "$i" "${MAP_CMDS[$i]}" "${MAP_VALS[$i]}" "${MAP_DESCS[$i]:+ (${MAP_DESCS[$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-matrix-listener ]; then
|
||||||
|
runner=/usr/local/bin/pos-communication-matrix-listener
|
||||||
|
else
|
||||||
|
runner="$(cd "$(dirname "$0")/.." && pwd)/bin/pos-communication-matrix-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 Matrix listener (/command to bash)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=$runner --run
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
# Do NOT pin Environment=HOME here — the systemd user manager already sets the
|
||||||
|
# correct HOME for the user; pinning a stale enable-time value made the Telegram
|
||||||
|
# daemon read ~/.config/... from the wrong 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%s\n' "${MAP_CMDS[$i]}" "${MAP_VALS[$i]}" "${MAP_DESCS[$i]:+ (${MAP_DESCS[$i]})}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── polling daemon ──────────────────────────────────────────────
|
||||||
|
# Build a compact sync filter: only timeline m.room.message events, drop
|
||||||
|
# presence/account_data/device noise. Reduces bandwidth on busy rooms.
|
||||||
|
SYNC_FILTER='{"presence":{"not_types":["*"]},"account_data":{"not_types":["*"]},"device":{"not_types":["*"]},"room":{"timeline":{"limit":20,"types":["m.room.message"]}}}'
|
||||||
|
|
||||||
|
reply_room() {
|
||||||
|
local room="$1" text="$2" event_id="${3:-}" rc="${4:-}"
|
||||||
|
text="${text:0:3800}"
|
||||||
|
[ -n "${MATRIX_ACCESS_TOKEN:-}" ] || { warn "reply failed — no access token"; return; }
|
||||||
|
local base txn body
|
||||||
|
base="$(api_base)"
|
||||||
|
txn="$(date +%s%N)"
|
||||||
|
room="$(urlencode "$room")"
|
||||||
|
if [ -n "$event_id" ]; then
|
||||||
|
body="$(jq -n --arg body "$text" --arg eid "$event_id" \
|
||||||
|
'{msgtype:"m.text", body:$body, "m.relates_to":{"m.in_reply_to":{"event_id":$eid}}}')"
|
||||||
|
else
|
||||||
|
body="$(jq -n --arg body "$text" '{msgtype:"m.text", body:$body}')"
|
||||||
|
fi
|
||||||
|
curl -fsS -m 60 -X PUT \
|
||||||
|
-H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data "$body" \
|
||||||
|
"${base}/_matrix/client/v3/rooms/${room}/send/m.room.message/${txn}" >/dev/null 2>&1 \
|
||||||
|
|| warn "reply failed (rc ${rc:-?})"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Strip common markdown so AI output reads cleanly in a plain-text
|
||||||
|
# Matrix message (no format is sent).
|
||||||
|
strip_markdown() {
|
||||||
|
local t="$1"
|
||||||
|
t="$(printf '%s' "$t" | sed -E \
|
||||||
|
-e 's/!\[[^]]*\]\([^)]*\)//g' \
|
||||||
|
-e 's/\[([^]]*)\]\([^)]*\)/\1/g' \
|
||||||
|
-e 's/\*\*([^*]*)\*\*/\1/g' \
|
||||||
|
-e 's/\*([^*]*)\*/\1/g' \
|
||||||
|
-e 's/__([^_]*)__/\1/g' \
|
||||||
|
-e 's/`([^`]*)`/\1/g' \
|
||||||
|
-e 's/^[[:space:]]*#{1,6}[[:space:]]+//' \
|
||||||
|
-e 's/^[[:space:]]*>[[:space:]]?//' \
|
||||||
|
-e 's/^[[:space:]]*([-*+]|[0-9]+\.)[[:space:]]+/• /')"
|
||||||
|
t="$(printf '%s' "$t" | sed -E '/^[[:space:]]*([-*_][[:space:]]*){3,}[[:space:]]*$/d')"
|
||||||
|
printf '%s' "$t"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Match map commands whether typed with '/' or '!' — keys in the map keep
|
||||||
|
# the leading '/'. Returns the map value for /cmd or !cmd.
|
||||||
|
map_lookup() {
|
||||||
|
local text="$1" key
|
||||||
|
if [[ "$text" == !* ]]; then
|
||||||
|
key="/${text#!}"
|
||||||
|
else
|
||||||
|
key="$text"
|
||||||
|
fi
|
||||||
|
map_get "$key"
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_message() {
|
||||||
|
local room="$1" text="$2" event_id="$3" sender="$4"
|
||||||
|
local value output rc quiet=0 key
|
||||||
|
case "$text" in
|
||||||
|
/help|!help|/start|!start)
|
||||||
|
reply_room "$room" "Mapped commands: $(map_cmds_list)" "$event_id"
|
||||||
|
return ;;
|
||||||
|
esac
|
||||||
|
# AI bridge: non-command text starting with "ai " (case-insensitive) is
|
||||||
|
# forwarded to Gemini; the model's answer is replied verbatim. Each room
|
||||||
|
# gets its own persistent memory session ("matrix-<room>"); the exact
|
||||||
|
# prompt "ai /reset" clears it.
|
||||||
|
if [[ "$text" != /* && "$text" != !* && "$text" =~ ^[Aa][Ii][[:space:]](.*)$ ]]; then
|
||||||
|
local prompt="${BASH_REMATCH[1]}" answer session
|
||||||
|
[ -n "$prompt" ] || { reply_room "$room" "Usage: ai <prompt> — e.g. 'ai what is Nvidia'" "$event_id"; return; }
|
||||||
|
session="matrix-${room}"
|
||||||
|
if [[ "$prompt" =~ ^/?reset[[:space:]]*$ ]]; then
|
||||||
|
if pos ai gemini sessions reset "$session" >/dev/null 2>&1; then
|
||||||
|
reply_room "$room" "Memory cleared." "$event_id"
|
||||||
|
else
|
||||||
|
reply_room "$room" "AI error: could not clear memory" "$event_id"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "ai: $prompt"
|
||||||
|
if answer="$(timeout 120 pos ai gemini ask --session "$session" --system "$AI_SYSTEM" "$prompt" 2>&1)"; then
|
||||||
|
reply_room "$room" "$(strip_markdown "$answer")" "$event_id"
|
||||||
|
else
|
||||||
|
[ -n "$answer" ] || answer="timed out after 120s"
|
||||||
|
reply_room "$room" "AI error: $answer" "$event_id"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$text" != /* && "$text" != !* ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
value="$(map_lookup "$text")"
|
||||||
|
if [ -z "$value" ]; then
|
||||||
|
reply_room "$room" "Unknown command: $text (send /help)" "$event_id"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "${value#"$QUIET_PREFIX "}" != "$value" ]; then
|
||||||
|
quiet=1
|
||||||
|
value="${value#"$QUIET_PREFIX "}"
|
||||||
|
fi
|
||||||
|
log "exec: $text"
|
||||||
|
if output="$(timeout 60 bash -c "$value" 2>&1)"; then
|
||||||
|
rc=0
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
fi
|
||||||
|
[ "$quiet" -eq 1 ] && return
|
||||||
|
if [ -z "$output" ]; then
|
||||||
|
output="OK"
|
||||||
|
fi
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
reply_room "$room" "$(printf 'exit %s\n%s' "$rc" "$output")" "$event_id" "$rc"
|
||||||
|
else
|
||||||
|
reply_room "$room" "$output" "$event_id"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_daemon() {
|
||||||
|
command -v jq &>/dev/null || err "jq not found (install jq — in preinstall PACKAGES)"
|
||||||
|
load_config
|
||||||
|
[ -n "${MATRIX_HOMESERVER:-}" ] || err "No homeserver — run 'pos config matrix'"
|
||||||
|
[ -n "${MATRIX_ACCESS_TOKEN:-}" ] || err "No access token — run 'pos communication matrix sender login'"
|
||||||
|
local owner="${MATRIX_USER_ID:-}"
|
||||||
|
local room_only="${MATRIX_ROOM_ID:-}"
|
||||||
|
if [ -z "$owner" ]; then
|
||||||
|
local who
|
||||||
|
who="$(curl -fsS -m 30 -H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" \
|
||||||
|
"$(api_base)/_matrix/client/v3/account/whoami" 2>/dev/null)" || { warn "cannot resolve own user id — set MATRIX_USER_ID"; owner=""; }
|
||||||
|
owner="$(printf '%s' "$who" | jq -r '.user_id // empty')"
|
||||||
|
[ -n "$owner" ] || err "cannot resolve own user id — run 'pos config matrix' and set MATRIX_USER_ID"
|
||||||
|
log "owner resolved: $owner"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local since="" filter_enc resp nb
|
||||||
|
filter_enc="$(printf '%s' "$SYNC_FILTER" | jq -sRr @uri)"
|
||||||
|
log "listener running (owner ${owner}${room_only:+ — room ${room_only}}) — Ctrl+C to stop"
|
||||||
|
while true; do
|
||||||
|
local args=("$(api_base)/_matrix/client/v3/sync?timeout=30000&filter=${filter_enc}")
|
||||||
|
[ -n "$since" ] && args[0]+="&since=${since}"
|
||||||
|
resp="$(curl -fsS -m 45 -H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" "${args[0]}" 2>/dev/null)" || { sleep 5; continue; }
|
||||||
|
nb="$(printf '%s' "$resp" | jq -r '.next_batch // empty')"
|
||||||
|
[ -n "$nb" ] || { sleep 5; continue; }
|
||||||
|
since="$nb"
|
||||||
|
|
||||||
|
local room ecount i roomid ev sender text eid
|
||||||
|
for room in $(printf '%s' "$resp" | jq -r '.rooms.join // {} | to_entries[] | .key' 2>/dev/null); do
|
||||||
|
if [ -n "$room_only" ] && [ "$room" != "$room_only" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
ecount="$(printf '%s' "$resp" | jq -r --arg r "$room" ".rooms.join[\"$room\"].timeline.events // [] | length")"
|
||||||
|
for ((i=0; i<ecount; i++)); do
|
||||||
|
ev="$(printf '%s' "$resp" | jq -c --arg r "$room" --argjson i "$i" ".rooms.join[\"$room\"].timeline.events[\$i]")"
|
||||||
|
[ -n "$ev" ] || continue
|
||||||
|
[ "$(printf '%s' "$ev" | jq -r '.type // empty')" = "m.room.message" ] || continue
|
||||||
|
[ "$(printf '%s' "$ev" | jq -r '.content.msgtype // empty')" = "m.text" ] || continue
|
||||||
|
sender="$(printf '%s' "$ev" | jq -r '.sender // empty')"
|
||||||
|
[ -n "$sender" ] || continue
|
||||||
|
[ "$sender" = "$owner" ] || continue
|
||||||
|
text="$(printf '%s' "$ev" | jq -r '.content.body // empty')"
|
||||||
|
[ -n "$text" ] || continue
|
||||||
|
eid="$(printf '%s' "$ev" | jq -r '.event_id // empty')"
|
||||||
|
handle_message "$room" "$text" "$eid" "$sender"
|
||||||
|
done
|
||||||
|
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
|
||||||
Executable
+214
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# POS: communication matrix-sender — Send messages to a Matrix room via the client-server API (send, test, login)
|
||||||
|
# POS_SUBCMDS: send test login
|
||||||
|
# POS_CONFIG: matrix | matrix.env | MATRIX_HOMESERVER=:Homeserver URL (https://matrix.example.org)::https://matrix.example.org | MATRIX_ACCESS_TOKEN=secret:Access token (from 'pos communication matrix sender login' or a Matrix client) | MATRIX_USER_ID=:Your Matrix user id (set by login)::@you:example.org | MATRIX_ROOM_ID=:Room id or alias::#pos:example.org
|
||||||
|
|
||||||
|
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/matrix.env"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos communication matrix sender [command] [args]
|
||||||
|
|
||||||
|
Send messages to a Matrix room via the client-server API (v3).
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
send <value> [options] Send a text message to the configured room
|
||||||
|
test Send a test message using the current config
|
||||||
|
login --user <@id> Get an access token (password prompt) and save it
|
||||||
|
|
||||||
|
Options (send):
|
||||||
|
--markdown Send with org.matrix.custom.html formatting
|
||||||
|
(best-effort markdown → HTML conversion)
|
||||||
|
--room <id|alias> Override the room for one send
|
||||||
|
|
||||||
|
Config: $CONFIG_FILE (MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN,
|
||||||
|
MATRIX_USER_ID, MATRIX_ROOM_ID) — edit it with 'pos config matrix'
|
||||||
|
|
||||||
|
Precedence: CLI flags > environment > config file.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos communication matrix sender send "Backup finished"
|
||||||
|
pos communication matrix sender send "**disk full**" --markdown
|
||||||
|
pos communication matrix sender send "hello" --room '#ops:example.org'
|
||||||
|
pos communication matrix sender login --user @you:example.org
|
||||||
|
pos communication matrix sender test
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
log() { echo "[+] $*"; }
|
||||||
|
|
||||||
|
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#\'}"
|
||||||
|
v="${v//$'\r'/}"
|
||||||
|
if [ -z "${!k:-}" ]; then
|
||||||
|
export "$k"="$v"
|
||||||
|
fi
|
||||||
|
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||||
|
}
|
||||||
|
|
||||||
|
save_config() {
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
local key="$1" val="$2" tmp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
grep -v "^${key}=" "$CONFIG_FILE" 2>/dev/null >"$tmp" || true
|
||||||
|
printf '%s="%s"\n' "$key" "$val" >>"$tmp"
|
||||||
|
mv "$tmp" "$CONFIG_FILE"
|
||||||
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
urlencode() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s//#/%23}"
|
||||||
|
s="${s//\"/%22}"
|
||||||
|
s="${s// /%20}"
|
||||||
|
printf '%s' "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Best-effort markdown → HTML for org.matrix.custom.html. Handles code
|
||||||
|
# fences, inline code, bold/italic/strike, links, headers, and lists. The
|
||||||
|
# conversion is deliberately simple — it must never fail the send.
|
||||||
|
md_to_html() {
|
||||||
|
local t="$1"
|
||||||
|
t="$(printf '%s' "$t" | sed -E \
|
||||||
|
-e 's/&/\&/g' \
|
||||||
|
-e 's/</\</g' \
|
||||||
|
-e 's/>/\>/g' \
|
||||||
|
-e 's/```([^`]*)```/\n<pre><code>\1<\/code><\/pre>\n/g' \
|
||||||
|
-e 's/`([^`]*)`/<code>\1<\/code>/g' \
|
||||||
|
-e 's/\*\*([^*]*)\*\*/<strong>\1<\/strong>/g' \
|
||||||
|
-e 's/__([^_]*)__/<strong>\1<\/strong>/g' \
|
||||||
|
-e 's/\*([^*]*)\*/<em>\1<\/em>/g' \
|
||||||
|
-e 's/_([^_]*)_/<em>\1<\/em>/g' \
|
||||||
|
-e 's/~~([^~]*)~~/<del>\1<\/del>/g' \
|
||||||
|
-e 's/\[([^]]*)\]\(([^)]*)\)/<a href="\2">\1<\/a>/g' \
|
||||||
|
-e 's/^[[:space:]]*#{1,6}[[:space:]]+/<h3>/' \
|
||||||
|
-e 's/^[[:space:]]*([-*+]|[0-9]+\.)[[:space:]]+/<li>/' \
|
||||||
|
-e 's/$/<br\/>/')"
|
||||||
|
printf '%s' "$t"
|
||||||
|
}
|
||||||
|
|
||||||
|
api_base() {
|
||||||
|
local base="${MATRIX_HOMESERVER:-}"
|
||||||
|
[ -n "$base" ] || err "No homeserver — run 'pos config matrix' (MATRIX_HOMESERVER)"
|
||||||
|
printf '%s' "${base%/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
send_room_message() {
|
||||||
|
local room="$1" text="$2" markdown="$3"
|
||||||
|
[ -n "${MATRIX_ACCESS_TOKEN:-}" ] || err "No access token — run 'pos communication matrix sender login'"
|
||||||
|
[ -n "$room" ] || err "No room — run 'pos config matrix' (MATRIX_ROOM_ID) or pass --room"
|
||||||
|
local base txn body fmt
|
||||||
|
base="$(api_base)"
|
||||||
|
txn="$(date +%s%N)"
|
||||||
|
room="$(urlencode "$room")"
|
||||||
|
if [ "$markdown" -eq 1 ]; then
|
||||||
|
fmt="$(md_to_html "$text")"
|
||||||
|
body="$(jq -n --arg body "$text" --arg html "$fmt" \
|
||||||
|
'{msgtype:"m.text", body:$body, format:"org.matrix.custom.html", formatted_body:$html}')"
|
||||||
|
else
|
||||||
|
body="$(jq -n --arg body "$text" '{msgtype:"m.text", body:$body}')"
|
||||||
|
fi
|
||||||
|
curl -fsS -m 60 -X PUT \
|
||||||
|
-H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data "$body" \
|
||||||
|
"${base}/_matrix/client/v3/rooms/${room}/send/m.room.message/${txn}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_send() {
|
||||||
|
local value="" markdown=0 room="${MATRIX_ROOM_ID:-}"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--markdown) markdown=1; shift ;;
|
||||||
|
--room)
|
||||||
|
[ $# -ge 2 ] || err "--room needs a value"
|
||||||
|
room="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
--) shift; [ $# -ge 1 ] || err "No value given for send"; value="$1"; shift ;;
|
||||||
|
-*) err "Unknown option '$1'" ;;
|
||||||
|
*) [ -z "$value" ] || err "Unexpected extra argument '$1'"; value="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$value" ] || err "No value given for send"
|
||||||
|
load_config
|
||||||
|
[ -n "${room:-}" ] || room="${MATRIX_ROOM_ID:-}"
|
||||||
|
send_room_message "$room" "$value" "$markdown"
|
||||||
|
log "m.text sent to room ${room}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_login() {
|
||||||
|
local user="" password
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--user)
|
||||||
|
[ $# -ge 2 ] || err "--user needs a value"
|
||||||
|
user="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
-*) err "Unknown option '$1'" ;;
|
||||||
|
*) err "Unexpected argument '$1' (use --user @you:server)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$user" ] || err "--user is required (e.g. --user @you:example.org)"
|
||||||
|
load_config
|
||||||
|
local base
|
||||||
|
base="$(api_base)"
|
||||||
|
read -rsp "Password for ${user}: " password </dev/tty || true
|
||||||
|
echo >&2
|
||||||
|
[ -n "$password" ] || err "empty password"
|
||||||
|
local resp token uid
|
||||||
|
resp="$(curl -fsS -m 60 -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data "$(jq -n --arg user "$user" --arg pw "$password" \
|
||||||
|
'{type:"m.login.password", identifier:{type:"m.id.user", user:$user}, password:$pw, initial_device_display_name:"pos"}')" \
|
||||||
|
"${base}/_matrix/client/v3/login")" || err "login failed (wrong credentials? homeserver reachable?)"
|
||||||
|
token="$(printf '%s' "$resp" | jq -r '.access_token // empty')"
|
||||||
|
uid="$(printf '%s' "$resp" | jq -r '.user_id // empty')"
|
||||||
|
[ -n "$token" ] || err "login succeeded but no access_token in response"
|
||||||
|
[ -n "$uid" ] || uid="$user"
|
||||||
|
save_config MATRIX_ACCESS_TOKEN "$token"
|
||||||
|
save_config MATRIX_USER_ID "$uid"
|
||||||
|
log "logged in as $uid — token saved to $CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_test() {
|
||||||
|
load_config
|
||||||
|
send_room_message "${MATRIX_ROOM_ID:-}" "Test message from pos $(date '+%Y-%m-%d %H:%M:%S')" 0
|
||||||
|
log "test message sent to room ${MATRIX_ROOM_ID:-}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="${1:-}"
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
send)
|
||||||
|
shift
|
||||||
|
cmd_send "$@"
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
shift
|
||||||
|
cmd_test "$@"
|
||||||
|
;;
|
||||||
|
login)
|
||||||
|
shift
|
||||||
|
cmd_login "$@"
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown matrix command '$cmd'"
|
||||||
|
echo "Run 'pos communication matrix-sender --help' for usage."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
# GEN:START posflags
|
# GEN:START posflags
|
||||||
declare -A _pos_flags
|
declare -A _pos_flags
|
||||||
_pos_flags[ai-gemini]="--model --session --system"
|
_pos_flags[ai-gemini]="--model --session --system"
|
||||||
|
_pos_flags[communication-matrix-listener]="--enable --disable --status --run"
|
||||||
_pos_flags[communication-telegram-listener]="--enable --disable --status --sync-commands --run"
|
_pos_flags[communication-telegram-listener]="--enable --disable --status --sync-commands --run"
|
||||||
_pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-preview --token --chat-id --markdown"
|
_pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-preview --token --chat-id --markdown"
|
||||||
_pos_flags[entertainment-send]="--print --markdown"
|
_pos_flags[entertainment-send]="--print --markdown"
|
||||||
@@ -18,13 +19,14 @@ _pos_flags[usb-server]="--ls --ls-shared --share --unshare --auto-share --callba
|
|||||||
# GEN:START possubcmds
|
# GEN:START possubcmds
|
||||||
declare -A _pos_subcmds
|
declare -A _pos_subcmds
|
||||||
_pos_subcmds[ai-gemini]="ask chat models sessions"
|
_pos_subcmds[ai-gemini]="ask chat models sessions"
|
||||||
|
_pos_subcmds[communication-matrix-sender]="send test login"
|
||||||
_pos_subcmds[communication-telegram-sender]="send test"
|
_pos_subcmds[communication-telegram-sender]="send test"
|
||||||
_pos_subcmds[docker-compose]="ls installed up down restart logs update config"
|
_pos_subcmds[docker-compose]="ls installed up down restart logs update config"
|
||||||
_pos_subcmds[docker-vbox]="create enter stop start rm ls"
|
_pos_subcmds[docker-vbox]="create enter stop start rm ls"
|
||||||
_pos_subcmds[system-event-trigger]="run config list enable disable status"
|
_pos_subcmds[system-event-trigger]="run config list enable disable status"
|
||||||
# GEN:END possubcmds
|
# GEN:END possubcmds
|
||||||
# GEN:START posconfigscopes
|
# GEN:START posconfigscopes
|
||||||
declare -a _pos_config_scopes=(ai compose entertainment notify system telegram)
|
declare -a _pos_config_scopes=(ai compose entertainment matrix notify system telegram)
|
||||||
# GEN:END posconfigscopes
|
# GEN:END posconfigscopes
|
||||||
|
|
||||||
_pos() {
|
_pos() {
|
||||||
|
|||||||
Reference in New Issue
Block a user