|
4 | 4 | <img src="media/logo.png" alt="ADVRider Notifier Logo" width="200"> |
5 | 5 | </p> |
6 | 6 |
|
7 | | -A secure, minimal Go service that notifies subscribers about new ADVRider forum posts via email. Designed for Google Cloud Run. |
8 | | - |
9 | | -## Features |
10 | | - |
11 | | -- **Multi-subscription support** - Users can subscribe to multiple threads with one email |
12 | | -- **Secure token-based authentication** - 64-char random tokens prevent enumeration attacks |
13 | | -- **Efficient thread fetching** - Each thread is fetched only once per check cycle, regardless of subscriber count |
14 | | -- **Smart URL normalization** - Handles page numbers and anchors automatically |
15 | | -- **Rate limiting** - 5 subscriptions per IP per hour |
16 | | -- **Thread verification** - Validates threads exist before creating subscriptions |
17 | | -- **Constant-time token comparison** - Prevents timing attacks |
18 | | -- **Comprehensive security headers** - CSP, X-Frame-Options, X-Content-Type-Options |
19 | | -- **Retry logic** - Exponential backoff with jitter for HTTP and Gmail API calls |
20 | | -- **Structured logging** - JSON logs for Cloud Run with slog |
21 | | -- **Graceful degradation** - Continues monitoring despite individual failures |
22 | | -- **HTML email formatting** - Clean, responsive email templates |
23 | | - |
24 | | -## Prerequisites |
25 | | - |
26 | | -- Go 1.23 or later |
27 | | -- Google Cloud Project with: |
28 | | - - Cloud Run API enabled |
29 | | - - Cloud Storage API enabled |
30 | | - - Gmail API enabled |
31 | | -- Service account with: |
32 | | - - Gmail API access (https://www.googleapis.com/auth/gmail.send) |
33 | | - - Cloud Storage access (Storage Object Admin role) |
34 | | -- [ko](https://ko.build/) for deployment |
| 7 | +Email notifications for ADVRider threads. Built for Cloud Run, written in Go. |
35 | 8 |
|
36 | | -## Environment Variables |
| 9 | +ADVRider's built-in notifications only email you once after your last visit to that thread. This keeps the party going by emailing every new post until you unsubscribe. |
| 10 | + |
| 11 | +## What it does |
37 | 12 |
|
38 | | -| Variable | Description | Example | |
39 | | -|----------|-------------|---------| |
40 | | -| `STORAGE_BUCKET` | Cloud Storage bucket name for subscription data | `advrider-subscriptions` | |
41 | | -| `BASE_URL` | Public URL of the deployed service | `https://advrider-notifier-xyz.run.app` | |
42 | | -| `GOOGLE_CREDENTIALS_JSON` | Service account credentials JSON (optional, uses ADC if not set) | `{"type":"service_account",...}` | |
43 | | -| `PORT` | HTTP server port (optional, defaults to 8080) | `8080` | |
44 | | -| `LOCAL_STORAGE` | Local filesystem path for subscription data (optional, defaults to ./data) | `/var/tmp/advrider-notify` | |
| 13 | +Subscribe to ADVRider forum threads and get emails when new posts appear. That's it. |
45 | 14 |
|
46 | | -## Local Development |
| 15 | +- Adaptive polling (5min to 4hr based on thread activity) |
| 16 | +- Multiple threads per email address |
| 17 | +- Handles login-required forums gracefully (reports 403, doesn't crash) |
| 18 | +- Exponential backoff with retry on transient failures |
| 19 | +- Local dev mode with mocked email |
47 | 20 |
|
48 | | -### Build |
| 21 | +## Running it |
49 | 22 |
|
| 23 | +**Local (filesystem + mock email):** |
50 | 24 | ```bash |
51 | | -make build |
| 25 | +go run . |
52 | 26 | ``` |
53 | 27 |
|
54 | | -### Run Tests |
| 28 | +Visit http://localhost:8080 and subscribe to a thread. |
55 | 29 |
|
| 30 | +**Cloud Run (GCS + Brevo):** |
56 | 31 | ```bash |
57 | | -make test |
| 32 | +export STORAGE_BUCKET=your-bucket-name |
| 33 | +export BASE_URL=https://your-service.run.app |
| 34 | +export BREVO_API_KEY=your-api-key |
| 35 | +ko apply -f service.yaml |
58 | 36 | ``` |
59 | 37 |
|
60 | | -### Run Locally |
| 38 | +Requires a service account with Cloud Storage access and a Brevo API key. |
61 | 39 |
|
62 | | -The service automatically runs in local development mode with mock email when no `STORAGE_BUCKET` is set: |
63 | | - |
64 | | -```bash |
65 | | -# Simplest - just run it (uses ./data for storage, mocks email) |
66 | | -go run . |
| 40 | +## Architecture |
67 | 41 |
|
68 | | -# Trigger a poll manually (POST only) |
69 | | -curl -X POST http://localhost:8080/pollz |
70 | 42 | ``` |
| 43 | +/ Homepage (subscribe form) |
| 44 | +/subscribe POST: Create subscription (verifies thread exists first) |
| 45 | +/manage?token=... View/delete subscriptions |
| 46 | +/pollz POST: Trigger immediate poll (no auth, rate limited by IP) |
| 47 | +``` |
| 48 | + |
| 49 | +Storage is either local filesystem (`./data`) or GCS (`STORAGE_BUCKET`). |
| 50 | + |
| 51 | +Email via Brevo API. Falls back to mock in dev. |
| 52 | + |
| 53 | +Each thread is scraped once per poll cycle regardless of subscriber count. Polling interval calculated per-thread using exponential backoff: `5min × 2^(hours_since_post / 3)`, capped at 4 hours. |
| 54 | + |
| 55 | +## Security |
71 | 56 |
|
72 | | -## Deployment |
| 57 | +- Rate limiting: 5 subscriptions/hour per IP |
| 58 | +- Token-based subscription management (64-char random, constant-time comparison) |
| 59 | +- Thread limit: 20 per user (prevents resource exhaustion) |
| 60 | +- Email limit: 10 posts per notification (prevents abuse) |
| 61 | +- CSP, X-Frame-Options, X-Content-Type-Options headers |
| 62 | +- Thread verification before subscription (validates URL is actually an ADVRider thread) |
73 | 63 |
|
74 | | -The service uses [ko](https://ko.build/) for containerless deployment to Cloud Run. |
| 64 | +## Environment Variables |
| 65 | + |
| 66 | +| Variable | Required | Description | |
| 67 | +|----------|----------|-------------| |
| 68 | +| `STORAGE_BUCKET` | Cloud only | GCS bucket for subscription data | |
| 69 | +| `BASE_URL` | Cloud only | Public URL (for manage links in emails) | |
| 70 | +| `BREVO_API_KEY` | Cloud only | Brevo API key for sending emails | |
| 71 | +| `SALT` | Required | Secret salt for token generation (set in GSM or env) | |
| 72 | +| `PORT` | Optional | HTTP port (default: 8080) | |
| 73 | +| `LOCAL_STORAGE` | Optional | Local storage path (default: ./data) | |
| 74 | +| `MAIL_FROM` | Optional | From email address (defaults to postmaster@domain) | |
| 75 | +| `MAIL_NAME` | Optional | From name (default: ADVRider Notifier) | |
75 | 76 |
|
76 | | -### Deploy |
| 77 | +## Development |
77 | 78 |
|
78 | 79 | ```bash |
79 | | -make deploy |
| 80 | +make build # Build binary |
| 81 | +make test # Run tests |
| 82 | +make lint # golangci-lint |
| 83 | +make deploy # Deploy to Cloud Run via ko |
80 | 84 | ``` |
| 85 | + |
| 86 | +Tests use real ADVRider HTML parsing (no mocks for scraper). Exponential backoff algorithm is tested for correctness across all activity levels. |
| 87 | + |
| 88 | +## Email Templates |
| 89 | + |
| 90 | +Dark mode support via `prefers-color-scheme`. WCAG AA compliant. Post numbers are clickable anchors to specific posts on specific pages. |
| 91 | + |
| 92 | +Footer links: "View thread" (goes to last page + last post anchor) and "Manage" (token-authenticated subscription management). |
| 93 | + |
| 94 | +## Why Go? |
| 95 | + |
| 96 | +Fast cold starts on Cloud Run, trivial deploys with ko, stdlib has everything we need. No npm, no containers, no Dockerfile. |
| 97 | + |
| 98 | +## License |
| 99 | + |
| 100 | +Apache 2.0 - see LICENSE file |
| 101 | + |
| 102 | +Built by [codeGROOVE llc](https://codegroove.dev) |
0 commit comments