openvibers/openvibe.quest Commits

Quest: Bring a robot online (#54)

OP OpenVibers committed 29 days ago · 799691f5ff · 4 files, +20 −3

* Quest: Bring a robot online (bot.robot.online names the owner since contracts 0.192.0)

* Quest: the robot quest's badge is Robot Pilot (Robot Wrangler is a Live setup badge)

Changes

ddiff --git a/README.md b/README.md
iindex f66c624..7ad6433 100644
---- a/README.md
++++ b/README.md
@@@ -87,7 +87,7 @@ own** and produces no events; no `/api/v1` route is guarded by one ([server/http
| Piece | Where | What it does |
|---|---|---|
--| Catalog | [server/quests/catalog.js](server/quests/catalog.js) | 35 first-party quests in code (the 18 of the Live setup series among them), one per real thing a person can do, plus the whole list of events Quest listens to (and the one it does not). A quest may not count an event the catalog does not document: that fails at load |
++| Catalog | [server/quests/catalog.js](server/quests/catalog.js) | 36 first-party quests in code (the 18 of the Live setup series among them), one per real thing a person can do, plus the whole list of events Quest listens to (and the one it does not). A quest may not count an event the catalog does not document: that fails at load |
| Events consumer | [server/events-consumer.js](server/events-consumer.js) | `POST /internal/events`, exactly as OpenVibe.Space does it: raw body, `parseDelivery(…, { requireV2: true })`, the `openvibe-sdk` inbox for once-only application, forwarding headers refused, 503 without `QUEST_EVENTS_SECRET`. It subscribes to the catalog's events at boot (idempotent, off without `QUEST_EVENTS_URL`) |
| What moves a quest | [server/quests/apply.js](server/quests/apply.js) | The person is taken from the field the event's own schema defines (`channel.subject`, `follower`, `author`, `user_subject`, `player.subject`, `owner`; the envelope actor only where the schema says the actor *is* that person). No subject, a `gst_` guest, or a service/app/mod principal → ignored, nothing written |
| Quest log | [server/quests/store.js](server/quests/store.js), [migrations/0002_quest.sql](migrations/0002_quest.sql) | Progress per step, one completion per person per quest (unique), one badge per person, and the Events receipts. Two deliveries of the same event change nothing; so does a replay of the whole request |
@@@ -133,6 +133,7 @@ OpenVibe.Events delivers an event when a site really does something. Quest count
| `deals.offer.created` / `deals.vote.changed` | OpenVibe.Deals | the envelope actor: the submitter or the voter (an imported deal's actor is `svc:deals`) |
| `run.job.succeeded` | OpenVibe.Run | `payload.requester` when it is a user (an app's or a service's job is nobody's) |
| `inventory.item.equipped` | OpenVibe.Inventory | `payload.owner` |
++| `bot.robot.online` | OpenVibe.Bot | `payload.owner`, the robot's owner (Bot names it since contracts 0.192.0) |
A step can also name payload values to `match` (`{ task: 'emote' }`): the "Set up your channel" series is one
quest per Live setup task, plus one that counts all 18 tasks. The home page shows a series as one card; its page is
@@@ -146,7 +147,7 @@ coins are recorded once; a redelivery of the same event, or the whole request ag
| **OpenVibe.Events** (`openvibe.events`) | the domain events that move the log, `POST /internal/events`, loopback only | Signed deliveries only: `X-OpenVibe-Signature-V2` (HMAC-SHA256 over `<timestamp>.<raw body>`), a 300-second replay window, `requireV2`, the secret from `QUEST_EVENTS_SECRET` (32+ characters, comma-separated for rotation). A delivery that carries a forwarding header is refused: nginx never proxies this route. Once-only application is the `openvibe-sdk` inbox, in the same transaction as the writes |
| **openvibe-contracts** v0.190.0 | the shape of every event and of the two Network calls | Every field read is the one the schema at `contracts/events/payloads/<type>.v1.json` defines; no field is inferred and nothing about a person is taken from a name or from the request |
| **OpenVibe.Network** (`openvibe.network`) | resolving a subject to its wallet id, and crediting OpenCoins | `GET /internal/identity/resolve` (capability `identity.subject.resolve`, `identity.resolve-result@1`) gives `network_user_id`; `POST /internal/coins/credit` (capability `network.coins.credit`, body `network.coins-change-request@1`, answer `network.coins-balance-result@1`) credits, with `app_id: quest` — a service token may only name its own app id. Both use this service's own client credentials over the internal URL; no token is logged |
--| **OpenVibe sites** (Live, Community, Space, Chat, Wiki, Games, Tools, Network, Blog, Deals, Run, Inventory) | nothing is fetched from them | Quest makes no outbound request to any site, and no URL from a request ever decides one: the only host it calls is `OV_NETWORK_INTERNAL_URL` |
++| **OpenVibe sites** (Live, Community, Space, Chat, Wiki, Games, Tools, Network, Blog, Deals, Run, Inventory, Bot) | nothing is fetched from them | Quest makes no outbound request to any site, and no URL from a request ever decides one: the only host it calls is `OV_NETWORK_INTERNAL_URL` |
Rewards are badges and OpenCoins. Nothing here is money, and nothing here is Vibes.
ddiff --git a/server/quests/apply.js b/server/quests/apply.js
iindex 486a84b..adb4b82 100644
---- a/server/quests/apply.js
++++ b/server/quests/apply.js
@@@ -62,6 +62,7 @@ function personOf(event) {
case 'run.job.succeeded': // payload.requester (identity.subject-ref@1: a user, service, app or mod)
return refUser(p.requester);
case 'inventory.item.equipped': // payload.owner (usr_…)
++ case 'bot.robot.online': // payload.owner (usr_…, contracts 0.192.0; absent from older events)
return isUser(p.owner) ? p.owner : null;
default:
return null;
ddiff --git a/server/quests/catalog.js b/server/quests/catalog.js
iindex 3da3f1c..5d8b92d 100644
---- a/server/quests/catalog.js
++++ b/server/quests/catalog.js
@@@ -41,6 +41,7 @@ const SITES = Object.freeze({
'openvibe.deals': { name: 'OpenVibe.Deals', what: 'deals the community shares and votes on' },
'openvibe.run': { name: 'OpenVibe.Run', what: 'jobs run on nodes' },
'inventory.openvibe.network': { name: 'OpenVibe.Inventory', what: 'the items you own across OpenVibe' },
++ 'openvibe.bot': { name: 'OpenVibe.Bot', what: 'robots you drive from the browser' },
});
/**
@@@ -204,6 +205,15 @@ const QUESTS = Object.freeze([
reward: { badge: { id: 'dressed-up', name: 'Dressed Up' }, coins: 5 },
repeatable: false,
},
++ {
++ id: 'bring-a-robot-online',
++ site: 'openvibe.bot',
++ title: 'Bring a robot online',
++ why: 'A robot is yours to drive from any browser once a device connects it: a Raspberry Pi, an ESP32, a phone.',
++ steps: [{ event_type: 'bot.robot.online', count: 1, label: 'Connect a device to one of your robots' }],
++ reward: { badge: { id: 'robot-pilot', name: 'Robot Pilot' }, coins: 20 },
++ repeatable: false,
++ },
{
id: 'settle-in',
site: 'openvibe.network',
@@@ -427,6 +437,7 @@ const EVENTS = Object.freeze([
{ event_type: 'deals.vote.changed', site: 'openvibe.deals', what: 'A vote on a deal was cast or changed; the envelope actor is the voter.' },
{ event_type: 'run.job.succeeded', site: 'openvibe.run', what: 'A job finished on a node; the payload names its requester (a service or app requester is ignored).' },
{ event_type: 'inventory.item.equipped', site: 'inventory.openvibe.network', what: 'An item was equipped; the payload names its owner, never what else they own.' },
++ { event_type: 'bot.robot.online', site: 'openvibe.bot', what: 'A device connected and serves a robot; the payload names the robot\'s owner (events from before Bot named it are ignored).' },
]);
// The events Quest actually counts: the consumer subscribes to exactly these, and a quest may not count an event
ddiff --git a/test/events-consumer.test.js b/test/events-consumer.test.js
iindex 64c5652..2aa1fd3 100644
---- a/test/events-consumer.test.js
++++ b/test/events-consumer.test.js
@@@ -175,7 +175,7 @@ const progressOf = async (t, subject, questId) => (await rows(t, 'SELECT step, c
assert.ok(quest.text.includes('<code>task</code> <code>emote</code>'), 'the quest page says which task it counts');
});
-- await check('the Blog, Deals, Run and Inventory quests: each event credits its person; a service never does', async () => {
++ await check('the Blog, Deals, Run, Inventory and Bot quests: each event credits its person; a service never does', async () => {
const done = async () => (await completions(t, sam.subject)).map((c) => c.quest_id);
// A post published by a schedule or a service is nobody's.
await deliver(t, envelope('blog.post.published', { canonical_url: 'https://openvibe.blog/@sam/hi' }, { source: 'blog', actor: { type: 'service', id: 'blog' } }));
@@@ -194,6 +194,10 @@ const progressOf = async (t, subject, questId) => (await rows(t, 'SELECT step, c
assert.ok((await done()).includes('run-a-job'));
await deliver(t, envelope('inventory.item.equipped', { instance_id: 'i', definition_id: 'd', kind: 'hat', slot: 'hat', owner: sam.subject }, { source: 'inventory' }));
assert.ok((await done()).includes('wear-an-item'));
++ await deliver(t, envelope('bot.robot.online', { robot_id: 'rob_1', device_id: 'dev_1' }, { source: 'bot' }));
++ assert.ok(!(await done()).includes('bring-a-robot-online'), 'an event that does not name the owner credits nobody');
++ await deliver(t, envelope('bot.robot.online', { robot_id: 'rob_1', device_id: 'dev_1', owner: sam.subject }, { source: 'bot' }));
++ assert.ok((await done()).includes('bring-a-robot-online'));
});
await check('an event that names nobody creditable is ignored: no subject, a guest, a service, an unknown type', async () => {