Add consent-gated media exchange library

This commit is contained in:
jjbrunton committed 2026-08-09 19:24:38 +01:00
1 parent b8e4a1bb88
commit bf474dfe10
42 files changed
+2533 -9

No files matched your search

+4
View File
@@ -80,6 +80,10 @@ an administrator credential.
| `GET` | `/api/media/sync` | Get media sync state | read | no |
| `POST` | `/api/media/sync` | Trigger media sync | admin | yes |
| `POST` | `/api/media/sync/full` | Trigger full media sync | admin | yes |
| `GET` | `/api/media-library` | List operator-owned exchange images | read | no |
| `POST` | `/api/media-library` | Upload and classify an operator-owned exchange image | admin | yes |
| `PATCH` | `/api/media-library/{id}` | Correct tags or enable an exchange image | admin | yes |
| `DELETE` | `/api/media-library/{id}` | Delete an exchange image | admin | yes |
| `POST` | `/api/start` | Start bot processing (legacy) | admin | yes |
| `POST` | `/api/stop` | Stop bot processing (legacy) | admin | yes |
| `GET` | `/api/timers` | Get rate-limit timers | read | no |
+3 -1
View File
@@ -58,7 +58,8 @@ SIGINT/SIGTERM abort shared loops, stop account runtimes, stop the web server, c
- Reddit request coordinator (shared across runtime construction);
- recipient history/reservations across accounts;
- Express server, dashboard assets, event stream, and operational jobs;
- configured subreddit catalog and application policy.
- configured subreddit catalog and application policy;
- shared operator-owned exchange library, classification metadata, and outbound-use ledger;
### Account-scoped
@@ -132,6 +133,7 @@ docs/ Maintainer and agent documentation
- New transport field: define it in `packages/api-contracts`, return it from the route, consume it in the dashboard, and add contract/route/UI tests.
- New API operation: add a focused router, register it in `src/web/server.ts`, describe it in `src/web/api-catalog.ts`, regenerate `docs/api.md`, and add authorization tests.
- New persisted state: add an append-only migration, expose it through a small repository, and test fresh plus upgraded databases.
- Shared operator media: keep it under `MediaLibraryService`/`MediaLibraryRepository`; never merge it with account-owned received media.
- New account behavior: place it in the account runtime or a service injected into it; do not add mutable cross-account identity to a singleton.
- New candidate stage: make transitions durable and idempotent, expose counts/reasons, and define lease/retry behavior.
- New dashboard feature: add a feature slice and route, use shared schemas and query keys, and treat HTTP as authoritative.
+6
View File
@@ -96,6 +96,12 @@ Legacy conversations may make at most one optional autonomous media request, inc
`MediaService` resolves Matrix media, downloads it into the persistent data directory, and writes an account-scoped media index. `MediaSyncService` schedules incremental work only while the account is authenticated. Private media is served behind the same authorization boundary as the dashboard and APIs.
The process-global exchange library is separate. Dashboard uploads are decoded and re-encoded with `sharp`, which validates the actual JPEG/PNG/WebP contents, applies orientation, and removes EXIF/location/camera metadata. OpenRouter then assigns overlapping anatomy tags plus SFW/NSFW classification. Failed or low-confidence classifications remain disabled until the operator corrects them.
For each latest inbound text event, `MediaLibraryExchangeService` detects direct or indirect image requests before normal reply generation. Exact requests require the matching tag. Generic requests use the conversation's completed library-send count to broaden the eligible safety tier and randomly choose an unused asset. A unique durable reservation on account + conversation + asset prevents reuse, while account + conversation + inbound event makes restart handling idempotent.
After the normal reply limiter waits, automation refreshes Matrix and rechecks that the same inbound request is still latest and that no image, boundary, seller signal, or other terminal state has appeared. It then uploads the private file to Matrix, sends an `m.image` event with a stable transaction ID, and only after confirmation sends the optional reciprocal text. If the image is confirmed but the text is not, the asset remains consumed and the conversation is handed to the operator rather than risking a duplicate.
Conversation performance is derived from the complete attributed inbound transcript. Engagement cannot become a prompt-learning example while automation is active, and a later decline, boundary, or upsell removes an earlier example before future prompts load it.
## Dashboard data and events
+4
View File
@@ -14,6 +14,8 @@ The application uses one `better-sqlite3` database, normally `data/reddit-bot.db
- `reddit_conversations`: account-scoped cached Matrix conversation payloads.
- `reddit_account_sync_state`: account-scoped sync cursor, full/delta timestamps, and invitation cooldown.
- `reddit_media`: account-scoped private-media metadata and relative file path.
- `media_library_assets`: process-global operator-owned image metadata, overlapping anatomy tags, SFW/NSFW classification, consent time, enablement, dimensions, hash, and private relative path.
- `media_library_sends`: account/conversation/inbound-event reservation and confirmed/failed Matrix delivery audit for a shared library asset.
The special legacy-unassigned account exists only to preserve rows created before account ownership was introduced. New runtime data must use a verified account ID.
@@ -50,6 +52,8 @@ The special legacy-unassigned account exists only to preserve rows created befor
5. Conversation policy is snapshotted in state JSON when automation starts for that conversation.
6. Secrets and the database path are not stored in `app_config`.
7. Private media paths in SQLite are relative to the persistent data root; HTTP access remains authenticated.
8. Received `reddit_media` and operator `media_library_assets` are separate ownership domains; no automatic copy or promotion exists.
9. A library image content hash has at most one reservation per account conversation, even if the same file is later re-uploaded under a new asset ID; an inbound request also has at most one selected asset. Failed or uncertain attempts remain reserved to prevent duplicates.
## Repository map
+3
View File
@@ -16,6 +16,7 @@ Shared HTTP/SSE schemas live in `packages/api-contracts`. The server and client
| `/conversations` | Searchable account-aware conversation inbox. |
| `/conversations/:conversationId` | Conversation history, context, suggestions, and guarded sending. |
| `/media` | Authenticated, privacy-conscious media gallery and sync controls. |
| `/media-library` | Upload, classify, correct, enable, preview, and delete shared exchange images. |
| `/performance` | Mature outcome trends, strategy/subreddit results, and model usage. |
| `/operations` | Background jobs, sync state, events, rate limits, and technical logs. |
| `/settings` | Validated runtime configuration and account/session management. |
@@ -58,6 +59,8 @@ The login form exchanges the administrator token for an HttpOnly, SameSite=Stric
Media may be private conversation content. Preserve authenticated URLs, blur/privacy defaults, explicit reveal behavior, safe new-tab links, and no token leakage into query strings or local storage.
Exchange-library uploads require an explicit consent checkbox before the raw image request is enabled. Previews remain blurred by default. The management card exposes AI tags, safety level, confidence/review state, enablement, and a two-step delete action. Its progression rail mirrors the actual nonspecific-request selection tiers rather than implying that explicit anatomy requests follow the same sequence.
## Feature structure
Each route is a slice under `features/<name>/` with page/route components, local styles, and feature-specific helpers. Cross-feature visual/domain components live under `components/`; application lifecycle belongs under `app/`; transport belongs under `api/`.
+11 -1
View File
@@ -47,6 +47,16 @@ Each subreddit snapshots a policy into a conversation when automation first hand
The snapshot matters: changing configuration later must not silently rewrite the rules for an active conversation. Automation stops for seller/upsell evidence, an image received, a detected boundary, unsafe content, an exhausted turn budget, a repeated media request, or a manual policy. Short refusals are interpreted in sequence: phrases such as “I’m good” become media boundaries only when they answer a media request.
## Operator exchange media
The exchange library contains only operator-uploaded images. Upload requires confirmation that every depicted adult consented to storage and bot delivery. Received conversation media is account-owned evidence and can never be promoted into this shared library.
The bot may continue to ask another person for an optional photo under the configured conversation policy, but it must never mention, advertise, or offer an exchange-library image proactively. A library send requires a direct or indirect inbound image request. Exact anatomy requests match the operator-reviewed tags `penis`, `chest`, `selfie`, `topless`, or `feet`; the bot does not substitute a different anatomy when a match is unavailable.
Nonspecific requests broaden progressively within one conversation. The first selects a random unused SFW asset, a later request may include topless/NSFW assets, and further requests may select from the full library. An asset is never selected twice in the same conversation, including after an uncertain send. After Matrix confirms the image event, the bot asks the other person to reciprocate without pressure. Missing inventory, failed delivery, changed conversation state, or an unconfirmed reciprocal message becomes an operator handoff.
Reddit candidate eligibility remains the product's adult-entry assumption; this feature adds no separate verification workflow. Existing deterministic underage disclosures and conversation boundaries still stop automation before a library send.
## Success and analytics
Do not collapse all progress into “messages sent.” Useful states include:
@@ -75,6 +85,6 @@ Read-only inspection, builds, tests, and documentation work do not grant any of
## Privacy and secrets
Treat these as sensitive: `.env`, Reddit cookies, exported session JSON, Matrix access tokens, dashboard/agent tokens, the SQLite database, browser profiles, conversation text, usernames, private media, and logs that may contain request metadata.
Treat these as sensitive: `.env`, Reddit cookies, exported session JSON, Matrix access tokens, dashboard/agent tokens, the SQLite database, browser profiles, conversation text, usernames, received private media, operator exchange media, and logs that may contain request metadata.
Never commit or reproduce them in documentation, fixtures, logs, issue text, or tool output. Prefer redacted structural evidence. Production backups contain account and private conversation data and need the same protection as the live volume.
+3
View File
@@ -44,6 +44,9 @@ This is an ownership map, not an API dump. Read the owning implementation and te
| `services/response-checker.ts` | Inbox/performance response checks and successful examples | Non-overlapping; do not confuse chat sync with outcome maturity. |
| `services/media.ts` | Download, deduplicate, index, and list private media | Preserve account ownership and authenticated serving. |
| `services/media-sync.ts` | Periodic authenticated media sync | Stop while auth is unavailable and on runtime shutdown. |
| `services/media-library.ts` | Shared operator uploads, image validation/re-encoding, classification state, and private files | Require consent confirmation; received media never enters this store. |
| `services/media-library-exchange.ts` | Inbound request detection, progressive selection, reservation, and image-first exchange | Never offer proactively; exact tags do not substitute; no asset reuse per conversation. |
| `database/media-library-repository.ts` | Shared asset metadata and durable account/conversation send audit | Reserve before remote send; uncertain sends stay consumed. |
## Persistence