Files
bitsforfree/README.md
T

135 lines
8.0 KiB
Markdown

# The Meme Protocol
Small self-hosted meme gallery matching the `stitch_the_meme_protocol` desktop mockup direction.
Current release: `1.1.0`.
The browser feed supports one-click image copy, downloads, shareable meme links, full-image viewing with live view/download counts, a fixed source footer, a two-build Homo Reseticus pet download chooser, and shareable lore at `/lore`.
Homepage and lore social shares use the fingerprinted, metadata-stripped `1200x630` JPEG at `/assets/social-preview-v2.78d008df9860.jpg`. Individual meme permalinks continue to use their own meme image.
Browser tabs, bookmarks, Apple home-screen links, and installed web apps use the supplied favicon suite under `/assets/favicon/`; the legacy `/favicon.ico` URL remains available for automatic browser requests.
## Run locally
```sh
npm start
```
The server listens on `http://localhost:8080` by default.
## Configuration
- `PORT`: HTTP port, default `8080`
- `HOST`: bind address, default `0.0.0.0`
- `DATA_DIR`: disk storage root, default `./data`
- `SITE_URL`: public canonical site URL used for SEO metadata, sitemaps, feeds, `llms.txt`, OpenAPI servers, and API skill instructions. Production should use `https://bitsforfree.com`.
- `SEED_DEMO_MEMES`: set to `false` to disable generated demo memes. When enabled, empty and legacy demo-only stores are filled to 18 samples so local pagination can be tested.
- `ADMIN_TOKEN`: secret review URL token. If omitted, one is generated at boot and printed in server logs.
- `OPENAI_API_KEY`: enables AI upload moderation. Without it, uploads are queued for admin review.
- `OPENAI_MODERATION_MODEL`: moderation vision model, default `gpt-4o-mini`
- `AUTO_APPROVE_MIN_SCORE`: minimum `MEME_CONSENSUS_SCORE` for immediate AI approval, default `80`
- `TRUST_PROXY`: set to `true` when running behind a trusted reverse proxy so upload limits use `X-Forwarded-For`
Uploads accept PNG and JPEG images. The server rejects files over 5 MB, any image edge over `6000px`, and images over 20 million pixels. Accepted uploads are decoded, metadata-stripped, resized so the longest edge is at most `1600px`, and stored as WebP.
Upload caps are 5 per hour per IP, 10 per day per IP, and 100 globally per day. Strong AI-approved uploads publish immediately; ambiguous or low-quality uploads are queued for the secret admin review page; likely illegal uploads are rejected immediately.
Files are stored under sharded date/hash paths:
```text
data/
index/memes.jsonl
index/tags.json
memes/YYYY/MM/DD/aa/bb/<sha256>.<ext>
meta/YYYY/MM/DD/aa/bb/<sha256>.json
```
## Discovery Metadata
The app serves crawler and answer-engine metadata without adding visible page copy:
- `/openapi.json`
- `/meme-api.skill.md`
- `/robots.txt`
- `/sitemap.xml` with the home page and approved meme URLs
- `/feed.json`
- `/llms.txt`
- `/site.webmanifest`
- Open Graph, Twitter card, canonical, and JSON-LD metadata on `/`
Set `SITE_URL` in production so canonical and API discovery URLs use the public domain instead of an internal proxy hostname. The server loads `.env` at startup when present, while real environment variables still take precedence.
## Public API
- `GET /api/memes?page=1&pageSize=12`: lists approved memes with `caption` and `tags`. `pageSize` is capped at `48`. Add `q=angry%20cat` for ranked caption/topic search and `tag=cats` for a topic filter. Repeat `tag` to require all selected topics.
- `GET /api/tags`: lists the available managed tag names.
- `GET /api/memes/<sha256>`: returns public metadata for one approved meme.
- `GET /meme/<sha256>`: opens an approved meme in the viewer and provides its shareable permalink.
- `GET /media/<sha256>`: returns the normalized WebP image for one approved meme.
- `POST /api/memes`: adds one PNG/JPEG upload as `multipart/form-data` with a file field named `meme`.
API uploads use the exact same path as the browser form: persistent IP upload quotas, file size checks, image dimension and pixel checks, metadata-stripping WebP normalization, AI moderation, and admin-review queueing. A published upload returns `201`; a queued upload returns `202` with `moderationReason`; moderation rejection returns `422` with `moderationReason`.
Public API reads are rate-limited per IP and return `429` with `Retry-After` when exceeded. List and metadata responses include `RateLimit-*` headers and a `rateLimit` object.
## Captions, tags, and administration
The LLM moderation call also writes a factual searchable caption and chooses relevant tags from the admin-managed catalog. The server filters out invented tags. Uploaders cannot supply captions or tags; reads and search are public, while every metadata mutation requires the existing `X-Admin-Token` authentication.
Search is lexical: case/accent-insensitive words, prefixes, and small spelling corrections, ranked by relevance then newest first. All query words and all selected tags must match. It does not use embeddings or infer semantic synonyms. Limits: 200 query characters, 20 tag filters, 200 managed tags, 60 characters per tag, and 2000 caption characters.
Open `/admin/<ADMIN_TOKEN>` to manage pending, published, or all images; edit captions and assigned tags; and add or remove topics. Tag names are lowercase and accept letters, numbers, spaces, underscores, and hyphens. Removing a tag clears its assignments from every image.
Existing records load with empty captions/tags; no offline migration is needed. Define your topics, then use **GENERATE ALL MISSING CAPTIONS** to backfill all existing images. The browser processes images sequentially, saves each result immediately, and can stop after the current image. Re-running resumes missing captions. **GENERATE AI FOR SELECTED** can regenerate metadata for existing captions (for example after adding topics). Generation uses `OPENAI_API_KEY` and `OPENAI_MODERATION_MODEL`, incurs normal model usage, and leaves existing metadata and approval status intact on failure. Manual edits made while an AI request is running are preserved via a version check. Without an API key, uploads remain pending with empty metadata, and admins can still edit manually.
Authenticated admin endpoints (not part of the public OpenAPI):
- `GET /api/admin/memes?page=1&pageSize=12&status=all`: paginated image management; `status` accepts `pending`, `approved`, or `all`. Supports `q`, repeated `tag`, and `missingCaption=true`.
- `PATCH /api/admin/memes/<sha256>` with `{ "caption": "...", "tags": ["cats"] }`: edit either or both metadata fields; unknown tags are rejected.
- `POST /api/admin/memes/<sha256>/generate`: generate caption and tags without re-moderating; optional `{ "onlyMissing": true }` skips already-captioned images.
- `GET /api/admin/tags`, `POST /api/admin/tags` with `{ "name": "cats" }`, and `DELETE /api/admin/tags/<URL-encoded-name>`: manage the catalog.
Admin responses use `Cache-Control: no-store`. Unauthenticated admin API requests return `404`.
## Docker
```sh
docker build -t meme-protocol .
docker run --rm -p 8080:8080 -v meme-protocol-data:/data meme-protocol
```
For production, copy `.env.example` to `.env`, set real secrets, then run:
```sh
docker compose up -d --build
```
The included compose file binds the app to `127.0.0.1:18080` on the host so a reverse proxy can publish it without exposing the Node container directly.
### Production Build Note
On the production host used for this project, npm registry downloads from inside Docker timed out unless `registry.npmjs.org` was pinned to a known-good IPv4 address during build. The proven build command is:
```sh
sudo docker build \
--network=host \
--add-host registry.npmjs.org:104.16.1.34 \
-t meme-protocol:latest .
```
Then start with the already-built image:
```sh
sudo docker compose up -d --no-build
```
If the registry IP ever stops working, resolve and test another IPv4 address for `registry.npmjs.org`, then replace `104.16.1.34` in the build command.
When using a host-mounted data directory, the container writes as UID/GID `10001:10001`:
```sh
sudo mkdir -p ./data
sudo chown -R 10001:10001 ./data
sudo chmod -R u+rwX,g+rwX,o-rwx ./data
```