app
RSS to Google Chat
Public Made by Adomby adom
Post new RSS feed items straight into your Google Chat spaces. A small always-on poller with a pinnable web editor: click the link in your space, add or edit feeds, Save — live within one poll cycle. Per-feed dedup handles both news feeds (post once) and status pages (re-post on updates).
← Commit history
README.md+81−40@@ -1,6 +1,6 @@ # RSS to Google Chat -Post new RSS feed items straight into your Google Chat spaces — and manage+Post new RSS feed items straight into your Google Chat spaces, and manage which feeds post from a web editor pinned right next to them. A small always-on service on your own Adom container polls your feeds (default@@ -14,24 +14,37 @@ Tue, 21 Jul 2026 14:00:00 +0000 https://vendor.example.com/blog/product-2-0 ``` -- **Per-feed routing** — each feed carries its own Chat webhook, so one+- **Per-feed routing**: each feed carries its own Chat webhook, so one instance can post different feeds to different spaces.-- **Two dedup modes** — `guid` posts once per item (news, blogs, release+- **Two dedup modes**: `guid` posts once per item (news, blogs, release notes); `guid+date` re-posts when an item updates (status pages: you see Investigating, then Resolved).-- **Pinnable editor** — a secret-free link you pin in the space; sign in+- **Pinnable editor**: a secret-free link you pin in the space; sign in with your Adom account to add/edit/remove feeds, live within one poll cycle.-- **Self-contained** — runs on your container, supervised by a watchdog.+- **Self-contained**: runs on your container, supervised by a watchdog. No central service, and your webhooks never leave your box. ## Setup -You need: an Adom account with `adom-cli` (any dev container has it), and a-Google Chat space you can add webhooks to. About five minutes.+You need an Adom account and a Google Chat space you can add webhooks to.+About five minutes. -### 1. Create a small service container+**Where should it run?** Anywhere that stays up, which on Adom is any+container: they run 24/7 in the cloud, independent of whether your laptop is+on. Most people should just use the container they already have. -From your dev container:+| | Your existing dev container (simplest) | A dedicated service container |+|---|---|---|+| Extra containers | none | one more to manage |+| Isolation | shares the box with your dev work | fully isolated |+| Survives an admin rebuild of your dev box | no, reinstall | yes |++Both are the same install. Pick the dedicated container only if you want the+isolation; otherwise skip step 0.++### 0. (Optional) Create a dedicated service container++Skip this if you are installing into a container you already have. ```bash IMAGE_ID=$(adom-cli carbon containers images | jq -r '.[] | select(.name=="default-light") | .id')@@ -40,37 +53,38 @@ REPO_ID=$(adom-cli carbon user repos | jq -r '.[] | select(.name=="rss-gchat") | adom-cli carbon containers create --image-id "$IMAGE_ID" --class small --repo-id "$REPO_ID" ``` -Note the `slug` and the SSH username in the response, then give it ~20 seconds-to boot.+Note the `slug` and SSH username from the response, give it ~20 seconds to+boot, then prefix the install command below with+`ssh -o StrictHostKeyChecking=accept-new <ssh-username>@adom.cloud '...'`. -### 2. Install (one command, on the service container)+### 1. Install++Run this on whichever container will host the service: ```bash-ssh -o StrictHostKeyChecking=accept-new <ssh-username>@adom.cloud \- 'curl -fsSL -H "Authorization: Bearer $(cat /var/run/adom/api-key)" \- https://wiki.adom.inc/download/adom/adom-rss-gchat/1.1.5/install.sh | bash'+curl -fsSL -H "Authorization: Bearer $(cat /var/run/adom/api-key)" \+ https://wiki.adom.inc/download/adom/adom-rss-gchat/1.1.6/install.sh | bash ``` -No Rust, git, or other tooling needed on the container — the installer-downloads a prebuilt binary, sets up the watchdog, and prints the exact next-steps. The `Authorization` header is what lets adom org members install while-this page is private; it is harmless once the page is public.+No Rust, git, or other tooling needed: the installer downloads a prebuilt+binary, installs a cron watchdog, and prints the exact next steps (including+your editor URL, resolved automatically when it can). The `Authorization`+header is what lets adom org members install while this page is private; it is+harmless once the page is public. -### 3. Expose the feed editor+### 2. Expose the feed editor -Back on your dev container:+The installer prints this command with your container's real slug filled in: ```bash adom-cli carbon containers port-add <slug> --port 8842 --prefix rss-editor ``` -Your editor URL is `https://<returned-hostname>/` — no secret in it. The-first visit asks you to **Sign in with Adom** (approve in the tab that opens,-once per browser); only the container's owner gets in. If the installer chose-token mode instead (no api-key on the container), the URL is-`https://<returned-hostname>/?token=<token-from-step-2>`.+Your editor URL is the returned `https://<hostname>/`, with no secret in it.+The first visit asks you to **Sign in with Adom**; only the container's owner+gets in. -### 4. Add your first feed+### 3. Add your first feed Open the editor URL. On first run it opens straight into the setup guide: @@ -93,7 +107,7 @@ as read silently, so your space is never flooded.  -Everything happens in the editor — it validates input, backs up the previous+Everything happens in the editor, it validates input, backs up the previous list before every save, and the service hot-reloads the file each poll cycle: | You want to | Do |@@ -110,30 +124,28 @@ by URL, so relabeling never re-posts old items. ## Security model -**Default: Sign in with Adom.** The pinned editor URL carries no secret — it's+**Default: Sign in with Adom.** The pinned editor URL carries no secret. It is just the hostname. Opening it prompts a one-time "Sign in with Adom" (approve the request in the tab that opens; the session lasts 30 days per browser).-Access is limited to the **container's owner**, resolved automatically —-nothing to configure. Allow teammates by adding+Access is limited to the **container's owner**, resolved automatically with nothing to configure. Allow teammates by adding `EDITOR_ALLOWED_USERS=name1,name2` to `~/rss-gchat/env`. Anyone else who clicks the pinned link gets a named "not authorized" refusal, so the space-audience and the feed audience can safely differ — including feeds whose URLs+audience and the feed audience can safely differ, including feeds whose URLs embed private credentials. -Defense-in-depth, active in both modes:+Extra safeguards, active in both modes: -- **Webhooks are write-only.** The editor never returns stored webhook URLs —- it shows a masked form with the key/token stripped. (Leave the field blank+- **Webhooks are write-only.** The editor never returns stored webhook URLs, it shows a masked form with the key/token stripped. (Leave the field blank to keep a saved webhook; paste to replace. Changing a feed's URL requires re-entering its webhook.) - **Changes are loud.** Every save posts a short "feed configuration changed"- notice to all affected spaces — including a space a feed was just removed+ notice to all affected spaces, including a space a feed was just removed from. Silently rerouting a feed is not possible. (`EDITOR_CHANGE_NOTICE=0` in `~/rss-gchat/env` disables this.) **Token mode** (`EDITOR_AUTH=token`) remains available for non-Adom deployments or when you *want* a capability link anyone in the space can use:-the URL embeds a long random token — treat it like a password, and rotate a+the URL embeds a long random token, treat it like a password, and rotate a leak with `rm ~/rss-gchat/editor.token` + re-run the installer + re-pin. Fresh installs default to `adom` when the container can identify its owner, `token` otherwise; upgrades never change an existing choice.@@ -145,7 +157,7 @@ your container. | Task | How | |------|-----|-| Upgrade | Re-run the install one-liner — feeds, state, and token are preserved |+| Upgrade | Re-run the install one-liner, feeds, state, and token are preserved | | Logs | `~/rss-gchat/bridge.log` and `~/rss-gchat/editor.log` | | Health | `pidof adom-rss-gchat` (bridge) and `curl localhost:8842/healthz` (editor) | | After a full container restart | `sudo service cron start` once; the watchdog revives both processes within 2 min |@@ -154,13 +166,42 @@ The watchdog (cron, every 2 minutes) relaunches the bridge or editor if either dies. State writes are atomic and recorded per successful post, so even a crash mid-batch never causes a duplicate post. +### Staying alive++Adom containers run 24/7 in the cloud, so closing your laptop or your editor+changes nothing. What does matter is process parentage: anything started from a+VS Code terminal is a child of that terminal and dies when the terminal is+reaped. This service is never started that way. Cron runs the watchdog, the+watchdog launches the daemons with `nohup`, and they reparent to PID 1. Check+it any time:++```bash+ps -o pid,ppid,comm -p $(cat ~/rss-gchat/bridge.pid) # PPID 1 = survives+```++The one gap is a **container restart**: cron is not started by the container+entrypoint, so nothing (this service or any other cron job) comes back until+cron does. Start it once after a restart:++```bash+sudo service cron start+```++To make that automatic, add this to the top of `~/.bashrc` (above its+non-interactive `return` guard) and to `~/.profile`, so any terminal or ssh+login re-arms cron and every cron job with it:++```bash+pgrep -x cron >/dev/null 2>&1 || sudo -n service cron start >/dev/null 2>&1+```+ ## Config reference Settings live in `~/rss-gchat/env` on the service container (all optional): | Variable | Default | Meaning | |----------|---------|---------|-| `GCHAT_WEBHOOK_URL` | unset | Deployment-wide default webhook; per-feed webhooks override it. Usually unset — feeds carry their own. |+| `GCHAT_WEBHOOK_URL` | unset | Deployment-wide default webhook; per-feed webhooks override it. Usually unset, feeds carry their own. | | `POLL_INTERVAL_MINUTES` | `30` | Poll frequency (min 1) | | `RSS_EDITOR_PORT` | `8842` | Editor port | | `EDITOR_AUTH` | auto | `adom` (Sign in with Adom, owner-only) or `token` (capability link) |@@ -184,7 +225,7 @@ Settings live in `~/rss-gchat/env` on the service container (all optional): ## Running it elsewhere -The binary is a plain Rust program with no Adom dependencies — it reads env+The binary is a plain Rust program with no Adom dependencies, it reads env vars and a feeds file, and POSTs to webhooks. `cargo build --release`, then run it under systemd, cron (`RUN_ONCE=1`), or Docker (see `Dockerfile` and `deploy/` in the source repo). `DRY_RUN=1` previews what would post without
SKILL.md+31−21@@ -12,19 +12,23 @@ own small Adom container; no central service, no shared credentials. ## Architecture (what runs where) -- **Service container** (default-light, user-owned): runs the `adom-rss-gchat`+- **Host container** (any container the user owns): runs the `adom-rss-gchat` binary (poll loop, default every 30 min) plus a tiny Python feed editor on- port 8842, both supervised by a 2-minute cron watchdog.+ port 8842, both supervised by a 2-minute cron watchdog. Do NOT create a new+ container by reflex: Adom containers run 24/7, so the user's existing dev+ container is usually the right host. A dedicated `default-light` container is+ only for isolation. - **Data** lives in `~/rss-gchat/` on that container: `feeds.json` (live feed- list, hot-reloaded each cycle), `state.json` (per-feed seen-item sets — never+ list, hot-reloaded each cycle), `state.json` (per-feed seen-item sets, never posts a duplicate), `sessions.json` (editor sign-in sessions), `env` (config, chmod 600). `editor.token` exists only in token-auth mode. - **Feed edits** go through the editor URL; a save is live within one poll- cycle. New feeds seed silently — only items published after adding are posted.+ cycle. New feeds seed silently, only items published after adding are posted. ## First-time setup (walk the user through this) -1. **Create a dedicated service container** (from the user's dev container):+1. **Pick a host.** Default to the container you are already on. Only create a+ dedicated one if the user asks for isolation, in which case: ```bash IMAGE_ID=$(adom-cli carbon containers images | jq -r '.[] | select(.name=="default-light") | .id')@@ -34,15 +38,14 @@ own small Adom container; no central service, no shared credentials. adom-cli carbon containers create --image-id "$IMAGE_ID" --class small --repo-id "$REPO_ID" ``` - Note the `slug` and `ssh_credentials.username` from the response. Wait- ~20s for SSH to come up.+ Note the `slug` and `ssh_credentials.username`, wait ~20s for SSH, and+ prefix the install below with `ssh <ssh-username>@adom.cloud '...'`. -2. **Install on the service container** (one-liner, no Rust/git needed):+2. **Install on the host container** (one-liner, no Rust/git needed): ```bash- ssh -o StrictHostKeyChecking=accept-new <ssh-username>@adom.cloud \- 'curl -fsSL -H "Authorization: Bearer $(cat /var/run/adom/api-key)" \- https://wiki.adom.inc/download/adom/adom-rss-gchat/1.1.5/install.sh | bash'+ curl -fsSL -H "Authorization: Bearer $(cat /var/run/adom/api-key)" \+ https://wiki.adom.inc/download/adom/adom-rss-gchat/1.1.6/install.sh | bash ``` The installer prints the exact next steps. The `Authorization` header is@@ -50,13 +53,15 @@ own small Adom container; no central service, no shared credentials. also authenticates its own internal downloads the same way); it is harmless once the page is public. -3. **Expose the editor** (back on the dev container):+3. **Expose the editor.** The installer prints this with the real slug filled+ in (it self-resolves via `adom-cli carbon containers current`), and prints+ the finished URL outright if a mapping already exists: ```bash adom-cli carbon containers port-add <slug> --port 8842 --prefix rss-editor ``` - Editor URL = `https://<returned-hostname>/` — no secret in it. Default auth+ Editor URL = `https://<returned-hostname>/`, no secret in it. Default auth is "Sign in with Adom", limited to the container owner (auto-resolved); add teammates via `EDITOR_ALLOWED_USERS=name1,name2` in `~/rss-gchat/env`. On a container with no api-key the installer picks token mode instead, and the@@ -68,23 +73,23 @@ own small Adom container; no central service, no shared credentials. on-page setup guide covers the Google Chat side: open the space, space name menu, Apps & integrations, Webhooks, Add webhook, copy the URL, paste it into the feed row. Tell the user to **pin the editor URL to the- space** afterward — it is safe to pin as-is.+ space** afterward, it is safe to pin as-is. ## Managing feeds Prefer sending the user to their editor URL (it validates, backs up, and hot-reloads). For direct automation, edit `~/rss-gchat/feeds.json` on the-service container over SSH — same format:+service container over SSH, same format: ```json { "feeds": [ { "url": "https://example.com/feed.xml", "label": "Example", "dedup": "guid", "webhook": "https://chat.googleapis.com/..." } ] } ``` -- `dedup: "guid"` — post once per item (news, blogs).-- `dedup: "guid+date"` — re-post when an item's pubDate changes (status pages:+- `dedup: "guid"`, post once per item (news, blogs).+- `dedup: "guid+date"`, re-post when an item's pubDate changes (status pages: Investigating, Monitoring, Resolved each post).-- `webhook` — per-feed Chat webhook. Feeds route independently, so one service+- `webhook`, per-feed Chat webhook. Feeds route independently, so one service instance can post different feeds to different spaces. - Saving an empty list pauses all posting (nothing is forgotten; state stays). @@ -95,8 +100,13 @@ service container over SSH — same format: - Auth: `EDITOR_AUTH=adom` (default; owner + EDITOR_ALLOWED_USERS sign in with Adom, URL has no secret) or `token`. Token mode leak: `rm ~/rss-gchat/editor.token`, re-run installer, re-pin.-- Cold-boot caveat: if the whole container restarts, start cron once:- `ssh <user>@adom.cloud 'sudo service cron start'` — the watchdog then- revives everything within 2 minutes.+- Staying alive: containers run 24/7; the daemons survive because cron starts+ them and they reparent to PID 1 (`ps -o pid,ppid -p $(cat ~/rss-gchat/bridge.pid)`+ should show PPID 1). Never start them from a VS Code terminal.+- Cold-boot caveat: a container restart does not start cron, so nothing revives+ until it does. Run `sudo service cron start` once, or make it automatic with+ `pgrep -x cron >/dev/null 2>&1 || sudo -n service cron start >/dev/null 2>&1`+ at the top of `~/.bashrc` (above its non-interactive guard) and in+ `~/.profile`. - Never `pkill -f` the binary path over SSH (self-matches the SSH wrapper shell); use `pidof adom-rss-gchat`.
package.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-rss-gchat", "title": "RSS to Google Chat", "type": "app",- "version": "1.1.5",+ "version": "1.1.6", "description": "Post new RSS feed items straight into your Google Chat spaces. A small always-on poller with a pinnable web editor: click the link in your space, add or edit feeds, Save \u2014 live within one poll cycle. Per-feed dedup handles both news feeds (post once) and status pages (re-post on updates).", "tags": [ "rss",
page.json+1−1@@ -2,7 +2,7 @@ "slug": "adom-rss-gchat", "title": "RSS to Google Chat", "type": "app",- "version": "1.1.5",+ "version": "1.1.6", "description": "Post new RSS feed items straight into your Google Chat spaces. A small always-on poller with a pinnable web editor: click the link in your space, add or edit feeds, Save — live within one poll cycle. Per-feed dedup handles both news feeds (post once) and status pages (re-post on updates).", "tags": [ "rss",