openvibers/openvibe.fun README.md

README.md

main 15.8 KB · 244 lines · markdown

1# OpenVibe.Fun
2
3> Something to do, alone, together, or live.
4
5**Live site:** https://openvibe.fun · **API docs:** https://openvibe.services · **For AI:** https://openvibe.fun/llms.txt
6**Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test`
7
8**Status:** v1 written and tested (see [STATUS.json](STATUS.json)).
9**Domain:** `openvibe.fun` · **Port:** 5140 · **Service id:** `fun` · **Env prefix:** `FUN`
10**License:** AGPL-3.0-only (the same as every OpenVibe service).
11
12OpenVibe.Fun is the Fun room. V1 has three things:
13
14- **IRL quests on Live.** A streamer opens a board for their public Live channel. Viewers suggest safe real-world
15 challenges, chat votes with `!quest vote <slug>`, the streamer accepts one on stream, marks it live and completed,
16 and the chat or a moderator verifies it. A badge is paid locally; OpenCoins are loyalty points, never money, and
17 only when the wallet is configured.
18- **The local guide.** "What's fun near me?" — places from OpenStreetMap (Nominatim + Overpass), local events from
19 Ticketmaster only when a key is configured, a tile-free map, times, links and an accessibility mark. It is
20 deterministic in v1: no model call, no new AI capability, no paid provider.
21- **Group plans.** Two or three options, one link, one vote each, a tie refused, the owner decides, and the winner
22 can be added to the viewer's own Today with the viewer's own Network token.
23
24**No social posting.** Fun reads OpenVibe.Chat's public history for votes and reads OpenVibe.Live's public channel
25for the streamer/live state. It never posts a chat message, a stream announcement or a social entry. The one
26external write is a plan added to *your own* Today.
27
28## Owns
29
30| Table | Holds | Migration |
31|---|---|---|
32| `fun_quest_boards` | `qbd_` + ULID, streamer subject (nullable after deletion), public Live/Chat username, title, city/point, accessible-only, max suggestions, status, accepted quest, window | `migrations/0001_initial.sql` |
33| `fun_quests` | `qst_` + ULID, board, unique slug (`/q/<slug>`), title, brief, category, source, status `suggested\|accepted\|live\|completed\|verified\|rejected\|hidden`, visibility, place, accessible, streamer, revision, reward badge/coins, timestamps | `0001_initial.sql` |
34| `fun_quest_votes` | one (`quest`, voter) row; voter is `usr_…` or `chat:<username>`; via web/chat, chat message id | `0001_initial.sql` |
35| `fun_quest_checks` | one verification check per voter; chat or moderator, chat message id | `0001_initial.sql` |
36| `fun_badges` | one local badge per (`quest`, subject) after verification | `0001_initial.sql` |
37| `fun_quest_rewards` | `qrw_` + ULID, the OpenCoin ledger per quest/streamer: pending/credited/skipped/failed, attempts | `0001_initial.sql` |
38| `fun_plans` | `pln_` + ULID, slug (`/plans/<slug>`), owner (nullable after deletion), title, city/point, timezone, start/close, status `voting\|decided\|cancelled`, winner, visibility, revision | `0001_initial.sql` |
39| `fun_plan_options` | `pop_` + ULID, plan, title/details/place/point, accessible, price note, position 1–10 | `0001_initial.sql` |
40| `fun_plan_votes` | one (`plan`, voter) row; option, via web/chat, chat message id | `0001_initial.sql` |
41| `fun_reports` | `frp_` + ULID, quest/plan target, reporter, kind `safety\|spam\|other`, reason, status | `0001_initial.sql` |
42| `fun_chat_seen`, `fun_chat_cursors` | the read-only chat page's idempotency and cursor | `0001_initial.sql` |
43| `fun_geo_cache` | public OSM/Ticketmaster JSON, fetched/expires (never a person, never a secret) | `0001_initial.sql` |
44| `fun_outbox` | Search Fabric documents/tombstones (Today/Meme shape) | `0001_initial.sql` |
45| `account_data_events` | `openvibe-sdk/account-data` export/deletion receipts | `0001_initial.sql` |
46| `telemetry_samples` | aggregated request telemetry, pruned after 14 days | `0001_initial.sql` |
47
48## Does not own
49
50- **Live, Chat and Today** — OpenVibe.Live, OpenVibe.Chat and OpenVibe.Today. Fun uses their public reads and, for
51 Today, the person's own token on the public event API. It never posts to Live or Chat and never writes to anyone
52 else's calendar.
53- **Identity and roles** — OpenVibe.Network. Fun is OAuth client `fun` (PKCE S256) and verifies session tokens
54 against the Network JWKS; staff means Network role `global_mod` or above.
55- **OpenStreetMap and Ticketmaster** — external public sources, cached and credited; never a paid provider.
56- **Search** — OpenVibe.Search receives `search.index-document@1` documents from `fun_outbox`:
57 `openvibe-publishing/search-relay` delivers them to its owner API with Fun's `search.document.write` grant
58 (`FUN_SEARCH_URL`; unset, they wait).
59- **Events, the host and the database** — OpenVibe.Events, OpenVibe.Host.
60
61## Capabilities and grants
62
63**Inbound, v1: none.** No Fun route requires a service, app or agent capability. A person is the `fun_at` session
64cookie or a Network user token; a signed-in write from the cookie must come from `https://openvibe.fun`
65(`fun.cross_site`).
66
67**Outbound, v1:**
68
69- `events.subscription.manage` — at boot, with `svc:fun`, to create the two account export/deletion subscriptions at
70 OpenVibe.Events. The delivery itself is verified by the `FUN_EVENTS_SECRET` signature, not a token.
71- `network.account.export.contribute` and `network.account.deletion.confirm` — the `createNetworkSender` calls that
72 push the person's export part and the deletion confirmation to OpenVibe.Network's internal routes.
73- `identity.subject.resolve` and `network.coins.credit` — **only when `FUN_COINS=on`**, with `svc:fun`, app id `fun`,
74 to credit OpenCoins through OpenVibe.Network after a quest is verified. Off by default; the badge is still paid.
75
76**Not needed in v1:** no Live, Chat, Today, Quest, Media or AI service token. The Today write uses the signed-in
77person's own Network token, and the guide reads public OSM/Ticketmaster endpoints. The Search relay uses
78`search.document.write` (held); a future streamer-ownership proof could use a Live capability, but v1 checks the Network
79username against the public Live channel.
80
81## API
82
83Everything is JSON under `/api/v1`; errors are `application/problem+json` with a `code`. A person is the `fun_at`
84session cookie or `Authorization: Bearer <Network user token>`, verified offline with the Network JWKS. A write
85made with the session cookie must come from `https://openvibe.fun` (403 `fun.cross_site`). A signed-in account also
86gets per-actor minute/hour limits (`FUN_LIMITS_MINUTE`, `FUN_LIMITS_HOUR`).
87
88| Route | What it does |
89|---|---|
90| `GET /guide?q=&kind=&accessible=&rainy=&kids=&cheap=&radius=&limit=` | OSM places with attribution/map links, plus Ticketmaster events only when configured; cached |
91| `GET /quests?limit=&cursor=&status=&city=` | public IRL quests, newest first, opaque keyset cursor |
92| `GET /quests/ideas` | the safe starter deck (curated in code, not model output) |
93| `GET /quests/boards/:id` | one board and its suggestions (the share link; noindex) |
94| `GET /quests/:slug` | one quest, its vote count, chat-check count and the viewer's vote |
95| `POST /quests/boards` | a streamer opens a board; the Network username must match the public Live channel |
96| `POST /quests/boards/:id/suggest` | a signed-in suggestion, safety-filtered before it is stored |
97| `POST /quests/boards/:id/idea` | owner/staff adds one unused safe idea from the deck |
98| `POST /quests/:slug/vote` | one vote per person; a repeat is idempotent |
99| `POST /quests/:slug/accept\|live\|complete\|verify` | the streamer (or staff) moves the quest on; `verify` takes `{ via: "chat"\|"moderator" }` |
100| `POST /quests/:slug/hide` | staff hides a public quest and writes a Search tombstone |
101| `POST /quests/:slug/report` | signed-in safety/spam/other report |
102| `GET /plans?limit=&cursor=&city=` | public group plans |
103| `GET /plans/:slug` | one plan with options, vote counts and the winner |
104| `POST /plans` | start a plan: title, start, timezone, 1–10 options |
105| `POST /plans/:slug/options\|vote\|decide\|today` | add an option, cast one vote, choose the winner (tie refused), add the winner to your own Today |
106| `POST /plans/:slug/hide` | staff hides a plan and writes a Search tombstone |
107| `POST /plans/:slug/report` | signed-in report |
108| `GET /reports` | staff: open reports |
109
110Validation refusals are `400 { code: "fun.invalid" }` (or `fun.quest.invalid`, `fun.plan.invalid`,
111`fun.report.invalid`). Other codes: `fun.sign_in_required`, `fun.person_required`, `fun.staff_required`,
112`fun.streamer_required`, `fun.streamer_username`, `fun.cross_site`, `fun.not_found`, `fun.quest_not_found`,
113`fun.plan_not_found`, `fun.board_not_found`, `fun.board_closed`, `fun.board_full`, `fun.quest_closed`,
114`fun.quest_state`, `fun.quest_quorum`, `fun.plan_owner`, `fun.plan_closed`, `fun.plan_tie`,
115`fun.plan_no_votes`, `fun.plan_not_decided`, `fun.rate_limited`, `fun.guide.invalid`, `fun.guide.not_found`,
116`fun.guide.upstream`, `fun.channel_not_found`, `fun.live.unavailable`, `fun.today.*`.
117
118## IRL quests and safety
119
120The state machine is `suggested → accepted → live → completed → verified`, with `rejected` for the suggestions the
121streamer did not choose and `hidden` for a staff takedown. Before any suggestion is stored, `server/quests/safety.js`
122checks it against a small, explainable list of dangerous, illegal, trespassing, harassment and adult patterns. The
123plan's own playful example ("get a stranger to beatbox") is allowed; "climb a bridge and jump off" is not. The
124streamer can make a board accessible-only and cap the number of suggestions. The public page carries a report form;
125staff see the open reports and can hide the quest.
126
127Chat voting is read-only. A timer reads one page of the public `GET /api/chat/global/history?username=<channel>` and
128applies at most one command per message id (`fun_chat_seen`):
129
130- `!quest vote <slug>`
131- `!quest suggest <text>` (safety-filtered; stored unlisted)
132- `!quest verify <slug>` (after the streamer marks it completed)
133- `!plan vote <slug> <number>`
134
135No message text beyond a bounded command, no account and no token is stored; a chat voter is a one-way key of the public username, not a copy of it. A re-read of the same public page
136changes nothing. Under `FUN_DRILL` the timer is not started; tests call `runOnce()`.
137
138Verification by `via: "chat"` needs `FUN_QUEST_VERIFY_VOTES` distinct chat checks (default 3); a Network moderator
139can verify directly with `via: "moderator"`. Verification always writes a local `fun_badges` row; it writes a
140`fun_quest_rewards` row for OpenCoins and settles it through the optional wallet adapter.
141
142## The local guide
143
144`server/guide/upstream.js` is the only outbound host list: Nominatim, Overpass and Ticketmaster. It follows their
145usage policies:
146
147- **Identify:** every request carries `FUN_OSM_USER_AGENT` (an `OpenVibeFun/0.1 (+https://openvibe.fun; …)` contact
148 string). A browser User-Agent is never used.
149- **Pace:** `createPacer` serialises calls per host, at least `FUN_OSM_MIN_INTERVAL_MS` (1000 ms) apart; Ticketmaster
150 has its own interval.
151- **Cache:** every answer is in `fun_geo_cache` for `FUN_GEO_CACHE_TTL_MS` (7 days OSM, 15 minutes Ticketmaster); the
152 last answer is served stale if the host is down.
153- **Bound:** one `around:<radius>,<lat>,<lon>` query with a small set of tags, `out center tags`, never a bulk
154 download. The radius is capped by `FUN_GUIDE_MAX_RADIUS_KM`.
155- **Credit:** every API answer and page shows © OpenStreetMap contributors, ODbL 1.0, and links back.
156
157Ticketmaster is **off by default**. It is read only when `TICKETMASTER_API_KEY` is set; the key is a query parameter
158at the edge, is never in the cache key, is never returned and is never logged. Events show name, venue, local times
159and a public link; Fun never sells a ticket, takes a payment or calls a paid provider.
160
161The guide is deterministic: `kind` (`all`, `outside`, `inside`, `arts`, `food`) plus `accessible`, `rainy`, `kids`
162and `cheap` filter the same OSM answer. There is no model call and no new AI capability.
163
164## Group plans and Today
165
166A plan has 1–10 options and one vote per person (or public chat username); a vote can change until `closes_at`.
167The owner or staff decides; a tie is `409 fun.plan_tie`. A decided plan's winner can be added to the viewer's own
168Today: `POST /plans/:slug/today` calls Today's public `POST /api/v1/events` with the viewer's Network token and
169`Idempotency-Key: fun-plan-<plan id>`. Fun never fans out to other people's calendars; each person adds it to
170their own Today. A hidden plan is 404 to the public.
171
172## Pages
173
174All server-rendered through the OpenVibe Frame (`openvibe-shared/shell`), readable without JavaScript, with
175semantic HTML and stable `fu-` ids/classes. Public pages are `public, max-age=60` with `Vary: Cookie`; a page drawn
176for a signed-in person is `private, no-store`; a board share page is `no-store`.
177
178| Page | What it is |
179|---|---|
180| `/` | the room, featured quests, plans, the guide teaser |
181| `/guide` | the guide form and its OSM/Ticketmaster answer |
182| `/quests` | public IRL quests and the safe idea deck |
183| `/quests/new` | open a board (streamer) |
184| `/quests/boards/:id` | one board, suggestions, vote and accept forms |
185| `/q/:slug` | one quest, votes/checks, streamer actions, report form |
186| `/live` | a streamer's boards and quests |
187| `/plans`, `/plans/new` | public plans and the form |
188| `/plans/:slug` | options, votes, decide, add to Today |
189| `/developers` | the API and Search document |
190| `/overlay/q/:slug` | a minimal OBS browser source, meta-refresh, noindex |
191| 404 | the not-found page |
192
193`public/css/fun.css` and `public/js/fun.js` are one-comment placeholder files: the pages are complete without them
194and the main session designs them. `robots.txt`, `sitemap.xml`, `llms.txt` and `llms-full.txt` list the public pages
195and every public quest/plan.
196
197## Search
198
199Each public quest is one `search.index-document@1` (owner `fun`, type `quest`, page `/q/<slug>`); each public plan is
200type `plan` (`/plans/<slug>`). The document carries the public title/brief/body, facets (`status`, `category`,
201`city`, `accessible`, `streamer`, `source`), human authorship and a Live channel provenance reference for a quest.
202Publishing, accepting, moving or deciding writes/updates the document in the same transaction; hiding or
203cancelling writes a tombstone. Rows live in `fun_outbox`; the relay is deliberately not in v1 and needs the
204`search.document.write` grant.
205
206## The first proof (covered by the tests)
207
208`test/chat-sync.test.js` and `test/quests.test.js` cover the plan's first proof end to end: a chat message suggests
209a quest, a public chat vote counts, the streamer accepts it on their board, marks it live and completed, three
210distinct chat checks (or a moderator) verify it, and a badge is paid. `test/plans.test.js` covers the second proof:
211a group votes, the winner is decided and goes to the viewer's own Today.
212
213## Run and test
214
215```sh
216npm install
217npm test # PGlite; every test file in its own process, no network needed
218npm run test:pg # the same through PostgreSQL + PgBouncer (openvibe-sdk scripts/test-services.sh up)
219OV_ALLOW_SUITE=1 npm test
220npm start # PORT 5140, loopback; DATABASE_URL or an embedded PGlite in development
221```
222
223The tests boot the app with a mock Network, a Live/Chat/Today/OSM/Ticketmaster fixture server, and a temporary
224database: the migration, the guide's cache/pacer/User-Agent and Ticketmaster off-by-default, quest safety and the
225full state machine, chat suggestion/vote/verify idempotency, plan voting/tie/Today, Search document validation,
226SSO, pages/cache, coins opt-in, and account export/deletion.
227
228## Deploy
229
230`deploy/systemd/openvibe-fun.service` (127.0.0.1:5140, `/etc/openvibe/fun.env`) and
231`deploy/nginx/openvibe.fun.conf` (TLS, www → apex, `/auth/me` on the API zone, a forms zone for the POST routes,
232`/internal` and `/metrics` 404). Not deployed by this change.
233
234## License
235
236AGPL-3.0-only. See [LICENSE](LICENSE).
237
238<!-- versions:start -->
239- openvibe-contracts: v0.419.0
240- openvibe-sdk: v0.161.0
241- openvibe-shared: v4.241.0
242- openvibe-publishing: v1.7.0
243<!-- versions:end -->
244