true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_HTTPHEADER => ["Content-Type: application/json", ...$headers], CURLOPT_POSTFIELDS => json_encode(["user" => (int)$me["id"], "title" => "The zoo says hello", "body" => "Exhibit 16 worked. Tap to go back to the zoo.", "url" => rc_app_url("zoo") . "/#e16"])]); $out = (string)curl_exec($c); $s = (int)curl_getinfo($c, CURLINFO_HTTP_CODE); $r = json_decode($out, true); return $s === 200 && isset($r["sent"]) ? $r : ["error" => "push answered $s: " . substr($out, 0, 160), "status" => 502]; } function ping_state(int $uid, ?array $set = null): ?array { if ($set !== null) meta_set("ping16.$uid", json_encode($set + ["at" => time()])); return json_decode((string)meta("ping16.$uid"), true) ?: null; } return [ "n" => 16, "try" => "get a notification", "wing" => "Notifications", "kind" => "human", "title" => "Ping me", "promise" => "An app can send a notification to your phone or browser.", "block" => 'The push app keeps each person\'s devices (Web Push); an app asks it to notify someone with rc_app_headers("push", true).', "show" => function (?array $me): string { if (!$me) return '

Sign in to be notified.

'; return '

' . '

The first time, push asks you to allow the zoo on this browser.

' . seen_button(16, $me, "The notification arrived and opened the zoo: this worked for me"); }, "api" => function (string $do, ?array $me, array $in, bool $post): ?array { if (!$me) return ["error" => "sign in first", "status" => 401]; if ($do === "status") return ["state" => ping_state((int)$me["id"])]; if (!$post) return null; if ($do === "send") return ping_send($me, rc_app_headers("push", true)); if ($do !== "notify") return null; $headers = rc_app_headers("push", true); // taken now: it names this person and lives two minutes $uid = (int)$me["id"]; ping_state($uid, ["state" => "waiting"]); register_shutdown_function(function () use ($me, $headers, $uid) { if (function_exists("fastcgi_finish_request")) fastcgi_finish_request(); // the browser has its answer set_time_limit(40); sleep(10); $r = ping_send($me, $headers); ping_state($uid, isset($r["error"]) ? ["state" => "error", "error" => $r["error"]] : ["state" => $r["sent"] > 0 ? "sent" : "nobody", "sent" => $r["sent"], "signed_out" => $r["signed_out"] ?? 0]); }); return ["scheduled" => true, "in" => 10]; }, // The send path, as the robot: push must accept the assertion and answer with its counts. // It has no browser, so sent is expected to be 0. "check" => function (): array { require_once ZOO_ROOT . "/lib/robot.php"; [$code, , $j] = robot_at_zoo()->post("https://" . env("RC_HOST") . "/api/16/send"); if ($code !== 200 || !is_array($j)) return [false, "push's send failed ($code): " . (is_array($j) ? ($j["error"] ?? "") : substr((string)$j, 0, 120))]; return [true, "push accepted the zoo's send for the robot: sent {$j['sent']}, gone {$j['gone']}, signed out {$j['signed_out']}, failed {$j['failed']}", $j]; }, "script" => <<<'JS' (() => { const btn = document.getElementById("ping16"); if (!btn) return; const out = document.getElementById("ping16-out"); const KEY = "zoo-push-allowed"; const get = () => { try { return localStorage.getItem(KEY); } catch (e) { return null; } }; const put = (v) => { try { v ? localStorage.setItem(KEY, "1") : localStorage.removeItem(KEY); } catch (e) {} }; if (/[?&]pushed=1/.test(location.search)) { put(true); history.replaceState(null, "", "/#e16"); out.textContent = "allowed: press the button again"; } const push = btn.dataset.push; document.getElementById("ping16-manage").addEventListener("click", () => { location.href = push + "/"; }); const go = () => { location.href = push + "/?enable=1&back=" + encodeURIComponent(location.origin + "/?pushed=1#e16"); }; btn.addEventListener("click", async () => { if (!get()) { out.textContent = "taking you to push to allow the zoo…"; go(); return; } btn.disabled = true; out.textContent = "scheduled: ten seconds"; const j = await zoo.call("/api/16/notify", {}); if (j.error) { out.textContent = j.error; btn.disabled = false; return; } for (let i = 0; i < 20; i++) { await new Promise(r => setTimeout(r, 1500)); const s = (await zoo.call("/api/16/status")).state; if (!s || s.state === "waiting") continue; btn.disabled = false; if (s.state === "sent") out.textContent = "sent to " + s.sent + " browser(s): look for it"; else if (s.state === "nobody") { put(false); out.innerHTML = 'Nothing was sent: the zoo is not allowed on any of your browsers yet. '; document.getElementById("ping16-allow").onclick = go; } else out.textContent = s.error || "failed"; return; } btn.disabled = false; out.textContent = "no answer yet"; }); })(); JS, ];