Files
QR-master/docs/automations/social-accounts-and-jobs.md
Timo Knuth eb932ebdaa Publish milestones per channel and add Instagram
Consent is bound to the channel it was given for: approving a post on X says
nothing about Instagram. Publishing state moves from the SocialMilestone row
into SocialMilestonePost, one row per channel, where a missing row means no
consent. The dialog asks per channel, shows the text each one will publish and
keeps a separate handle for each; Instagram captions end in hashtags because a
link there is not clickable.

Also fixes three problems in the existing X path:

- A QR code already past several thresholds produced one prompt per threshold,
  and since the post quotes the current scan count, every one of them would
  have published the same number. Only the highest threshold is announced now.
- Detection ran after every unique scan and re-read the QR code's full scan
  history just to hit skipDuplicates. Known milestones are filtered first.
- A failed post stayed failed forever because the consent dialog only opens
  once. The queue now retries three times on its own, spaces first attempts by
  SOCIAL_MILESTONE_MIN_GAP_HOURS, and Settings lists every milestone per
  channel with restart and revoke.

The worker no longer renders the card itself; it downloads the image the app
renders at /s/m/<token>/og, which also serves the new square and portrait
formats. Instagram publishing stays off until SOCIAL_MILESTONE_CHANNELS and
SOCIAL_WORKER_CHANNELS both name it.

Schema changes are manual SQL, see sql/2026-08-16_*.sql. Run both before
deploying this version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 13:46:13 +02:00

107 lines
8.4 KiB
Markdown

# Social Accounts
## Account Mapping
| Brand | Facebook Page | Facebook Page ID | Instagram Business Account | Instagram User ID | X/Twitter Account | Target Username |
| --- | --- | --- | --- | --- | --- | --- |
| QRMaster.net | QRMaster.net | 884792004727212 | QRMaster.net | 17841480094799114 | TIMO_QRMASTER | qrmaster |
| GreenLens Pro | GreenLens Pro | 1150261294842586 | GreenLens Pro | 17841422012320229 | greenlensproai | greenlens.pro |
## Brand Rules
- QRMaster-Posts dürfen **nur** auf `QRMaster.net`-Accounts posten.
- GreenLens-Posts dürfen **nur** auf `GreenLens Pro`-Accounts posten.
- Kein Crossposting zwischen Brands.
- Token niemals öffentlich teilen; nur Referenz in dieser Datei.
## Scripts
| Path | Purpose |
| --- | --- |
| `C:\Users\timo\Documents\meta_token_refresh.py` | Meta Long-Lived + Page Token Refresh |
| `C:\Users\timo\Documents\instagram_r2_carousel_post.py` | Instagram single-image/carousel über R2 |
| `C:\Users\timo\x-api-autopost\x_media_post.py` | X/Twitter for QRMaster |
| `C:\Users\timo\greenlens-x-autopost\` | X/Twitter for GreenLens |
## Token Files / Paths
| Path | Contains |
| --- | --- |
| `C:\Users\timo\Documents\meta_instagram_tokens.env` | Meta Facebook + Instagram long-lived/page tokens |
| `C:\Users\timo\Documents\r2_social_media.env` | R2 upload credentials for IG assets |
| `C:\Users\timo\Documents\instagram_r2_carousel_post.py` | uses both env files above |
| `C:\Users\timo\x-api-autopost\.env` | X/Twitter QRMaster creds (`X_API_KEY`, `X_API_SECRET`, `X_ACCESS_TOKEN`, `X_ACCESS_TOKEN_SECRET`) |
| `C:\Users\timo\Documents\greenlens-x-autopost\.env` | X/Twitter GreenLens creds |
| `C:\Users\timo\Documents\greenlens\Greenlens\.env` | GreenLens TikTok + plant import admin creds |
## QRMaster TikTok Cron Credentials
- Source of truth: the server-side `.env` for the QRMaster app.
- Required variables: `TIKTOK_CLIENT_KEY`, `TIKTOK_CLIENT_SECRET`, `TIKTOK_REDIRECT_URI`.
- Optional helper: `TIKTOK_ADMIN_KEY` for guarding the `/api/tiktok/connect` start route.
- OAuth result: access/refresh tokens are stored in the database after `/api/tiktok/connect` and `/api/tiktok/callback`.
- Do not put TikTok access tokens in `.env`; the cron job reads them from the DB through the app flow.
- Configure `TIKTOK_EXPECTED_OPEN_ID` for QRMaster before deploying the hardened routes. The callback and every API call fail closed when it is missing or belongs to another account.
- `GET /api/tiktok/token` is status-only and never returns access or refresh tokens. Automations must call the server upload route instead of caching credentials locally.
- For cron posting, use the same QRMaster server environment that already contains `CRON_SECRET` / `INTERNAL_API_SECRET` for internal APIs.
## TikTok Connection Status + Credential Locations
- QRMaster TikTok OAuth is connected and tested live via `https://qrmaster.net/api/tiktok/callback`; the callback success screen returned `TikTok account connected. You can close this tab.`
- GreenLens TikTok OAuth is connected and tested live via `https://greenlenspro.com/api/tiktok/callback`; the callback success screen returned `TikTok account connected. You can close this tab.`
- QRMaster app credentials live in the QRMaster server-side `.env` and are wired through `QR-Master/docker-compose.yml` (`TIKTOK_CLIENT_KEY`, `TIKTOK_CLIENT_SECRET`, `TIKTOK_REDIRECT_URI`, `TIKTOK_ADMIN_KEY`).
- GreenLens app credentials live in `C:\Users\timo\Documents\greenlens\Greenlens\.env` / the corresponding server-side `.env` and are wired through `Greenlens/docker-compose.yml` (`TIKTOK_CLIENT_KEY`, `TIKTOK_CLIENT_SECRET`, `TIKTOK_REDIRECT_URI`, `PLANT_IMPORT_ADMIN_KEY`).
- Hermes scratch/test reference file for local operator checks: `C:\Users\timo\.hermes\.env`. Treat it as local helper state, not production source of truth.
## TikTok Posting Policy for Cron Jobs and Live Posting
- Default policy for **both** QRMaster and GreenLens: TikTok jobs should **upload/draft only**, never direct-post, unless Timo explicitly says otherwise.
- If Timo says only **"Go"**, interpret that as permission for TikTok **upload/draft only**. Timo finishes the final publish manually inside TikTok.
- QRMaster specifically should be treated as **upload-only**.
- GreenLens should also remain **upload-only by default** unless Timo explicitly asks for a different behavior.
- Do not treat a generic approval like "Go" as permission for automatic/direct TikTok publishing.
## TikTok Analytics (Display API, read-only)
- The requested scopes MUST exactly match what each app has approved in the TikTok Developer Portal — requesting an unapproved scope aborts the whole OAuth login with a generic "scope" error.
- Approved scopes per app (as of 2026-07-09):
- QRMaster: `user.info.basic, user.info.profile, user.info.stats, video.list, video.upload` (NO `video.publish` — upload/draft only).
- GreenLens Pro: `user.info.basic, video.upload, video.publish` (NO analytics scopes yet — a portal revision adding `user.info.stats` + `video.list` is required before GreenLens analytics works).
- Admin-key-protected live analytics endpoints (no data is stored, every call reads fresh from TikTok):
- QRMaster: `GET /api/tiktok/analytics?key=<TIKTOK_ADMIN_KEY>&max_videos=50`
- GreenLens: `GET /api/tiktok/analytics` (guarded by the plant import admin key)
- Response: account stats (followers, total likes, video count) plus per-video views/likes/comments/shares with computed `engagement_rate`, `posted_weekday_utc` and `posted_hour_utc`, and a summary block (totals, average/median views).
- After any scope change the account must re-authorize via `/api/tiktok/connect`, otherwise the stored token keeps the old permissions.
## TikTok Photo/Carousel Upload Notes
- Photo posts use `POST /v2/post/publish/content/init/` with `post_mode: MEDIA_UPLOAD` (draft in the creator's inbox, needs only `video.upload`). Valid post modes are only `MEDIA_UPLOAD` and `DIRECT_POST``DRAFT` is not a valid value.
- Photos are delivered via `source_info.photo_images` as public URLs (`PULL_FROM_URL`). TikTok only pulls from **verified domains** — verify the hosting domain (e.g. `greenlenspro.com` for MinIO storage URLs) under Content Posting API → "Verify domains" in the Developer Portal, otherwise the upload fails.
- QRMaster's only verified property is `qrmaster.net` (via `public/tiktokVwGRbyf2BbBLqUlFrnehtntSEU9Ihiok.txt`). The Cloudflare R2 public domain (`pub-*.r2.dev`, used for Instagram) is **not** verified — never pass R2 URLs to the TikTok photo API for QRMaster.
## QRMaster Social Asset Hosting (no-deploy image URLs)
- `POST https://qrmaster.net/api/social-assets` (header `x-admin-key: <TIKTOK_ADMIN_KEY>`) with JSON `{ "files": [{ "filename", "mimeType", "dataBase64" }] }` stores images in PostgreSQL and returns public `https://qrmaster.net/api/social-assets/<id>` URLs on the verified domain.
- `GET /api/social-assets/<id>` serves the file publicly (immutable cache); `GET /api/social-assets` (admin) lists the last 100 assets; `DELETE /api/social-assets/<id>` (admin) removes one.
- Allowed types: jpeg/png/webp/mp4, max 10 MB per file.
- The `SocialAsset` table must be created manually on the server (one-time, no auto-create in the route). Run on the server:
```bash
docker-compose exec db psql -U postgres -d qrmaster -c 'CREATE TABLE IF NOT EXISTS "SocialAsset" ("id" TEXT PRIMARY KEY, "filename" TEXT NOT NULL, "mimeType" TEXT NOT NULL, "data" BYTEA NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP);'
```
- Typical carousel flow: upload slides here → pass the returned URLs as `photos` to `POST /api/tiktok/upload` → check via `/api/tiktok/upload/status?publish_id=...`. No app deploy needed per carousel.
## Refresh Behavior
- Meta user/page tokens werden automatisch refreshed durch `python C:\Users\timo\Documents\meta_token_refresh.py`.
- QRMaster Page-Auswahl erzwingt Page-ID `884792004727212`.
- GreenLens Page-Auswahl erzwingt Page-ID `1150261294842586`.
## Cron Jobs
| Job Name | Schedule | Deliver | Notes |
| --- | --- | --- | --- |
| `Weekly Meta Token Refresh` | Montag 09:00 | Telegram + Email | Refreshes Meta tokens |
> Social-package deliverables only after explicit `Go`.
## Publish Checklist
1) Brand wählen → nur passende Accounts zulassen.
2) Media vorbereiten: QRMaster → R2-Prefix `qrmaster/`, GreenLens → `greenlens/`.
3) Facebook: Page-ID + Page-Token verwenden.
4) Instagram: IG User ID + Page-Token über Container + `media_publish`.
5) X: zugehöriges Brand-Skript + zugehöriges `.env` verwenden.
6) Keine Token/Secrets loggen.