A daily Hindi quote bot for WhatsApp groups. Posts one uplifting message per day at a scheduled time, sourced from Hindi Wikiquote with a local fallback.
Built with Baileys (WhatsApp Web protocol). Intended for small, consenting groups — one message per day, no bulk messaging.
- Architecture — system design, module map, runtime flows, and agent onboarding
- Health webhook setup — Apple Shortcuts → daily health report on WhatsApp
- Daily scheduling — in-process cron (
node-cron), default 06:00 IST - Wikiquote sourcing — approved author list, deduplication, round-robin rotation
- Local fallback — curated
quotes.jsonwhen Wikiquote is unavailable - AI reflections — optional Ollama Cloud for the daily reflection line (quote text unchanged)
- Idempotent sends — tracks sent dates in
data/state.json; restarts never duplicate - Health webhook (optional) — Apple Shortcuts posts daily Health stats (steps, sleep, workouts) to a webhook; the bot stores them and posts a Hindi health report with streaks and a 7-day average. See docs/HEALTH-SHORTCUT.md
- Fly.io ready — always-on machine with persistent volume for auth and state
- Links to WhatsApp as a paired device and stays connected.
- At the configured time, fetches a quote (Wikiquote → local fallback).
- Optionally enriches the reflection via Ollama Cloud.
- Sends to the target group JID and records the send in state.
- Node.js 20+
- A WhatsApp account that can link a new device
- Docker Compose (VPS) or Fly.io (recommended)
git clone git@github.com:sidntrivedi/wapp-quote.git
cd wapp-quote
npm install
cp .env.example .envEdit .env with your phone number and schedule:
AUTH_METHOD=pairing
PAIRING_PHONE_NUMBER=91XXXXXXXXXX
QUOTE_TIME=06:00
TZ=Asia/KolkataPair, find your group, and configure the target:
npm run dev -- pair # link WhatsApp (clears stale session first)
npm run dev -- list-groups # copy the JID ending in @g.us
# set WHATSAPP_GROUP_JID in .env
npm run dev -- preview # dry run
npm run dev -- send-now # send immediately
npm run dev -- serve # start daily schedulerPairing alternatives
| Command | Use when |
|---|---|
pair-qr |
Phone-number pairing is rejected |
reset-auth |
You need a clean session before re-pairing |
| Command | Description |
|---|---|
serve |
Run the daily scheduler (production mode) |
send-now |
Send a quote immediately (forces send, even if already sent today) |
preview |
Print the next quote without sending |
list-groups |
List group names and JIDs |
pair |
Link WhatsApp via pairing code |
pair-qr |
Link WhatsApp via QR code |
reset-auth |
Delete saved WhatsApp session |
help |
Show command help |
See .env.example for the full list. Key variables:
| Variable | Default | Description |
|---|---|---|
WHATSAPP_GROUP_JID |
— | Target group (ends with @g.us) |
QUOTE_TIME |
06:00 |
Send time, 24-hour local format |
TZ |
Asia/Kolkata |
Timezone for schedule and date tracking |
QUOTE_SOURCE |
wikiquote |
wikiquote or local |
WIKIQUOTE_LANGUAGE |
hi |
Hindi Wikiquote (hi.wikiquote.org) |
WIKIQUOTE_MODE |
pages |
pages, authors, or any |
AI_PROVIDER |
none |
none, openai, or ollama-cloud |
OPENAI_API_KEY |
— | Required when AI_PROVIDER=openai |
OPENAI_MODEL |
gpt-4o-mini |
OpenAI chat model |
OLLAMA_API_KEY |
— | Required when AI_PROVIDER=ollama-cloud |
HEALTH_WEBHOOK_ENABLED |
false |
Enable the Apple Shortcuts health webhook |
HEALTH_WEBHOOK_PORT |
8080 |
Port the webhook listens on |
HEALTH_WEBHOOK_TOKEN |
— | Bearer secret; required when the webhook is enabled |
HEALTH_GROUP_JID |
— | Group for health reports (falls back to WHATSAPP_GROUP_JID) |
HEALTH_STEP_GOAL |
8000 |
Daily steps goal for the ✅ mark and streaks |
Wikiquote modes
pages— built-in approved author list (recommended)authors— random author from Wikiquote categoriesany— random page (less reliable attribution)
Override the author list with WIKIQUOTE_PAGES=page\|author,page\|author.
AI reflection
When AI_PROVIDER=openai or AI_PROVIDER=ollama-cloud, the bot calls the configured provider to generate the reflection line (आज की दिशा). The quote and author are never modified. On failure or validation error, the built-in fallback is used.
Recommended for OpenAI: AI_PROVIDER=openai with OPENAI_MODEL=gpt-4o-mini (~1 call/day, very low cost).
When HEALTH_WEBHOOK_ENABLED=true, the serve process also runs an HTTP server
that accepts a daily health payload from an Apple Shortcut and posts a Hindi
health report to your group. It reuses the same WhatsApp session — no second
process or login.
HEALTH_WEBHOOK_ENABLED=true
HEALTH_WEBHOOK_TOKEN=$(openssl rand -hex 32)
HEALTH_GROUP_JID=120363xxxxxxxxxxxxxx@g.us # optional; defaults to WHATSAPP_GROUP_JID
HEALTH_STEP_GOAL=8000Endpoints:
GET /healthz— liveness probePOST /health— ingest payload; auth viaAuthorization: Bearer <token>orx-webhook-token
curl -X POST "https://<app>.fly.dev/health" \
-H "Authorization: Bearer $HEALTH_WEBHOOK_TOKEN" \
-H "content-type: application/json" \
-d '{"steps":9123,"sleepSeconds":27000,"exerciseMinutes":35}'The report is posted once per day (use ?force=true to re-post). Full setup,
payload reference, and the Apple Shortcut walkthrough are in
docs/HEALTH-SHORTCUT.md.
One always-on machine with a persistent volume at /app/data for WhatsApp auth and send state.
brew install flyctl && fly auth loginEdit app in fly.toml, then:
fly apps create <your-app-name>
fly volumes create wapp_quote_data --size 1 --region sin --app <your-app-name>
fly secrets set \
PAIRING_PHONE_NUMBER=91XXXXXXXXXX \
WHATSAPP_GROUP_JID=120363xxxxxxxxxxxxxx@g.us \
OPENAI_API_KEY=your_key \
--app <your-app-name>
fly deploy --app <your-app-name>To enable the health webhook in production, also set its secret (the fly.toml
already turns the feature on and exposes HTTPS):
fly secrets set HEALTH_WEBHOOK_TOKEN="$(openssl rand -hex 32)" --app <your-app-name>
# optional, if health reports go to a different group:
fly secrets set HEALTH_GROUP_JID=120363xxxxxxxxxxxxxx@g.us --app <your-app-name>On first deploy, watch fly logs for the pairing code. Auth is stored on the volume at /app/data/auth.
Operations
fly status --app <your-app-name>
fly logs --app <your-app-name>
fly ssh console -C "node dist/src/cli.js send-now" --app <your-app-name>
fly secrets set WHATSAPP_GROUP_JID=<new-jid> --app <your-app-name> # change target groupThere is no Fly Cron job — scheduling runs in-process via node-cron inside the serve process.
docker compose up -d --build
docker compose logs -fThe ./data directory holds WhatsApp auth and send state. Back it up before migrating servers.
npm run build
npm start| Path | Purpose |
|---|---|
data/auth/ |
WhatsApp session credentials |
data/state.json |
Sent dates and used quote IDs |
data/health.json |
Daily health entries and posted markers (when webhook enabled) |
- Restarts do not resend today's quote.
- Failed sends retry up to 3 times; state is updated only after WhatsApp accepts the message.
- Never commit
.envordata/— both are in.gitignore.
- Scheduled send retries — at 6:00 AM, retries up to 3 times (5 minutes apart) before giving up.
- Missed-cron catch-up — if
node-cronmisses the 06:00 slot (timer drift / CPU scheduling), the bot retries onexecution:missedand polls every 15 minutes until today's quote is sent, but only within 4 hours ofQUOTE_TIME(until 10:00 IST by default). After that it logs once and skips today. - Session conflict recovery — reconnects automatically after temporary WhatsApp session conflicts.
- Serve lock —
send-nowandlist-groupsrefuse to run whileserveis active, so SSH tests do not steal the live session. - After re-pairing — run
fly machine restartsoserveloads the new auth session.
npm test
npm run typecheck
npm run validate:quotes
npm run build