Metadata-Version: 2.4
Name: pretix-fc-access
Version: 0.1.0
Summary: The Festival Crowd — rotating (screenshot-proof) ticket QR + reason-rich gate validation for pretix
Author: The Festival Crowd
License: AGPL-3.0-or-later
Description-Content-Type: text/markdown
Requires-Dist: qrcode
Dynamic: author
Dynamic: description
Dynamic: description-content-type
Dynamic: license
Dynamic: requires-dist
Dynamic: summary

# pretix-fc-access — The Festival Crowd rotating access

A pretix plugin that adds **screenshot-resistant rotating ticket QR codes** with
**online, reason-rich gate validation** — while reusing pretix's own check-in
engine so you keep dedupe and every existing rejection reason for free.

This is graft #1 of the Festival Crowd platform (Pretix core + Square payments +
rotating-QR scanner + Hi.Events UI + external-ticket import).

## Exclusive ticket transfer

Each pass link is keyed to that **ticket's own** `web_secret`, so a link *is* the
ticket — safe to share one without exposing the rest of the order. The pass has a
**"Give this ticket to a friend"** action: it reassigns the holder, reissues the
scan secret **and** the web_secret, and emails the recipient their new pass link.
The sender's QR, link and any screenshot **stop working immediately** — a real
hand-off, no double-use. (Ticketmaster-style transfer.)

## Email placeholder: `{pass_link}`

Add `{pass_link}` to an event's *order placed* email (Settings → Email) and it
renders each buyer's own live-pass URL — the "email a link, barcode unlocks 3
days before" flow. Static events simply omit it. See `docs/event-playbook.md`.

## Turning the rotating QR on / off

**Ticket pass → Anti-screenshot mode** is the on/off switch for the random QR on
mobile tickets, per event and per ticket type (blank per-item = inherit):

- **On** — `rotate` (default) or `totp` (below).
- **Off** — `static`: the pass shows a plain, standard pretix QR (screenshottable,
  like a normal e-ticket). No rotation, no revocation, no liveness UI.

## Two anti-screenshot modes (when on)

Set it under **Ticket pass → Anti-screenshot mode**:

- **`rotate` — rotate-and-revoke (default, NO app fork).** On a cadence (default
  every 2 min) and lazily whenever the live pass fetches its QR, the plugin calls
  pretix's own `assign_ticket_secret(..., force_invalidate=True)` to regenerate
  the ticket's **real** pretix secret; that primitive appends the previous secret
  to `RevokedTicketSecret` when the event's generator uses a revocation list. The
  QR encodes the current secret **raw**, so the **stock pretixSCAN app reads it
  natively** (full dedupe + every reason). A screenshot holds a rolled-out code:
  with the default random generator it no longer resolves, and with the signed
  generator it is explicitly revoked and synced to offline scanners (via the
  [venue box](../../deploy/venue-box/)). Rotation is lazy plus a run-up-to-doors
  backstop sweep, so it stays cheap at scale.
- **`totp` — rolling code (needs the forked scanner).** The QR is a TOTP-style
  code that **refreshes every 20 seconds**; the gate scanner unwraps it online
  and, on a stale code, returns **"EXPIRED CODE — likely a screenshot"**, then
  hands the canonical pretix secret to pretix's normal `redeem`. Use this when
  you also want OS-level screenshot *blocking* from the native fork.

Either way: **reason-rich validation** (already used with time + gate, unknown
ticket, not paid, wrong type, wrong time, blocked, revoked) comes from pretix's
own check-in engine, and **offline lanes still work** — the ticket keeps a
scannable secret at all times.

Plus on the pass: **barcode-delay** (QR hidden until N days before, ticket still
transferable), gate/venue/accessibility/bag links, order details, Add-to-Wallet,
a sellable **sponsor slot**, event **logo**, and a **face-value resale** label.

> **Operational note for `rotate` mode:** because it rolls the ticket's real
> secret, any *previously issued static QR* for that position (an emailed PDF or
> a wallet pass minted before a roll) stops scanning — the **live pass becomes
> the ticket**. That's the point (it's what kills screenshots), and it pairs with
> the barcode-delay so the code only ever exists on the live screen. If you need
> static PDF tickets to keep working, use `totp` mode or leave the plugin off for
> that event.

## Why rotating + offline can't be the same scan

A rotating code can only be validated online (the server checks the live window).
Offline scanning needs a static code. So: **rotating QR at connected gates**
(anti-screenshot) + **static-secret pretixSCAN at offline lanes** (dedupe). Each
ticket carries both; you pick the scanner per lane. See the platform build doc.

## Endpoints

Attendee (event-scoped, authenticated by the order secret in the URL):

    /<org>/<event>/fcpass/<order>/<position>/<order_secret>/          # live pass page
    /<org>/<event>/fcpass/<order>/<position>/<order_secret>/qr.svg    # current QR (SVG)
    /<org>/<event>/fcpass/<order>/<position>/<order_secret>/state.json# countdown sync

Gate (device- or team-token authenticated), for the forked scanner:

    POST /api/v1/organizers/<org>/events/<event>/fc/unwrap/
    body: {"barcode": "FCX1:ABCDE:42:26006966"}
    -> {"ok": true,  "secret": "<pretix secret>"}                     # then call pretix redeem
    -> {"ok": false, "reason": "expired",  "message": "EXPIRED CODE — likely a screenshot"}
    -> {"ok": false, "reason": "invalid",  "message": "Unknown ticket"}

The scanner flow: scan → `POST …/fc/unwrap/` → if `ok`, call the standard
pretix `POST …/checkinlists/<id>/redeem/` with the returned `secret` and show its
rich result; if not `ok`, show the returned message.

## Install (on the pretix instance)

```bash
# in the pretix python environment
pip install /path/to/pretix-fc-access      # or: pip install -e .
python -m pretix migrate
python -m pretix rebuild
# restart the pretix web + worker processes
```

For the containerised demo stack, add the package to the pretix image's
requirements (or bind-mount it) and restart the `pretix` service, then enable
**Festival Crowd — Rotating Access** under the event's *Settings → Plugins*.

## Configuration

- Code step / digits / skew tolerance live in `crypto.py`
  (`DEFAULT_STEP=20`, `DEFAULT_DIGITS=8`, `DEFAULT_WINDOW=1`). 20s is a good
  balance of anti-screenshot strength vs. phone-clock skew.

## Tests

`crypto.py` is framework-free and unit-tested (valid window, stale-screenshot
rejection, clock-skew tolerance, barcode parse). Run:

```bash
python -m pytest tests/    # or the inline check in the build notes
```

## Status

v0.1 — crypto verified; Django/pretix integration written against pretix's
current plugin API (secret model, `order_info_top` signal, event/api url
patterns, `DeviceTokenAuthentication`). Needs a deploy-test on your pretix
instance to confirm end-to-end, then the forked pretixSCAN work (graft #2).

## Licence

AGPL-3.0-or-later (matches pretix). Internal use.
