Please describe your situation before submitting.'; $out .= scn01_render_landing($association_slug, $access); return $out; } // Load association config from vs01 (shared source of association data) $cfg_raw = @file_get_contents('addon/vs01/config.json'); $cfg = $cfg_raw ? json_decode($cfg_raw, true) : []; $assoc = $cfg['associations'][$association_slug] ?? []; // Load scn01 config for receiver_url and node_token $scn_config = scn01_load_config(); // Load VS and DSC snapshots for this participant. // These are embedded in the Scenario envelope — the record must be self-contained. $participant_id = scn01_get_participant_id(); $vs_snapshot = scn01_load_vs_snapshot($participant_id, $association_slug); $dsc_snapshot = scn01_load_dsc_snapshot($participant_id, $association_slug); // Prerequisite gate: both VS and DSC sets must exist before a Scenario can be submitted. if (empty($vs_snapshot) || empty($dsc_snapshot)) { return '
Please complete your Vital Signs and DSC Category records first. Your Vital Signs and DSC records establish the context for any Scenario you submit. Complete Vital Signs | Complete DSC Categories
'; } $envelope = scn01_build_spool_envelope( $narrative, $pinned, $association_slug, $assoc['channel_id'] ?? '', $access, $vs_snapshot, $dsc_snapshot, $scn_config['node_token'] ?? '' ); $result = scn01_post_to_orchestrator($envelope, $scn_config); if ($result === true) { $assoc_name = scn01_h($assoc['name'] ?? $association_slug); return '
Your account for ' . $assoc_name . ' has been submitted. Records cannot be edited — if you want to add more later, submit a new record. Return to ' . $assoc_name . '
'; } $out = '
Submission failed. Please try again or contact the operator.
' . scn01_h($result) . '
'; $out .= scn01_render_landing($association_slug, $access); return $out; } // --------------------------------------------------------------------------- // SNAPSHOT LOADERS // Read the participant's current VS and DSC records from the orchestrator's // stored JSON files. These are embedded verbatim in the Scenario envelope. // --------------------------------------------------------------------------- function scn01_load_vs_snapshot($participant_id, $association_slug) { if (!$participant_id || !$association_slug) return []; // VS records are stored by the orchestrator at a known path. // On the Hubzilla node we read the stored record via the orchestrator's // GET endpoint — but since there is no GET endpoint yet, we read // from pconfig as the interim source. // When a GET /vs01/record endpoint is added to the orchestrator, // this function should be updated to call it instead. $stored = get_pconfig(local_channel(), 'vs01_snapshot', $association_slug); if ($stored) { $data = json_decode($stored, true); if (json_last_error() === JSON_ERROR_NONE) return $data; } return []; } function scn01_load_dsc_snapshot($participant_id, $association_slug) { if (!$participant_id || !$association_slug) return []; $stored = get_pconfig(local_channel(), 'dsc01_snapshot', $association_slug); if ($stored) { $data = json_decode($stored, true); if (json_last_error() === JSON_ERROR_NONE) return $data; } return []; } // --------------------------------------------------------------------------- // PARTICIPANT ID // --------------------------------------------------------------------------- function scn01_get_participant_id() { $observer = get_observer_hash(); if (!$observer) return ''; $r = q("SELECT xchan_addr FROM xchan WHERE xchan_hash = '%s' LIMIT 1", dbesc($observer) ); if (!$r) return ''; $addr = $r[0]['xchan_addr'] ?? ''; $parts = explode('@', $addr); // Strip guest: prefix — Placekey is the channel name without the guest: qualifier. return preg_replace('/^guest:/', '', $parts[0] ?? ''); } // --------------------------------------------------------------------------- // SPOOL ENVELOPE // Matches contracts/scn01/record-v1.json — _header / _payload / _credentials. // --------------------------------------------------------------------------- function scn01_build_spool_envelope($narrative, $pinned_scenario_ids, $association_slug, $channel_id, $standing, $vs_snapshot, $dsc_snapshot, $node_token) { return [ '_header' => [ 'addon' => 'scn01', 'contract_version' => '1.0', 'association_slug' => $association_slug, 'association_channel_id' => (string) $channel_id, 'participant_id' => scn01_get_participant_id(), 'submitted_at' => date('c'), 'standing' => $standing, 'node_token_hash' => hash('sha256', $node_token), ], '_payload' => [ 'pinned_scenario_ids' => $pinned_scenario_ids, 'narrative' => $narrative, 'vs_snapshot' => $vs_snapshot, 'dsc_snapshot' => $dsc_snapshot, ], '_credentials' => [ 'g1_tx_hash' => null, // populated when Ğ1 payment is wired ], ]; } // --------------------------------------------------------------------------- // ORCHESTRATOR POST // --------------------------------------------------------------------------- function scn01_post_to_orchestrator($envelope, $config = null) { if ($config === null) { $config = scn01_load_config(); } $receiver_url = $config['receiver_url'] ?? ''; $node_token = $config['node_token'] ?? ''; if (!$receiver_url) { logger('scn01_spool: receiver_url not configured'); return 'Orchestrator receiver URL not configured.'; } $payload = json_encode($envelope); if ($payload === false) { logger('scn01_spool: failed to encode envelope'); return 'Failed to encode submission.'; } $ch = curl_init($receiver_url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Node-Token: ' . $node_token, ], ]); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); curl_close($ch); if ($curl_error) { logger('scn01_spool: curl error: ' . $curl_error); return 'Network error: ' . $curl_error; } if ($http_code === 200 || $http_code === 201) { return true; } logger('scn01_spool: orchestrator returned HTTP ' . $http_code . ': ' . $response); return 'Orchestrator error (HTTP ' . $http_code . ').'; } // --------------------------------------------------------------------------- // CONFIG // --------------------------------------------------------------------------- function scn01_load_config() { $path = 'addon/scn01/config.json'; $raw = @file_get_contents($path); if ($raw === false) return []; $data = json_decode($raw, true); return (json_last_error() === JSON_ERROR_NONE) ? $data : []; }