Skip to content

Coronelcortez/fortune-motd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

11 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Thematic Daily Message System

Release Python FastAPI Docker License

A self-hosted FastAPI service that generates a coherent themed daily message from live APIs and can deliver it to WhatsApp groups through Evolution API.

Each daily message contains:

  • 1 poem
  • 1 quote
  • 1 joke
  • 1 historical event
  • 1 curious fact

All items are matched to one controlled theme. The system is API-first, keeps only minimal local state, and uses deterministic date-based generation so the same day returns the same saved result.

Features

  • Dynamic content from PoetryDB, Quotable/Wikiquote, JokeAPI, Wikidata SPARQL, and Useless Facts.
  • Small local fallback fortune file for resilience when external APIs fail.
  • Fixed theme vocabulary with local keyword and synonym scoring. No LLM is used for theme extraction or item matching.
  • Date-seeded generation with persisted daily outputs in JSON.
  • WhatsApp text and mobile-friendly server-rendered HTML output.
  • Evolution API integration with randomized delivery order, retry handling, optional group skips, and a hard 120 second minimum delay between sends.
  • FastAPI endpoints for health, current daily message, and generation history.
  • Docker and Docker Compose setup, including an Evolution API service stub.

Theme Vocabulary

The system only selects from this fixed set:

love, death, war, nature, time, religion, politics, technology, science,
philosophy, humor, absurdity, friendship, family, power, freedom, suffering,
hope, fear

Theme extraction is handled in app/theme.py by tokenizing text and scoring it against local keyword sets. This keeps matching deterministic and avoids using an LLM for classification.

How It Works

  1. The generator receives a date in YYYY-MM-DD format.
  2. It creates a deterministic RNG seed from sha256(YYYY-MM-DD).
  3. It picks a leader item type, fetches a leader item, and extracts the theme from that item.
  4. It fetches candidate items for the other content types using theme keywords where the upstream API supports it.
  5. It ranks candidates locally by theme score and deterministic hash ordering.
  6. It assembles WhatsApp text and HTML.
  7. It stores the result in daily_outputs.json. Future requests for the same date return the stored result.

Project Layout

app/
  clients/          API clients and fallback data client
  config.py         Environment-based settings
  delivery.py       WhatsApp group delivery engine
  generator.py      Daily generation pipeline
  matching.py       Theme-aware item matching and ranking
  models.py         Pydantic schemas
  rendering.py      WhatsApp and HTML rendering
  scheduler.py      Optional internal daily scheduler
  storage.py        JSON persistence
  theme.py          Theme vocabulary, tokenizer, scorer
  web.py            FastAPI application
  whatsapp.py       Evolution API client
data/
  fortunes.json     Small local fallback dataset
tests/
  ...               Unit and route tests

Requirements

  • Python 3.12+
  • Docker and Docker Compose, if running containerized
  • Network access to the configured public APIs
  • Evolution API, only if WhatsApp delivery is enabled

Quick Start

python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env
uvicorn app.web:app --reload --env-file .env

Then open:

  • JSON: http://127.0.0.1:8000/today
  • HTML: http://127.0.0.1:8000/today?format=html
  • WhatsApp text: http://127.0.0.1:8000/today?format=text
  • Health: http://127.0.0.1:8000/health

The root path, http://127.0.0.1:8000/, redirects to the HTML daily view.

To save the generated outputs locally:

mkdir -p out
curl -fsS http://127.0.0.1:8000/today -o out/today.json
curl -fsS "http://127.0.0.1:8000/today?format=html" -o out/today.html
curl -fsS "http://127.0.0.1:8000/today?format=text" -o out/today-whatsapp.txt

Open out/today.html in your browser and use out/today-whatsapp.txt as the message body for a manual WhatsApp send.

If the browser appears to load forever, test the plain text endpoint first:

curl -v -m 45 "http://127.0.0.1:8000/today?format=text"

On first generation the app may wait briefly for upstream APIs. If a source is slow, it is abandoned after SOURCE_TIMEOUT_SECONDS and the local fallback is used. The storage files are created at startup, so their presence alone does not mean daily generation completed; daily_outputs.json must contain an entry for today's date.

PoetryDB can be slow or intermittently unavailable. The poetry client intentionally uses the smallest random endpoint and lets the local fallback fill the poem slot if PoetryDB does not respond before the source timeout.

Docker

cp .env.example .env
docker compose up --build

The application listens on http://127.0.0.1:8000 by default.

The Compose file defines:

  • app: the FastAPI generator and API service
  • evolution-api: an Evolution API container placeholder
  • fortune_data: persisted generated outputs and group metadata
  • evolution_instances: Evolution API instance state

The Compose file uses environment defaults, so docker compose config works even before .env exists. For real deployments, create .env and replace placeholder secrets.

Docker Swarm

Build and publish the image to a registry reachable by your Swarm nodes:

docker build -t your-registry/fortune-motd:latest .
docker push your-registry/fortune-motd:latest

Update the app.image value in docker-compose.yml, then deploy:

docker stack deploy -c docker-compose.yml fortune-motd

Use Swarm secrets or your platform's secret manager for EVO_API_KEY in production.

Configuration

Configuration is read from environment variables. .env.example contains working defaults for local use.

Variable Default Purpose
POETRYDB_BASE_URL https://poetrydb.org Poetry API base URL
QUOTABLE_BASE_URL https://api.quotable.io Quotable API base URL
WIKIQUOTE_API_URL https://en.wikiquote.org/w/api.php Wikiquote MediaWiki API URL
JOKEAPI_BASE_URL https://v2.jokeapi.dev JokeAPI base URL
WIKIDATA_SPARQL_URL https://query.wikidata.org/sparql Wikidata SPARQL endpoint
USELESS_FACTS_BASE_URL https://uselessfacts.jsph.pl Useless Facts API base URL
REQUEST_TIMEOUT_SECONDS 5 Timeout per external API request
REQUEST_RETRIES 1 Retry count for external API calls
SOURCE_TIMEOUT_SECONDS 8 Hard maximum time spent on one content source before fallback
LANGUAGES en,fr,es Preferred language list; current upstream support is source-dependent
TIMEZONE Europe/Madrid Time zone used by /today and the scheduler
SCHEDULE_TIME 08:00 Daily scheduler run time
ENABLE_SCHEDULER false Enables the internal daily scheduler
STORAGE_DIR data/storage locally; Docker forces /data Runtime storage directory
FALLBACK_FORTUNES_PATH data/fortunes.json Small local fallback dataset
WHATSAPP_ENABLED false Enables WhatsApp delivery
WHATSAPP_SKIP_PROBABILITY 0.05 Optional behavioral skip probability per group
WHATSAPP_MIN_DELAY_SECONDS 120 Hard minimum delay between group sends
WHATSAPP_MAX_DELAY_SECONDS 300 Maximum randomized delay between group sends
WHATSAPP_MAX_ATTEMPTS 2 Send attempts per group
MAX_ITEM_CHARS 1200 Global maximum stored text length for any item
MAX_POEM_CHARS 900 Maximum poem text length
MAX_QUOTE_CHARS 350 Maximum quote text length
MAX_JOKE_CHARS 500 Maximum joke text length
MAX_EVENT_CHARS 450 Maximum historical event text length
MAX_FACT_CHARS 320 Maximum curious fact text length
MAX_POEM_LINES 16 Maximum poem lines kept from PoetryDB responses
MAX_WHATSAPP_CHARS 3000 Final maximum WhatsApp message length
EVO_BASE_URL empty locally, http://evolution-api:8080 in Compose Evolution API base URL
EVO_API_KEY empty locally, change-me in Compose Evolution API key
EVO_INSTANCE empty locally, default in Compose Evolution API instance name

WHATSAPP_MIN_DELAY_SECONDS must be at least 120. The app will fail startup if a lower value is configured.

Storage

The app uses JSON files for minimal persistence:

STORAGE_DIR/
  daily_outputs.json
  group_store.json
  history.json

daily_outputs.json stores generated daily messages keyed by date. history.json stores seed hashes used for history metadata. group_store.json stores WhatsApp group delivery state.

Runtime storage is ignored by Git. In Docker, it is mounted through the fortune_data volume.

API Routes

GET /health

Returns service status and basic configuration flags:

{
  "ok": true,
  "storage_dir": "data/storage",
  "whatsapp_enabled": false,
  "scheduler_enabled": false
}

GET /today

Returns the generated daily output as JSON. The response includes the normalized item schema for each type, the rendered WhatsApp text, and the rendered HTML string.

GET /today?format=html

Returns the same daily output as a server-rendered HTML page.

GET /today?format=text

Returns the final WhatsApp-ready text message as text/plain.

GET /history

Returns compact history metadata for generated days. Use ?limit=30 to adjust the result count up to 365.

Normalized Item Schema

All API clients normalize upstream responses into this shape:

{
  "id": "string",
  "type": "poem|quote|joke|event|fact",
  "language": "en|fr|es|unknown",
  "title": "string|null",
  "author": "string|null",
  "year": "integer|null",
  "date": "string|null",
  "content": "string",
  "tags": ["string"],
  "source": "string"
}

WhatsApp Delivery

WhatsApp delivery is disabled by default. To enable it:

  1. Configure and authenticate Evolution API.
  2. Set WHATSAPP_ENABLED=true.
  3. Set EVO_BASE_URL, EVO_API_KEY, and EVO_INSTANCE.
  4. Add group entries to ${STORAGE_DIR}/group_store.json.

Example group_store.json:

{
  "groups": [
    {
      "id": "120363XXXX@g.us",
      "name": "Group Name",
      "timezone": "Europe/Madrid",
      "enabled": true,
      "last_sent": null
    }
  ]
}

The sender posts to:

POST /message/sendText/{instance}

with:

{
  "number": "<group_id>",
  "text": "<message>"
}

Delivery behavior:

  • Enabled groups are shuffled deterministically per day.
  • Each group may set an IANA timezone such as Europe/Madrid or America/New_York.
  • A group is sent only when its local time is at or after SCHEDULE_TIME.
  • Groups already sent for the same day are skipped.
  • About 5% of groups are skipped by default through WHATSAPP_SKIP_PROBABILITY.
  • Failed sends are retried up to WHATSAPP_MAX_ATTEMPTS.
  • Failures are logged and do not stop the loop.
  • The engine waits a random delay between WHATSAPP_MIN_DELAY_SECONDS and WHATSAPP_MAX_DELAY_SECONDS after each attempted send.

Important: This project implements conservative pacing controls, but no automation can guarantee avoidance of platform enforcement. Use responsibly and comply with WhatsApp and Evolution API policies.

Scheduling

You can run generation with either external cron or the internal scheduler.

Internal scheduler:

ENABLE_SCHEDULER=true
SCHEDULE_TIME=08:00
TIMEZONE=Europe/Madrid

When enabled, the scheduler generates the daily output at the configured local time and runs WhatsApp delivery if WHATSAPP_ENABLED=true.

External cron example:

0 8 * * * curl -fsS http://127.0.0.1:8000/today >/dev/null

If you use external cron for WhatsApp delivery, wire it through application code or add a small command entrypoint that calls DailyGenerator and DeliveryEngine.

Output Format

The WhatsApp text follows this format:

Theme of the Day: Hope

πŸ“œ Poem
...

πŸ’¬ Quote
...

πŸ˜„ Joke
...

πŸ“… On this day
...

🧠 Fact
...

The HTML page uses the same stored output, so JSON, WhatsApp text, and HTML stay consistent for a given date.

Development

Install development dependencies:

pip install -e ".[dev]"

Run tests:

pytest

Run a syntax/import check:

python -m compileall app

Validate Compose:

docker compose config

Build the image:

docker compose build app

Smoke test the built image:

docker run --rm -d -p 8001:8000 --name fortune-motd-smoke fortune-motd:latest
curl -fsS http://127.0.0.1:8001/health
docker stop fortune-motd-smoke

API Sources

  • Poetry: PoetryDB
  • Quotes: Quotable, with Wikiquote fallback
  • Jokes: JokeAPI
  • Historical events: Wikidata SPARQL
  • Curious facts: Useless Facts API
  • Fallback: small local data/fortunes.json

Respect each upstream service's terms, rate limits, and attribution requirements for your deployment.

The fallback dataset intentionally contains one small item for every supported theme and content type. If an upstream source fails or times out, the generator can still produce a complete WhatsApp-ready message without keeping a large local corpus.

Production Notes

  • Keep STORAGE_DIR on persistent storage.
  • Keep .env out of version control.
  • Replace EVO_API_KEY=change-me before enabling WhatsApp.
  • Prefer platform secrets for credentials in Swarm or hosted deployments.
  • Monitor logs for API failures and WhatsApp send failures.
  • Consider adding a reverse proxy and TLS if exposing the web service publicly.
  • Keep scheduler replicas to 1, or use an external scheduler, to avoid duplicate sends.

License

This project is licensed under the Apache License 2.0. See LICENSE.

About

Self-hosted thematic daily message generator with FastAPI, API fallbacks, and WhatsApp delivery.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors