@okostream/mcp 0.18.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -15
- package/dist/server.js +1049 -135
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -70598,6 +70598,7 @@ function buildConnectionStatus(status, options2 = {}) {
|
|
|
70598
70598
|
nextSteps: nextSteps(status, options2.minimumVersion, options2.latestVersion),
|
|
70599
70599
|
...options2.minimumVersion ? { minimumVersion: options2.minimumVersion } : {},
|
|
70600
70600
|
...options2.latestVersion ? { latestVersion: options2.latestVersion } : {},
|
|
70601
|
+
...options2.detail && !connected ? { detail: options2.detail } : {},
|
|
70601
70602
|
...options2.uid ? { uid: options2.uid } : {},
|
|
70602
70603
|
...options2.scopes ? { scopes: options2.scopes } : {},
|
|
70603
70604
|
...options2.update ? { update: options2.update } : {}
|
|
@@ -70659,7 +70660,7 @@ var GUIDE_RESOURCES = [
|
|
|
70659
70660
|
"1. Call `oko_whoami` and inspect scopes.",
|
|
70660
70661
|
"2. If the workspace is unclear, call `oko_list_workspaces`.",
|
|
70661
70662
|
"3. Call `oko_create_webinar` with title/date/workspace and video URL when known.",
|
|
70662
|
-
"4. Call `oko_configure_blocks` for video, forms, CTA, banner, and Telegram settings (Telegram channels there need telegram:write plus the notification permission on the event).",
|
|
70663
|
+
"4. Call `oko_configure_blocks` for video, forms, polls, CTA, banner, and Telegram settings (Telegram channels there need telegram:write plus the notification permission on the event).",
|
|
70663
70664
|
"5. Call `oko_configure_cards` with stable card ids before uploading card images.",
|
|
70664
70665
|
"6. Upload images with `oko_upload_card_images_batch` or `oko_upload_image`, then wait for variants.",
|
|
70665
70666
|
"7. Call `oko_validate_event_setup` and fix blocking findings.",
|
|
@@ -70677,7 +70678,7 @@ var GUIDE_RESOURCES = [
|
|
|
70677
70678
|
text: [
|
|
70678
70679
|
"# OKO Platform Invariants",
|
|
70679
70680
|
"",
|
|
70680
|
-
"OKO does not host or encode video. An organizer brings a source such as YouTube or an iframe player. OKO builds the webinar experience around it: an event page, configurable blocks, cards, forms, chat, analytics, and integrations.",
|
|
70681
|
+
"OKO does not host or encode video. An organizer brings a source such as YouTube or an iframe player. OKO builds the webinar experience around it: an event page, configurable blocks, cards, forms, polls, chat, analytics, and integrations.",
|
|
70681
70682
|
"",
|
|
70682
70683
|
"- OKO events have no `draft`, `live`, or `ended` status field.",
|
|
70683
70684
|
"- Public event pages render whenever the event exists and normal access rules allow it.",
|
|
@@ -70710,15 +70711,15 @@ var GUIDE_RESOURCES = [
|
|
|
70710
70711
|
"- `events:read`: list and read events.",
|
|
70711
70712
|
"- `events:write`: create, duplicate, move, and update events.",
|
|
70712
70713
|
"- `events:delete`: trash, restore, and permanently delete already-trashed events.",
|
|
70713
|
-
"- `setup:write`: configure blocks, forms, cards, and images.",
|
|
70714
|
+
"- `setup:write`: configure blocks, forms, polls, cards, and images.",
|
|
70714
70715
|
"- `analytics:read`: read live state, sessions, reports, and chat history.",
|
|
70715
70716
|
"- `analytics:write` and `live:control`: manage analytics sessions.",
|
|
70716
|
-
"- `live:control`: change live visibility and
|
|
70717
|
+
"- `live:control`: change live visibility, refresh viewers and command polls (`oko_control_poll`).",
|
|
70717
70718
|
"- `chat:write` and `chat:moderate`: publish and moderate event chat.",
|
|
70718
70719
|
"- `telegram:write`: link channels and manage Telegram posts.",
|
|
70719
70720
|
"- `workspace:read` and `workspace:write`: workspaces, members, invites, and notifications.",
|
|
70720
70721
|
"- `leads:read`: read form submissions; deletion also requires `setup:write`.",
|
|
70721
|
-
"- `wallet:read`: read the lumen balance, grants and ledger of the organizer's own wallet (`oko_wallet`); nothing can be bought, granted or spent with it.",
|
|
70722
|
+
"- `wallet:read`: read the plan, the lumen balance, grants and ledger of the organizer's own wallet (`oko_wallet`) and the balance of the wallet that pays for a workspace (`oko_workspace_wallet`); nothing can be bought, changed, granted or spent with it.",
|
|
70722
70723
|
"",
|
|
70723
70724
|
"Payment and terms are web-only: no tool collects a password, card or bank details, or agreement to terms on anyone's behalf. Plans and lumens are bought, and terms accepted, only in the OKO web cabinet."
|
|
70724
70725
|
].join("\n")
|
|
@@ -70735,11 +70736,12 @@ var GUIDE_RESOURCES = [
|
|
|
70735
70736
|
"- `meta.eventDate`: scheduled date.",
|
|
70736
70737
|
"- `video.originalUrl` / `video.embedUrl`: raw and normalized video URLs.",
|
|
70737
70738
|
"- `forms[]`: lead forms with stable ids, fields, consent, submit, and success copy.",
|
|
70739
|
+
"- `polls[]`: polls with stable ids \u2014 a question, 2 to 10 options with their own ids, one answer or several. A separate list beside `forms[]`: writing one never touches the other.",
|
|
70738
70740
|
"- `blocks.primaryCta`, `blocks.promoBanner`, and `blocks.cards`: audience content and live visibility.",
|
|
70739
70741
|
"- `telegram.channelIds`: claimed Telegram channels for notifications.",
|
|
70740
70742
|
"- Access mirrors are server-owned and must not be written directly.",
|
|
70741
70743
|
"",
|
|
70742
|
-
"A `formId` on a CTA, banner, or card opens that event form in a popup. Use `buttonUrl` only for navigation and set exactly one target in new writes.",
|
|
70744
|
+
"A `formId` on a CTA, banner, or card opens that event form in a popup; a `pollId` opens that poll. Use `buttonUrl` only for navigation and set exactly one target in new writes.",
|
|
70743
70745
|
"",
|
|
70744
70746
|
"Do not remove unknown fields or write access mirrors. Use dedicated MCP tools instead of whole-document rewrites."
|
|
70745
70747
|
].join("\n")
|
|
@@ -116320,7 +116322,7 @@ function sanitizeScopes(scopes) {
|
|
|
116320
116322
|
}
|
|
116321
116323
|
|
|
116322
116324
|
// build/version.js
|
|
116323
|
-
var OKO_MCP_VERSION = "0.
|
|
116325
|
+
var OKO_MCP_VERSION = "0.19.0";
|
|
116324
116326
|
|
|
116325
116327
|
// build/auth.js
|
|
116326
116328
|
var state = null;
|
|
@@ -116526,14 +116528,19 @@ async function getConnectionStatus() {
|
|
|
116526
116528
|
}
|
|
116527
116529
|
const detail = error3 instanceof Error ? `${error3.name}: ${error3.message}` : String(error3);
|
|
116528
116530
|
console.error(`[oko-mcp] connection status: unexpected error \u2014 ${detail}`);
|
|
116529
|
-
return buildConnectionStatus("unavailable", {
|
|
116531
|
+
return buildConnectionStatus("unavailable", { detail: `Unexpected client-side error: ${detail}` });
|
|
116530
116532
|
}
|
|
116531
116533
|
}
|
|
116532
116534
|
function authErrorOptions(error3) {
|
|
116533
116535
|
return {
|
|
116534
116536
|
minimumVersion: error3.minimumVersion,
|
|
116535
116537
|
latestVersion: error3.latestVersion,
|
|
116536
|
-
serverMessage: error3.serverMessage
|
|
116538
|
+
serverMessage: error3.serverMessage,
|
|
116539
|
+
// 🔴 An `unavailable` error carries its cause in `message` (the transport failure, the HTTP status with the
|
|
116540
|
+
// server's note, the client-side initialisation error). Until 0.19.0 only the constant sentence of the status
|
|
116541
|
+
// went out, and a failed smoke could not say why (28.09: `oko_whoami` failed after a 200 of the exchange, the
|
|
116542
|
+
// retry passed, and the first cause was lost). The other statuses say all there is in their fixed wording.
|
|
116543
|
+
...error3.status === "unavailable" ? { detail: error3.message } : {}
|
|
116537
116544
|
};
|
|
116538
116545
|
}
|
|
116539
116546
|
function authErrorResult(error3) {
|
|
@@ -116565,6 +116572,9 @@ function sessionReportUrl(eventId, sessionId) {
|
|
|
116565
116572
|
function inviteUrl(code) {
|
|
116566
116573
|
return `${APP_ORIGIN}/invite/${code}`;
|
|
116567
116574
|
}
|
|
116575
|
+
function billingUrl() {
|
|
116576
|
+
return `${APP_ORIGIN}/app/billing`;
|
|
116577
|
+
}
|
|
116568
116578
|
function eventLinks(eventId) {
|
|
116569
116579
|
return {
|
|
116570
116580
|
url: eventUrl(eventId),
|
|
@@ -116592,7 +116602,7 @@ var canonicalTelegramChannelSchema = telegramChannelSchema.extend({
|
|
|
116592
116602
|
|
|
116593
116603
|
// build/tools/imagePipeline.js
|
|
116594
116604
|
var import_undici = __toESM(require_undici(), 1);
|
|
116595
|
-
import { randomUUID as
|
|
116605
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
116596
116606
|
import { lookup } from "node:dns/promises";
|
|
116597
116607
|
import { stat, readFile, realpath } from "node:fs/promises";
|
|
116598
116608
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -116814,6 +116824,207 @@ function detectAndNormalize(url) {
|
|
|
116814
116824
|
return { provider: "unknown", normalizedUrl: null };
|
|
116815
116825
|
}
|
|
116816
116826
|
|
|
116827
|
+
// build/tools/pollModel.js
|
|
116828
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
116829
|
+
var POLL_ID_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,63}$/;
|
|
116830
|
+
var POLL_OPTION_ID_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,31}$/;
|
|
116831
|
+
var RESERVED_IDS = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
116832
|
+
function isSafeId(id, pattern) {
|
|
116833
|
+
return typeof id === "string" && pattern.test(id) && !RESERVED_IDS.has(id);
|
|
116834
|
+
}
|
|
116835
|
+
var MAX_POLLS = 10;
|
|
116836
|
+
var MIN_POLL_OPTIONS = 2;
|
|
116837
|
+
var MAX_POLL_OPTIONS = 10;
|
|
116838
|
+
var POLL_QUESTION_MAX = 300;
|
|
116839
|
+
var POLL_OPTION_TEXT_MAX = 200;
|
|
116840
|
+
var POLL_TITLE_MAX = 200;
|
|
116841
|
+
var POLL_LABEL_MAX = 60;
|
|
116842
|
+
var POLL_THANKS_MAX = 500;
|
|
116843
|
+
var DEFAULT_POLL_CTRL = {
|
|
116844
|
+
closed: false,
|
|
116845
|
+
live: false,
|
|
116846
|
+
liveAt: null,
|
|
116847
|
+
resultsShown: false,
|
|
116848
|
+
round: 1
|
|
116849
|
+
};
|
|
116850
|
+
var POLL_ACTIONS = ["close", "open", "show_results", "hide_results", "launch", "stop", "reset"];
|
|
116851
|
+
var CONTROL_ROOM_POLL_ACTIONS = ["launch", "reset"];
|
|
116852
|
+
function isRecord(value) {
|
|
116853
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
116854
|
+
}
|
|
116855
|
+
function boundedText(value, max) {
|
|
116856
|
+
if (typeof value !== "string")
|
|
116857
|
+
return null;
|
|
116858
|
+
const trimmed = value.trim();
|
|
116859
|
+
return trimmed && trimmed.length <= max ? trimmed : null;
|
|
116860
|
+
}
|
|
116861
|
+
function parsePoll(raw) {
|
|
116862
|
+
if (!isRecord(raw))
|
|
116863
|
+
return null;
|
|
116864
|
+
const id = typeof raw.id === "string" ? raw.id : "";
|
|
116865
|
+
const question = boundedText(raw.question, POLL_QUESTION_MAX);
|
|
116866
|
+
if (!isSafeId(id, POLL_ID_PATTERN) || !question || !Array.isArray(raw.options))
|
|
116867
|
+
return null;
|
|
116868
|
+
const options2 = [];
|
|
116869
|
+
const seen = /* @__PURE__ */ new Set();
|
|
116870
|
+
for (const item of raw.options) {
|
|
116871
|
+
if (!isRecord(item))
|
|
116872
|
+
return null;
|
|
116873
|
+
const optionId = typeof item.id === "string" ? item.id : "";
|
|
116874
|
+
const text = boundedText(item.text, POLL_OPTION_TEXT_MAX);
|
|
116875
|
+
if (!isSafeId(optionId, POLL_OPTION_ID_PATTERN) || !text || seen.has(optionId))
|
|
116876
|
+
return null;
|
|
116877
|
+
seen.add(optionId);
|
|
116878
|
+
options2.push({ id: optionId, text });
|
|
116879
|
+
}
|
|
116880
|
+
if (options2.length < MIN_POLL_OPTIONS || options2.length > MAX_POLL_OPTIONS)
|
|
116881
|
+
return null;
|
|
116882
|
+
return {
|
|
116883
|
+
id,
|
|
116884
|
+
question,
|
|
116885
|
+
options: options2,
|
|
116886
|
+
multiple: raw.multiple === true,
|
|
116887
|
+
title: boundedText(raw.title, POLL_TITLE_MAX),
|
|
116888
|
+
submitLabel: boundedText(raw.submitLabel, POLL_LABEL_MAX),
|
|
116889
|
+
thanksText: boundedText(raw.thanksText, POLL_THANKS_MAX)
|
|
116890
|
+
};
|
|
116891
|
+
}
|
|
116892
|
+
function parseEventPolls(raw) {
|
|
116893
|
+
if (!Array.isArray(raw))
|
|
116894
|
+
return [];
|
|
116895
|
+
const polls = [];
|
|
116896
|
+
const ids = /* @__PURE__ */ new Set();
|
|
116897
|
+
for (const item of raw.slice(0, MAX_POLLS)) {
|
|
116898
|
+
const poll = parsePoll(item);
|
|
116899
|
+
if (poll && !ids.has(poll.id)) {
|
|
116900
|
+
ids.add(poll.id);
|
|
116901
|
+
polls.push(poll);
|
|
116902
|
+
}
|
|
116903
|
+
}
|
|
116904
|
+
return polls;
|
|
116905
|
+
}
|
|
116906
|
+
function pollIdSet(polls) {
|
|
116907
|
+
return new Set((Array.isArray(polls) ? polls : []).map((poll) => isRecord(poll) && typeof poll.id === "string" ? poll.id.trim() : "").filter(Boolean));
|
|
116908
|
+
}
|
|
116909
|
+
function readPollState(node) {
|
|
116910
|
+
const raw = isRecord(node) ? node : {};
|
|
116911
|
+
const round = typeof raw.round === "number" && Number.isInteger(raw.round) && raw.round >= 1 ? raw.round : 1;
|
|
116912
|
+
return {
|
|
116913
|
+
closed: raw.closed === true,
|
|
116914
|
+
live: raw.live === true,
|
|
116915
|
+
liveAt: typeof raw.liveAt === "number" && Number.isFinite(raw.liveAt) ? raw.liveAt : null,
|
|
116916
|
+
resultsShown: raw.resultsShown === true,
|
|
116917
|
+
round
|
|
116918
|
+
};
|
|
116919
|
+
}
|
|
116920
|
+
function readPollTally(node) {
|
|
116921
|
+
const raw = isRecord(node) ? node : {};
|
|
116922
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
116923
|
+
const options2 = {};
|
|
116924
|
+
if (isRecord(raw.options)) {
|
|
116925
|
+
for (const [optionId, value] of Object.entries(raw.options)) {
|
|
116926
|
+
if (isSafeId(optionId, POLL_OPTION_ID_PATTERN))
|
|
116927
|
+
options2[optionId] = count(value);
|
|
116928
|
+
}
|
|
116929
|
+
}
|
|
116930
|
+
return { total: count(raw.total), options: options2 };
|
|
116931
|
+
}
|
|
116932
|
+
function readSessionPolls(raw) {
|
|
116933
|
+
if (!Array.isArray(raw))
|
|
116934
|
+
return [];
|
|
116935
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
116936
|
+
const rounds = [];
|
|
116937
|
+
for (const item of raw) {
|
|
116938
|
+
if (!isRecord(item))
|
|
116939
|
+
continue;
|
|
116940
|
+
if (typeof item.pollId !== "string" || !item.pollId)
|
|
116941
|
+
continue;
|
|
116942
|
+
if (typeof item.round !== "number" || !Number.isInteger(item.round) || item.round < 1)
|
|
116943
|
+
continue;
|
|
116944
|
+
const options2 = Array.isArray(item.options) ? item.options.flatMap((option) => isRecord(option) && typeof option.id === "string" ? [{ id: option.id, text: typeof option.text === "string" ? option.text : option.id, count: count(option.count) }] : []) : [];
|
|
116945
|
+
rounds.push({
|
|
116946
|
+
pollId: item.pollId,
|
|
116947
|
+
question: typeof item.question === "string" ? item.question : "",
|
|
116948
|
+
round: item.round,
|
|
116949
|
+
multiple: item.multiple === true,
|
|
116950
|
+
options: options2,
|
|
116951
|
+
total: count(item.total)
|
|
116952
|
+
});
|
|
116953
|
+
}
|
|
116954
|
+
return rounds;
|
|
116955
|
+
}
|
|
116956
|
+
var OPTION_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
116957
|
+
function newPollId() {
|
|
116958
|
+
return `p-${randomUUID2()}`;
|
|
116959
|
+
}
|
|
116960
|
+
function newOptionId(taken = /* @__PURE__ */ new Set()) {
|
|
116961
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
116962
|
+
const chars = Array.from(randomUUID2().replace(/-/g, "").slice(0, 8), (char) => OPTION_ID_ALPHABET[parseInt(char, 16) % OPTION_ID_ALPHABET.length]);
|
|
116963
|
+
const id = `o${chars.join("")}`;
|
|
116964
|
+
if (!taken.has(id))
|
|
116965
|
+
return id;
|
|
116966
|
+
}
|
|
116967
|
+
return `o${randomUUID2().replace(/-/g, "").slice(0, 16)}`;
|
|
116968
|
+
}
|
|
116969
|
+
var orNull = (value) => value?.trim() || null;
|
|
116970
|
+
function buildPollsForWrite(inputs, ids = { poll: newPollId, option: newOptionId }) {
|
|
116971
|
+
if (inputs.length > MAX_POLLS)
|
|
116972
|
+
throw new Error(`At most ${MAX_POLLS} polls fit on a page.`);
|
|
116973
|
+
const seenPolls = /* @__PURE__ */ new Set();
|
|
116974
|
+
return inputs.map((input, index) => {
|
|
116975
|
+
const where2 = `polls[${index}]`;
|
|
116976
|
+
const id = input.id ?? ids.poll();
|
|
116977
|
+
if (!isSafeId(id, POLL_ID_PATTERN)) {
|
|
116978
|
+
throw new Error(`${where2}.id "${id}" is not a usable poll id: a letter first, then letters, digits or hyphens, up to 64 characters.`);
|
|
116979
|
+
}
|
|
116980
|
+
if (seenPolls.has(id))
|
|
116981
|
+
throw new Error(`${where2}.id "${id}" is used by another poll in this call: every poll needs its own id.`);
|
|
116982
|
+
seenPolls.add(id);
|
|
116983
|
+
const question = input.question.trim();
|
|
116984
|
+
if (!question)
|
|
116985
|
+
throw new Error(`${where2}.question is empty.`);
|
|
116986
|
+
if (question.length > POLL_QUESTION_MAX)
|
|
116987
|
+
throw new Error(`${where2}.question is longer than ${POLL_QUESTION_MAX} characters.`);
|
|
116988
|
+
const taken = new Set(input.options.flatMap((option) => option.id ? [option.id] : []));
|
|
116989
|
+
const seenOptions = /* @__PURE__ */ new Set();
|
|
116990
|
+
const options2 = input.options.map((option, optionIndex) => {
|
|
116991
|
+
const optionWhere = `${where2}.options[${optionIndex}]`;
|
|
116992
|
+
const optionId = option.id ?? ids.option(taken);
|
|
116993
|
+
taken.add(optionId);
|
|
116994
|
+
if (!isSafeId(optionId, POLL_OPTION_ID_PATTERN)) {
|
|
116995
|
+
throw new Error(`${optionWhere}.id "${optionId}" is not a usable option id: a letter first, then letters, digits or hyphens, up to 32 characters.`);
|
|
116996
|
+
}
|
|
116997
|
+
if (seenOptions.has(optionId))
|
|
116998
|
+
throw new Error(`${optionWhere}.id "${optionId}" is used by another option of the same poll.`);
|
|
116999
|
+
seenOptions.add(optionId);
|
|
117000
|
+
const text = option.text.trim();
|
|
117001
|
+
if (!text)
|
|
117002
|
+
throw new Error(`${optionWhere}.text is empty.`);
|
|
117003
|
+
if (text.length > POLL_OPTION_TEXT_MAX)
|
|
117004
|
+
throw new Error(`${optionWhere}.text is longer than ${POLL_OPTION_TEXT_MAX} characters.`);
|
|
117005
|
+
return { id: optionId, text };
|
|
117006
|
+
});
|
|
117007
|
+
if (options2.length < MIN_POLL_OPTIONS || options2.length > MAX_POLL_OPTIONS) {
|
|
117008
|
+
throw new Error(`${where2} needs ${MIN_POLL_OPTIONS} to ${MAX_POLL_OPTIONS} options, got ${options2.length}.`);
|
|
117009
|
+
}
|
|
117010
|
+
const capped = (name7, max) => {
|
|
117011
|
+
const value = orNull(input[name7]);
|
|
117012
|
+
if (value !== null && value.length > max)
|
|
117013
|
+
throw new Error(`${where2}.${name7} is longer than ${max} characters.`);
|
|
117014
|
+
return value;
|
|
117015
|
+
};
|
|
117016
|
+
return {
|
|
117017
|
+
id,
|
|
117018
|
+
question,
|
|
117019
|
+
options: options2,
|
|
117020
|
+
multiple: input.multiple === true,
|
|
117021
|
+
title: capped("title", POLL_TITLE_MAX),
|
|
117022
|
+
submitLabel: capped("submitLabel", POLL_LABEL_MAX),
|
|
117023
|
+
thanksText: capped("thanksText", POLL_THANKS_MAX)
|
|
117024
|
+
};
|
|
117025
|
+
});
|
|
117026
|
+
}
|
|
117027
|
+
|
|
116817
117028
|
// build/tools/formatting.js
|
|
116818
117029
|
var RETIRED_SESSION_FIELDS = ["nmoEvaluation"];
|
|
116819
117030
|
function cursorMeta(count, max, hasMore, nextCursor) {
|
|
@@ -116984,17 +117195,31 @@ function reportQualityCaveats(dataQuality) {
|
|
|
116984
117195
|
caveats.push("watchTimeUnmeasured");
|
|
116985
117196
|
if (marks.watchTime === "unknown_duration")
|
|
116986
117197
|
caveats.push("watchTimeUncapped");
|
|
117198
|
+
if (marks.polls === "unmeasured")
|
|
117199
|
+
caveats.push("pollsUnmeasured");
|
|
116987
117200
|
return caveats;
|
|
116988
117201
|
}
|
|
116989
117202
|
var REPORT_QUALITY_CAVEAT_TEXT = {
|
|
116990
117203
|
baselineMissing: "The session baseline was not saved: every figure below is counted from zero and includes everything the event accumulated before this broadcast.",
|
|
116991
117204
|
startTimeUnknown: "The broadcast start time was not saved: the report is stamped with the moment it ended and its duration reads zero.",
|
|
116992
117205
|
watchTimeUnmeasured: "Watch time was not measured: the engagement snapshot could not be read.",
|
|
116993
|
-
watchTimeUncapped: "Watch time is not capped by the broadcast length \u2014 it is unknown, so nothing vouches for the total, average and median times."
|
|
117206
|
+
watchTimeUncapped: "Watch time is not capped by the broadcast length \u2014 it is unknown, so nothing vouches for the total, average and median times.",
|
|
117207
|
+
pollsUnmeasured: "The votes of this session could not be counted: an empty polls section means \u201Cnot measured\u201D, not \u201Cnobody voted\u201D."
|
|
116994
117208
|
};
|
|
116995
117209
|
function sessionQualityWarnings(dataQuality) {
|
|
116996
117210
|
return reportQualityCaveats(dataQuality).map((caveat) => REPORT_QUALITY_CAVEAT_TEXT[caveat]);
|
|
116997
117211
|
}
|
|
117212
|
+
function pollRoundMarkdown(round) {
|
|
117213
|
+
const name7 = round.question || "(a poll no longer on the page)";
|
|
117214
|
+
const several = round.multiple ? ", several answers allowed" : "";
|
|
117215
|
+
const voters = `${round.total} ${round.total === 1 ? "voter" : "voters"}`;
|
|
117216
|
+
const lines = [`- ${name7} \u2014 round ${round.round}${several}: ${voters}`];
|
|
117217
|
+
for (const option of round.options) {
|
|
117218
|
+
const share = round.total > 0 ? ` (${Math.round(option.count / round.total * 1e3) / 10}%)` : "";
|
|
117219
|
+
lines.push(` - ${option.text}: ${option.count}${share}`);
|
|
117220
|
+
}
|
|
117221
|
+
return lines;
|
|
117222
|
+
}
|
|
116998
117223
|
function sessionReportMarkdown({ eventId, event, sessionId, session }) {
|
|
116999
117224
|
const lines = [
|
|
117000
117225
|
`# OKO session report`,
|
|
@@ -117024,6 +117249,12 @@ function sessionReportMarkdown({ eventId, event, sessionId, session }) {
|
|
|
117024
117249
|
markdownMetric("CTA clicks", session.ctaClicks),
|
|
117025
117250
|
markdownMetric("Cards clicks", session.cardsClicks)
|
|
117026
117251
|
];
|
|
117252
|
+
const pollRounds = readSessionPolls(session.polls);
|
|
117253
|
+
if (pollRounds.length > 0) {
|
|
117254
|
+
lines.push("", "## Polls", "Votes cast during this session. A round that ran through several sessions is split between their reports.");
|
|
117255
|
+
for (const round of pollRounds)
|
|
117256
|
+
lines.push(...pollRoundMarkdown(round));
|
|
117257
|
+
}
|
|
117027
117258
|
const warnings = sessionQualityWarnings(session.dataQuality);
|
|
117028
117259
|
if (warnings.length > 0) {
|
|
117029
117260
|
lines.push("", "## What this report did not measure");
|
|
@@ -117420,10 +117651,10 @@ async function uploadImageFromSource(eventId, kind, cardId, source) {
|
|
|
117420
117651
|
const uploaded2 = await uploadMasterImage(eventId, kind, cardId, { sourceUrl: source.url });
|
|
117421
117652
|
return { uploaded: uploaded2, sourceBytes: uploaded2.sourceBytes ?? 0, sourceVia: "server", notes: [SERVER_FETCH_NOTE] };
|
|
117422
117653
|
} catch (error3) {
|
|
117423
|
-
const
|
|
117424
|
-
if (!
|
|
117654
|
+
const refusal7 = serverFetchRefusal(error3);
|
|
117655
|
+
if (!refusal7)
|
|
117425
117656
|
throw error3;
|
|
117426
|
-
serverFailure =
|
|
117657
|
+
serverFailure = refusal7;
|
|
117427
117658
|
}
|
|
117428
117659
|
let buffer;
|
|
117429
117660
|
const fetchNotes = [];
|
|
@@ -117517,7 +117748,7 @@ function readCardsConfig(eventData) {
|
|
|
117517
117748
|
// 🔴 The UUID is generated only when it is needed: previously the
|
|
117518
117749
|
// crypto RNG was invoked for every card on every read, and the value
|
|
117519
117750
|
// was discarded (review 02.09, R3c-14)
|
|
117520
|
-
id: hasUsableId ? item.id :
|
|
117751
|
+
id: hasUsableId ? item.id : randomUUID3(),
|
|
117521
117752
|
title: safeString(item.title).trim() || null,
|
|
117522
117753
|
text: safeString(item.text).trim() || null,
|
|
117523
117754
|
buttonLabel: safeString(item.buttonLabel).trim() || null,
|
|
@@ -117527,7 +117758,8 @@ function readCardsConfig(eventData) {
|
|
|
117527
117758
|
imageVariants: readImageVariants(item.imageVariants),
|
|
117528
117759
|
imageWidth: typeof item.imageWidth === "number" ? item.imageWidth : null,
|
|
117529
117760
|
imageHeight: typeof item.imageHeight === "number" ? item.imageHeight : null,
|
|
117530
|
-
formId: safeString(item.formId).trim() || null
|
|
117761
|
+
formId: safeString(item.formId).trim() || null,
|
|
117762
|
+
pollId: safeString(item.pollId).trim() || null
|
|
117531
117763
|
};
|
|
117532
117764
|
});
|
|
117533
117765
|
const ratio = safeString(raw.cardImageRatio);
|
|
@@ -117566,6 +117798,9 @@ function eventFormIds(eventData) {
|
|
|
117566
117798
|
const rawForms = Array.isArray(eventData.forms) ? eventData.forms : [];
|
|
117567
117799
|
return new Set(rawForms.map((form) => form && typeof form === "object" && !Array.isArray(form) ? safeString(form.id).trim() : "").filter(Boolean));
|
|
117568
117800
|
}
|
|
117801
|
+
function eventPollIds(eventData) {
|
|
117802
|
+
return pollIdSet(eventData.polls);
|
|
117803
|
+
}
|
|
117569
117804
|
function cardHasImage(item) {
|
|
117570
117805
|
return Boolean(item.imageMasterUrl || item.imageUrl || item.imageVariants);
|
|
117571
117806
|
}
|
|
@@ -117576,7 +117811,8 @@ var cardItemInputSchema = external_exports.object({
|
|
|
117576
117811
|
text: external_exports.string().max(MAX_CARD_TEXT_LENGTH).nullable().optional(),
|
|
117577
117812
|
buttonLabel: external_exports.string().max(MAX_CARD_BUTTON_LABEL_LENGTH).nullable().optional(),
|
|
117578
117813
|
buttonUrl: external_exports.string().max(MAX_CARD_BUTTON_URL_LENGTH).nullable().optional(),
|
|
117579
|
-
formId: cardIdSchema.nullable().optional()
|
|
117814
|
+
formId: cardIdSchema.nullable().optional(),
|
|
117815
|
+
pollId: cardIdSchema.nullable().optional().describe("Opens this event poll (an id of oko_configure_blocks polls) instead of a link or a form. A button has one action: set at most one of buttonUrl, formId, pollId.")
|
|
117580
117816
|
}).strict();
|
|
117581
117817
|
var imageSourceSchema = external_exports.object({
|
|
117582
117818
|
path: external_exports.string().min(1).optional().describe("Local image path on the MCP client machine (inside OKO_IMAGE_ROOTS)."),
|
|
@@ -117584,13 +117820,14 @@ var imageSourceSchema = external_exports.object({
|
|
|
117584
117820
|
}).strict().refine((source) => Boolean(source.path) !== Boolean(source.url), {
|
|
117585
117821
|
message: "Provide exactly one image source: path or url."
|
|
117586
117822
|
});
|
|
117587
|
-
function buildCardsItems({ inputs, existing, formIds }) {
|
|
117823
|
+
function buildCardsItems({ inputs, existing, formIds, pollIds }) {
|
|
117588
117824
|
const existingById = new Map(existing.map((item) => [item.id, item]));
|
|
117589
117825
|
const seen = /* @__PURE__ */ new Set();
|
|
117590
117826
|
const generatedCardIds = [];
|
|
117591
117827
|
const preservedImageCardIds = [];
|
|
117828
|
+
const droppedBindings = [];
|
|
117592
117829
|
const items = inputs.map((input) => {
|
|
117593
|
-
const id = input.id ??
|
|
117830
|
+
const id = input.id ?? randomUUID3();
|
|
117594
117831
|
if (seen.has(id))
|
|
117595
117832
|
throw new Error(`Duplicate card id "${id}". Each card id must be unique.`);
|
|
117596
117833
|
seen.add(id);
|
|
@@ -117602,9 +117839,20 @@ function buildCardsItems({ inputs, existing, formIds }) {
|
|
|
117602
117839
|
if (formId && !formIds.has(formId)) {
|
|
117603
117840
|
throw new Error(`Form ${formId} not found in event.forms. Create it with oko_configure_blocks forms first, or set formId:null.`);
|
|
117604
117841
|
}
|
|
117842
|
+
const pollId = normalizeNullableText(input.pollId);
|
|
117843
|
+
if (pollId && !pollIds.has(pollId)) {
|
|
117844
|
+
throw new Error(`Poll ${pollId} not found in event.polls. Create it with oko_configure_blocks polls first, or set pollId:null.`);
|
|
117845
|
+
}
|
|
117846
|
+
if (formId && pollId) {
|
|
117847
|
+
throw new Error(`Card ${id} cannot open a form and a poll at once: a button has one action. Keep formId or pollId and set the other to null.`);
|
|
117848
|
+
}
|
|
117605
117849
|
const previous = existingById.get(id);
|
|
117606
117850
|
if (previous && cardHasImage(previous))
|
|
117607
117851
|
preservedImageCardIds.push(id);
|
|
117852
|
+
if (previous?.formId && !formId)
|
|
117853
|
+
droppedBindings.push({ cardId: id, kind: "form", targetId: previous.formId });
|
|
117854
|
+
if (previous?.pollId && !pollId)
|
|
117855
|
+
droppedBindings.push({ cardId: id, kind: "poll", targetId: previous.pollId });
|
|
117608
117856
|
return {
|
|
117609
117857
|
id,
|
|
117610
117858
|
title: normalizeNullableText(input.title),
|
|
@@ -117616,10 +117864,11 @@ function buildCardsItems({ inputs, existing, formIds }) {
|
|
|
117616
117864
|
imageVariants: previous?.imageVariants ?? null,
|
|
117617
117865
|
imageWidth: previous?.imageWidth ?? null,
|
|
117618
117866
|
imageHeight: previous?.imageHeight ?? null,
|
|
117619
|
-
formId
|
|
117867
|
+
formId,
|
|
117868
|
+
pollId
|
|
117620
117869
|
};
|
|
117621
117870
|
});
|
|
117622
|
-
return { items, generatedCardIds, preservedImageCardIds };
|
|
117871
|
+
return { items, generatedCardIds, preservedImageCardIds, droppedBindings };
|
|
117623
117872
|
}
|
|
117624
117873
|
async function deleteStoragePaths(eventId, paths) {
|
|
117625
117874
|
const uniquePaths = [...new Set(paths.filter(Boolean))];
|
|
@@ -117890,6 +118139,8 @@ var connectionStatusOutputSchema = external_exports.object({
|
|
|
117890
118139
|
status: external_exports.enum(OKO_CONNECTION_STATUSES),
|
|
117891
118140
|
errorCode: external_exports.string().nullable(),
|
|
117892
118141
|
message: external_exports.string(),
|
|
118142
|
+
/** Why the connection could not be verified (`unavailable` only): the cause, next to the stable message. */
|
|
118143
|
+
detail: external_exports.string().optional(),
|
|
117893
118144
|
product: external_exports.object({
|
|
117894
118145
|
name: external_exports.string(),
|
|
117895
118146
|
summary: external_exports.string(),
|
|
@@ -118045,8 +118296,18 @@ var notificationsMarkOutputSchema = external_exports.object({
|
|
|
118045
118296
|
markedRead: external_exports.number().int().nonnegative(),
|
|
118046
118297
|
notificationIds: external_exports.array(external_exports.string())
|
|
118047
118298
|
}).passthrough();
|
|
118299
|
+
var walletPlanSchema = external_exports.object({
|
|
118300
|
+
tier: external_exports.enum(["core", "pro", "max", "legacy"]),
|
|
118301
|
+
status: external_exports.enum(["active", "past_due", "canceled_at_period_end", "expired"]),
|
|
118302
|
+
source: external_exports.enum(["lava", "operator", "legacy-exit", "transition-b", "smoke"]).nullable(),
|
|
118303
|
+
periodEnd: external_exports.string().nullable(),
|
|
118304
|
+
graceUntil: external_exports.string().nullable()
|
|
118305
|
+
});
|
|
118048
118306
|
var walletOutputSchema = external_exports.object({
|
|
118049
118307
|
uid: external_exports.string(),
|
|
118308
|
+
billingUrl: external_exports.string().url(),
|
|
118309
|
+
plan: walletPlanSchema.nullable().optional(),
|
|
118310
|
+
planRead: external_exports.enum(["ok", "unreadable"]).optional(),
|
|
118050
118311
|
hasWallet: external_exports.boolean(),
|
|
118051
118312
|
balance: external_exports.number().int().nullable(),
|
|
118052
118313
|
unit: external_exports.literal("lumens"),
|
|
@@ -118057,6 +118318,15 @@ var walletOutputSchema = external_exports.object({
|
|
|
118057
118318
|
hints: external_exports.array(external_exports.string()),
|
|
118058
118319
|
...cursorMetaSchema
|
|
118059
118320
|
}).passthrough();
|
|
118321
|
+
var workspaceWalletOutputSchema = external_exports.object({
|
|
118322
|
+
workspaceId: external_exports.string(),
|
|
118323
|
+
hasWallet: external_exports.boolean(),
|
|
118324
|
+
available: external_exports.number().int().nullable(),
|
|
118325
|
+
unit: external_exports.literal("lumens"),
|
|
118326
|
+
payerIsCaller: external_exports.boolean(),
|
|
118327
|
+
billingUrl: external_exports.string().url(),
|
|
118328
|
+
hints: external_exports.array(external_exports.string())
|
|
118329
|
+
}).passthrough();
|
|
118060
118330
|
var FORM_FIELD_TYPES = ["text", "tel", "email", "textarea", "select", "checkbox"];
|
|
118061
118331
|
var formFieldSchema = external_exports.object({
|
|
118062
118332
|
id: external_exports.string().min(1).optional(),
|
|
@@ -118085,6 +118355,19 @@ var formSchema = external_exports.object({
|
|
|
118085
118355
|
consentRequired: external_exports.boolean().default(true),
|
|
118086
118356
|
fields: external_exports.array(formFieldSchema).max(30)
|
|
118087
118357
|
}).strict();
|
|
118358
|
+
var pollOptionSchema = external_exports.object({
|
|
118359
|
+
id: external_exports.string().regex(POLL_OPTION_ID_PATTERN, "A letter first, then letters, digits or hyphens, up to 32 characters.").optional().describe("Stable option id. Keep it when editing a poll: the votes of an option are tied to its id, so a renamed option keeps its count. Omit for a NEW option."),
|
|
118360
|
+
text: external_exports.string().min(1).max(POLL_OPTION_TEXT_MAX).describe("The option as viewers read it.")
|
|
118361
|
+
}).strict();
|
|
118362
|
+
var pollSchema = external_exports.object({
|
|
118363
|
+
id: external_exports.string().regex(POLL_ID_PATTERN, "A letter first, then letters, digits or hyphens, up to 64 characters.").optional().describe("Stable poll id; buttons bind to it (pollId). Keep it when editing. Omit for a NEW poll."),
|
|
118364
|
+
question: external_exports.string().min(1).max(POLL_QUESTION_MAX).describe("The question viewers answer."),
|
|
118365
|
+
options: external_exports.array(pollOptionSchema).min(MIN_POLL_OPTIONS).max(MAX_POLL_OPTIONS),
|
|
118366
|
+
multiple: external_exports.boolean().optional().describe("true: viewers may pick several options (checkboxes); false or omitted: exactly one."),
|
|
118367
|
+
title: external_exports.string().max(POLL_TITLE_MAX).nullable().optional().describe("Heading of the popup; null or omitted: the question is the heading."),
|
|
118368
|
+
submitLabel: external_exports.string().max(POLL_LABEL_MAX).nullable().optional().describe("Label of the vote button."),
|
|
118369
|
+
thanksText: external_exports.string().max(POLL_THANKS_MAX).nullable().optional().describe("Shown after the vote while the results are hidden.")
|
|
118370
|
+
}).strict();
|
|
118088
118371
|
var profileDisplayNameSchema = external_exports.string().max(40).nullable();
|
|
118089
118372
|
var timezoneSchema = external_exports.string().min(1).max(100).regex(/^[A-Za-z0-9_+./-]+$/, "Use an IANA timezone such as Europe/Moscow or UTC.").refine((value) => {
|
|
118090
118373
|
try {
|
|
@@ -118158,14 +118441,14 @@ var SCOPE_UNLOCKS = {
|
|
|
118158
118441
|
"setup:write": "image uploads and setup-only changes",
|
|
118159
118442
|
"analytics:read": "analytics, sessions, and reports",
|
|
118160
118443
|
"analytics:write": "starting, ending, and repairing analytics sessions",
|
|
118161
|
-
"live:control": "live block visibility, live badge, viewer refresh",
|
|
118444
|
+
"live:control": "live block visibility, live badge, viewer refresh, poll commands",
|
|
118162
118445
|
"chat:write": "sending organizer chat messages",
|
|
118163
118446
|
"chat:moderate": "chat mode, moderation, bans and mutes",
|
|
118164
118447
|
"telegram:write": "claiming channels and publishing Telegram posts",
|
|
118165
118448
|
"workspace:read": "listing workspaces, members, invites, notifications",
|
|
118166
118449
|
"workspace:write": "workspace management, invites, profile edits",
|
|
118167
118450
|
"leads:read": "reading and exporting leads",
|
|
118168
|
-
"wallet:read": "reading the lumen balance, grants and ledger of the organizer's own wallet"
|
|
118451
|
+
"wallet:read": "reading the plan, the lumen balance, grants and ledger of the organizer's own wallet, and the balance of the wallet that pays for a workspace"
|
|
118169
118452
|
};
|
|
118170
118453
|
function presetsWithScope(scope) {
|
|
118171
118454
|
return Object.entries(MCP_PRESETS).filter(([, scopes]) => scopes.includes(scope)).map(([name7]) => name7);
|
|
@@ -118409,6 +118692,7 @@ var SCOPE_WORKAROUNDS = {
|
|
|
118409
118692
|
"oko_trash_event|events:delete": { text: "This key cannot delete events. Tell the organizer which event to remove; they can do it in the OKO cabinet, or create a key with the full preset." },
|
|
118410
118693
|
"oko_delete_event_permanently|events:delete": { text: "This key cannot delete events. Tell the organizer which event to remove; they can do it in the OKO cabinet, or create a key with the full preset." },
|
|
118411
118694
|
"oko_wallet|wallet:read": { text: "A key created before wallet:read existed does not carry it, and the scopes of a key cannot be changed: the organizer creates a new key in the OKO cabinet, puts it into OKO_API_KEY of the MCP client and revokes the old one. Until then the balance and the ledger are visible in the cabinet under Billing." },
|
|
118695
|
+
"oko_workspace_wallet|wallet:read": { text: "A key created before wallet:read existed does not carry it, and the scopes of a key cannot be changed: the organizer creates a new key in the OKO cabinet, puts it into OKO_API_KEY of the MCP client and revokes the old one. Until then the balance is visible in the cabinet under Billing." },
|
|
118412
118696
|
"oko_publish_telegram_post|telegram:write": { text: "This key cannot publish to Telegram. The organizer can publish the announcement from the OKO cabinet, or create a key with the full preset." },
|
|
118413
118697
|
// 🔴 The most frequent refusal of this whole release (`ADV-A6-19`): an
|
|
118414
118698
|
// `operator` key calling oko_configure_blocks with channel ids. The advice
|
|
@@ -119155,7 +119439,7 @@ function registerPublicationTools(server2) {
|
|
|
119155
119439
|
examples: ["Call without confirm, show the organizer the bill from the preview, and call again with confirm: true after explicit consent."],
|
|
119156
119440
|
notes: [
|
|
119157
119441
|
"The wallet belongs to the workspace owner; a member publishing in a shared workspace spends the owner's lumens.",
|
|
119158
|
-
"Refusals carry errorCode and whatToDo: ENTITLEMENT_REQUIRED, INSUFFICIENT_TOKENS, EVENT_DATE_REQUIRED, EVENT_WINDOW_PASSED (the event date is more than 48 hours in the past \u2014 move it forward), PAYER_UNRESOLVED, WORKSPACE_INACTIVE (
|
|
119442
|
+
"Refusals carry errorCode and whatToDo: ENTITLEMENT_REQUIRED, INSUFFICIENT_TOKENS, EVENT_DATE_REQUIRED, EVENT_WINDOW_PASSED (the event date is more than 48 hours in the past \u2014 move it forward), PAYER_UNRESOLVED, WORKSPACE_INACTIVE (only a NEW publication into a shared workspace is closed: Core \u2014 shared workspaces are not included (Pro); Pro \u2014 one active workspace (the owner's choice, or Max); no live subscription \u2014 renew; the owner makes a workspace the active one on the OKO cabinet's Workspaces page), EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH, EVENT_DATE_LOCKED (from oko_update_event, not this tool).",
|
|
119159
119443
|
'Repeating confirm on a published page charges nothing and repairs viewer-side switches; on a page whose show is over ("held"/\u041F\u0440\u043E\u0432\u0435\u0434\u0435\u043D\u043E) it charges nothing either and starts nothing new \u2014 duplicate the page for a new show.',
|
|
119160
119444
|
"Once published, the event date moves only inside the show window bought here (oko_update_event refuses EVENT_DATE_LOCKED outside it): for another date, unpublish with oko_unpublish_event first while the window has not opened, or duplicate the page."
|
|
119161
119445
|
]
|
|
@@ -119226,6 +119510,8 @@ function paidBlocksTurnedOn(patch) {
|
|
|
119226
119510
|
blocks.push("cards");
|
|
119227
119511
|
if (Array.isArray(patch.forms) && patch.forms.length > 0)
|
|
119228
119512
|
blocks.push("forms");
|
|
119513
|
+
if (Array.isArray(patch.polls) && patch.polls.length > 0)
|
|
119514
|
+
blocks.push("polls");
|
|
119229
119515
|
return blocks;
|
|
119230
119516
|
}
|
|
119231
119517
|
function pageSoldItsShow(event) {
|
|
@@ -119390,6 +119676,59 @@ function controlRoomPreview(event) {
|
|
|
119390
119676
|
};
|
|
119391
119677
|
}
|
|
119392
119678
|
|
|
119679
|
+
// build/tools/pollBindings.js
|
|
119680
|
+
var BLOCK_POLL_BINDING_FIELDS = [
|
|
119681
|
+
"blocks.promoBanner.pollId",
|
|
119682
|
+
"blocks.primaryCta.pollId"
|
|
119683
|
+
];
|
|
119684
|
+
var FORM_FIELD_OF = {
|
|
119685
|
+
"blocks.promoBanner.pollId": "blocks.promoBanner.formId",
|
|
119686
|
+
"blocks.primaryCta.pollId": "blocks.primaryCta.formId"
|
|
119687
|
+
};
|
|
119688
|
+
function pollBindingsAfterPatch(fresh, patch) {
|
|
119689
|
+
const bindings = [];
|
|
119690
|
+
for (const field2 of BLOCK_POLL_BINDING_FIELDS) {
|
|
119691
|
+
const value = valueAfterPatch(fresh, patch, field2);
|
|
119692
|
+
if (typeof value === "string" && value)
|
|
119693
|
+
bindings.push({ field: field2, pollId: value });
|
|
119694
|
+
}
|
|
119695
|
+
const cards = valueAfterPatch(fresh, patch, "blocks.cards.items");
|
|
119696
|
+
if (Array.isArray(cards)) {
|
|
119697
|
+
cards.forEach((card, index) => {
|
|
119698
|
+
const pollId = card && typeof card === "object" && !Array.isArray(card) ? card.pollId : null;
|
|
119699
|
+
if (typeof pollId === "string" && pollId) {
|
|
119700
|
+
bindings.push({ field: `blocks.cards.items[${index}].pollId`, pollId });
|
|
119701
|
+
}
|
|
119702
|
+
});
|
|
119703
|
+
}
|
|
119704
|
+
return bindings;
|
|
119705
|
+
}
|
|
119706
|
+
function assertPollBindingsExist(fresh, patch) {
|
|
119707
|
+
const pollsAfter = pollIdSet(valueAfterPatch(fresh, patch, "polls"));
|
|
119708
|
+
for (const field2 of BLOCK_POLL_BINDING_FIELDS) {
|
|
119709
|
+
if (!Object.prototype.hasOwnProperty.call(patch, field2))
|
|
119710
|
+
continue;
|
|
119711
|
+
const value = patch[field2];
|
|
119712
|
+
if (typeof value !== "string" || !value)
|
|
119713
|
+
continue;
|
|
119714
|
+
if (!pollsAfter.has(value)) {
|
|
119715
|
+
throw new Error(`${field2}: poll "${value}" not found in event.polls. Create the poll in the same call (or beforehand) \u2014 a button bound to a missing poll does nothing for viewers.`);
|
|
119716
|
+
}
|
|
119717
|
+
const formField = FORM_FIELD_OF[field2];
|
|
119718
|
+
const form = valueAfterPatch(fresh, patch, formField);
|
|
119719
|
+
if (typeof form === "string" && form) {
|
|
119720
|
+
throw new Error(`${field2}: this button already opens the form "${form}", and a button has one action. Pass ${field2.split(".")[1]}: {formId: null} in the same call to switch it to the poll.`);
|
|
119721
|
+
}
|
|
119722
|
+
}
|
|
119723
|
+
const pollsBefore = pollIdSet(valueAfterPatch(fresh, {}, "polls"));
|
|
119724
|
+
const orphaned = pollBindingsAfterPatch(fresh, patch).filter((binding) => !pollsAfter.has(binding.pollId) && pollsBefore.has(binding.pollId));
|
|
119725
|
+
if (orphaned.length === 0)
|
|
119726
|
+
return;
|
|
119727
|
+
const listed = orphaned.map((binding) => `${binding.field} -> "${binding.pollId}"`).join(", ");
|
|
119728
|
+
const cardsAffected = orphaned.some((binding) => binding.field.startsWith("blocks.cards."));
|
|
119729
|
+
throw new Error(`This call removes a poll that buttons still point at: ${listed}. A button bound to a missing poll does nothing for viewers, so the removal is refused while something still points at it. Unbind them first: pass promoBanner: {pollId: null} and/or primaryCta: {pollId: null} in this same call` + (cardsAffected ? ", and clear the card bindings with oko_configure_cards (pollId: null on those cards) before removing the poll." : "."));
|
|
119730
|
+
}
|
|
119731
|
+
|
|
119393
119732
|
// build/tools/eventCreate.js
|
|
119394
119733
|
async function assertCanCreateInWorkspace(uid, workspaceId) {
|
|
119395
119734
|
if (workspaceId === uid)
|
|
@@ -119467,6 +119806,11 @@ function buildDuplicateEventDocument(input) {
|
|
|
119467
119806
|
meta: withoutSessionPlan(source.meta) ?? { eventDate: null, speakerName: null, description: null },
|
|
119468
119807
|
blocks: source.blocks ?? {},
|
|
119469
119808
|
...Array.isArray(source.forms) ? { forms: source.forms } : {},
|
|
119809
|
+
// Polls travel with the copy like forms do: a button of `blocks` can point at a pollId, and a copy without
|
|
119810
|
+
// `polls` would leave that button bound to a poll the new document does not have. Only the CONFIGURATION is
|
|
119811
|
+
// copied — the votes, the rounds and the state of the show stay with the source, so the copy starts at round 1
|
|
119812
|
+
// with no votes (the web's duplicate does the same, `EventsPageClient.tsx`).
|
|
119813
|
+
...Array.isArray(source.polls) ? { polls: source.polls } : {},
|
|
119470
119814
|
// 🔴 The copy does NOT inherit server-owned Telegram markers.
|
|
119471
119815
|
// `reminderSent` is set by CF17 after sending the reminder, and the same
|
|
119472
119816
|
// function skips events with this flag forever: a copy of an event whose
|
|
@@ -119525,7 +119869,7 @@ function scheduleWindowPassed(sessionMode, eventDateMs, rawDurationMinutes, nowM
|
|
|
119525
119869
|
}
|
|
119526
119870
|
|
|
119527
119871
|
// build/tools/sessionPlan.js
|
|
119528
|
-
function
|
|
119872
|
+
function isRecord2(value) {
|
|
119529
119873
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
119530
119874
|
}
|
|
119531
119875
|
function timestampMillis(value) {
|
|
@@ -119537,7 +119881,7 @@ function timestampMillis(value) {
|
|
|
119537
119881
|
return null;
|
|
119538
119882
|
}
|
|
119539
119883
|
function readSessionPlan(event, durationMsOf) {
|
|
119540
|
-
const meta2 =
|
|
119884
|
+
const meta2 = isRecord2(event.meta) ? event.meta : {};
|
|
119541
119885
|
const startMs = timestampMillis(meta2.sessionStartAt);
|
|
119542
119886
|
const endMs = timestampMillis(meta2.sessionEndAt);
|
|
119543
119887
|
if (startMs !== null && endMs !== null && endMs > startMs)
|
|
@@ -119555,7 +119899,7 @@ function sessionPlanWarnings(event) {
|
|
|
119555
119899
|
const plan = sessionPlanOf(event);
|
|
119556
119900
|
if (!plan || plan.source !== "meta")
|
|
119557
119901
|
return [];
|
|
119558
|
-
const meta2 =
|
|
119902
|
+
const meta2 = isRecord2(event.meta) ? event.meta : {};
|
|
119559
119903
|
const eventDateMs = timestampMillis(meta2.eventDate);
|
|
119560
119904
|
if (eventDateMs === null)
|
|
119561
119905
|
return [];
|
|
@@ -119759,11 +120103,40 @@ function validateEventSetup(eventId, event, nowMs = Date.now()) {
|
|
|
119759
120103
|
if (id && !formIds.has(id))
|
|
119760
120104
|
add(errors, "form_binding_missing", path, `Referenced formId "${id}" does not exist in event.forms.`);
|
|
119761
120105
|
};
|
|
120106
|
+
const rawPolls = Array.isArray(event.polls) ? event.polls : [];
|
|
120107
|
+
const servedPollIds = new Set(parseEventPolls(event.polls).map((poll) => poll.id));
|
|
120108
|
+
const seenPollIds = /* @__PURE__ */ new Set();
|
|
120109
|
+
rawPolls.slice(0, MAX_POLLS).forEach((raw, index) => {
|
|
120110
|
+
const id = safeString(asRecord(raw).id).trim();
|
|
120111
|
+
if (id && seenPollIds.has(id)) {
|
|
120112
|
+
add(errors, "poll_id_duplicate", `polls[${index}].id`, `Duplicate poll id: ${id}. OKO serves the first poll with an id and drops the rest.`);
|
|
120113
|
+
} else if (parsePoll(raw) === null) {
|
|
120114
|
+
add(errors, "poll_invalid", `polls[${index}]`, "This poll is not complete, so viewers cannot open it: a poll needs its own id, a question and 2 to 10 options, each with an id and a text. Complete it or remove it with oko_configure_blocks polls.");
|
|
120115
|
+
}
|
|
120116
|
+
if (id)
|
|
120117
|
+
seenPollIds.add(id);
|
|
120118
|
+
});
|
|
120119
|
+
if (rawPolls.length > MAX_POLLS) {
|
|
120120
|
+
add(warnings, "polls_too_many", "polls", `The page lists ${rawPolls.length} polls; OKO serves the first ${MAX_POLLS} and ignores the rest.`);
|
|
120121
|
+
}
|
|
120122
|
+
const checkPollBinding = (path, pollId) => {
|
|
120123
|
+
const id = safeString(pollId).trim();
|
|
120124
|
+
if (id && !servedPollIds.has(id)) {
|
|
120125
|
+
add(errors, "poll_binding_missing", path, `Referenced pollId "${id}" is not a poll OKO serves: it does not exist in event.polls, or it is not complete.`);
|
|
120126
|
+
}
|
|
120127
|
+
};
|
|
120128
|
+
const checkTargetsConflict = (path, formId, pollId) => {
|
|
120129
|
+
if (safeString(formId).trim() && safeString(pollId).trim()) {
|
|
120130
|
+
add(warnings, "poll_binding_shadowed", path, "The button is bound to a form AND a poll; the form wins, so the poll never opens. Keep one: set the other to null.");
|
|
120131
|
+
}
|
|
120132
|
+
};
|
|
119762
120133
|
const promo = asRecord(blocks.promoBanner);
|
|
119763
120134
|
if (promo.enabled === true) {
|
|
119764
120135
|
checkFormBinding("blocks.promoBanner.formId", promo.formId);
|
|
119765
|
-
|
|
119766
|
-
|
|
120136
|
+
checkPollBinding("blocks.promoBanner.pollId", promo.pollId);
|
|
120137
|
+
checkTargetsConflict("blocks.promoBanner", promo.formId, promo.pollId);
|
|
120138
|
+
if (!safeString(promo.link).trim() && !safeString(promo.formId).trim() && !safeString(promo.pollId).trim()) {
|
|
120139
|
+
add(suggestions, "promo_target_missing", "blocks.promoBanner", "Enabled promo banner has no link, form or poll binding.");
|
|
119767
120140
|
}
|
|
119768
120141
|
if (!safeString(promo.imageUrl).trim()) {
|
|
119769
120142
|
add(warnings, "promo_image_missing", "blocks.promoBanner.imageUrl", "Enabled promo banner has no image.");
|
|
@@ -119776,14 +120149,16 @@ function validateEventSetup(eventId, event, nowMs = Date.now()) {
|
|
|
119776
120149
|
const cta = asRecord(blocks.primaryCta);
|
|
119777
120150
|
if (cta.enabled === true) {
|
|
119778
120151
|
checkFormBinding("blocks.primaryCta.formId", cta.formId);
|
|
120152
|
+
checkPollBinding("blocks.primaryCta.pollId", cta.pollId);
|
|
120153
|
+
checkTargetsConflict("blocks.primaryCta", cta.formId, cta.pollId);
|
|
119779
120154
|
if (!safeString(cta.title).trim() && !safeString(cta.text).trim()) {
|
|
119780
120155
|
add(suggestions, "cta_copy_missing", "blocks.primaryCta", "Enabled CTA has no title or text.");
|
|
119781
120156
|
}
|
|
119782
120157
|
if (!safeString(cta.buttonLabel).trim()) {
|
|
119783
120158
|
add(warnings, "cta_button_missing", "blocks.primaryCta.buttonLabel", "Enabled CTA has no button label.");
|
|
119784
120159
|
}
|
|
119785
|
-
if (safeString(cta.buttonLabel).trim() && !safeString(cta.buttonUrl).trim() && !safeString(cta.formId).trim()) {
|
|
119786
|
-
add(warnings, "cta_target_missing", "blocks.primaryCta", "CTA button has no URL or
|
|
120160
|
+
if (safeString(cta.buttonLabel).trim() && !safeString(cta.buttonUrl).trim() && !safeString(cta.formId).trim() && !safeString(cta.pollId).trim()) {
|
|
120161
|
+
add(warnings, "cta_target_missing", "blocks.primaryCta", "CTA button has no URL, form or poll binding.");
|
|
119787
120162
|
}
|
|
119788
120163
|
}
|
|
119789
120164
|
const videoPlaceholder = asRecord(asRecord(blocks.video).placeholder);
|
|
@@ -119812,10 +120187,13 @@ function validateEventSetup(eventId, event, nowMs = Date.now()) {
|
|
|
119812
120187
|
cardIds.add(card.id);
|
|
119813
120188
|
if (!card.title && !card.text)
|
|
119814
120189
|
add(suggestions, "card_copy_missing", path, "Card has no title or text.");
|
|
119815
|
-
if (card.buttonLabel && !card.buttonUrl && !card.formId)
|
|
119816
|
-
add(warnings, "card_target_missing", path, "Card button has no URL or
|
|
120190
|
+
if (card.buttonLabel && !card.buttonUrl && !card.formId && !card.pollId)
|
|
120191
|
+
add(warnings, "card_target_missing", path, "Card button has no URL, form or poll binding.");
|
|
119817
120192
|
if (card.formId && !formIds.has(card.formId))
|
|
119818
120193
|
add(errors, "card_form_missing", `${path}.formId`, `Referenced formId "${card.formId}" does not exist.`);
|
|
120194
|
+
if (card.pollId && !servedPollIds.has(card.pollId))
|
|
120195
|
+
add(errors, "card_poll_missing", `${path}.pollId`, `Referenced pollId "${card.pollId}" is not a poll OKO serves: it does not exist in event.polls, or it is not complete.`);
|
|
120196
|
+
checkTargetsConflict(path, card.formId, card.pollId);
|
|
119819
120197
|
if (card.imageMasterUrl && !card.imageVariants)
|
|
119820
120198
|
add(warnings, "card_variants_pending", `${path}.imageVariants`, "Card image variants are not ready yet.");
|
|
119821
120199
|
});
|
|
@@ -119865,20 +120243,20 @@ function stateWithin(now, window2) {
|
|
|
119865
120243
|
return "open";
|
|
119866
120244
|
return "after";
|
|
119867
120245
|
}
|
|
119868
|
-
function
|
|
120246
|
+
function isRecord3(value) {
|
|
119869
120247
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
119870
120248
|
}
|
|
119871
120249
|
function toMillis2(value) {
|
|
119872
120250
|
if (typeof value === "number" && Number.isFinite(value))
|
|
119873
120251
|
return value;
|
|
119874
|
-
if (
|
|
120252
|
+
if (isRecord3(value) && typeof value.toMillis === "function") {
|
|
119875
120253
|
const ms = value.toMillis();
|
|
119876
120254
|
return typeof ms === "number" && Number.isFinite(ms) ? ms : null;
|
|
119877
120255
|
}
|
|
119878
120256
|
return null;
|
|
119879
120257
|
}
|
|
119880
120258
|
function storedWindow(event) {
|
|
119881
|
-
const billing =
|
|
120259
|
+
const billing = isRecord3(event.billing) ? event.billing : {};
|
|
119882
120260
|
const start = toMillis2(billing.windowStart);
|
|
119883
120261
|
const end = toMillis2(billing.windowEnd);
|
|
119884
120262
|
return start !== null && end !== null && end > start ? { windowStart: new Date(start), windowEnd: new Date(end) } : null;
|
|
@@ -119887,12 +120265,12 @@ function instantMs(value) {
|
|
|
119887
120265
|
return toMillis2(value);
|
|
119888
120266
|
}
|
|
119889
120267
|
function windowFromEvent(event, nowMs) {
|
|
119890
|
-
const billing =
|
|
120268
|
+
const billing = isRecord3(event.billing) ? event.billing : {};
|
|
119891
120269
|
const storedStart = toMillis2(billing.windowStart);
|
|
119892
120270
|
const storedEnd = toMillis2(billing.windowEnd);
|
|
119893
120271
|
let window2 = storedStart !== null && storedEnd !== null ? { windowStart: new Date(storedStart), windowEnd: new Date(storedEnd) } : null;
|
|
119894
120272
|
if (!window2) {
|
|
119895
|
-
const meta2 =
|
|
120273
|
+
const meta2 = isRecord3(event.meta) ? event.meta : {};
|
|
119896
120274
|
const eventDateMs = toMillis2(meta2.eventDate);
|
|
119897
120275
|
if (eventDateMs !== null)
|
|
119898
120276
|
window2 = showWindow(new Date(eventDateMs));
|
|
@@ -119901,18 +120279,18 @@ function windowFromEvent(event, nowMs) {
|
|
|
119901
120279
|
}
|
|
119902
120280
|
|
|
119903
120281
|
// build/tools/eventDate.js
|
|
119904
|
-
function
|
|
120282
|
+
function isRecord4(value) {
|
|
119905
120283
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
119906
120284
|
}
|
|
119907
120285
|
function readMeta(event) {
|
|
119908
|
-
return
|
|
120286
|
+
return isRecord4(event.meta) ? event.meta : {};
|
|
119909
120287
|
}
|
|
119910
120288
|
function judgeEventDateChange(fresh, newValue, nowMs) {
|
|
119911
120289
|
const newMs = newValue === null ? null : instantMs(newValue);
|
|
119912
120290
|
const currentMs = instantMs(readMeta(fresh).eventDate);
|
|
119913
120291
|
if (newMs !== null && currentMs !== null && newMs === currentMs)
|
|
119914
120292
|
return { allowed: true };
|
|
119915
|
-
const billing =
|
|
120293
|
+
const billing = isRecord4(fresh.billing) ? fresh.billing : null;
|
|
119916
120294
|
if (!billing || billing.state !== "published" && billing.state !== "held")
|
|
119917
120295
|
return { allowed: true };
|
|
119918
120296
|
const window2 = storedWindow(fresh);
|
|
@@ -120090,6 +120468,10 @@ function assertFormBindingsExist(fresh, patch) {
|
|
|
120090
120468
|
const cardsAffected = orphaned.some((binding) => binding.field.startsWith("blocks.cards."));
|
|
120091
120469
|
throw new Error(`This call removes a form that buttons still point at: ${listed}. A button bound to a missing form does nothing for viewers, so the removal is refused while something still points at it. Unbind them first: pass promoBanner: {formId: null} and/or primaryCta: {formId: null} in this same call` + (cardsAffected ? ", and clear the card bindings with oko_configure_cards before removing the form." : "."));
|
|
120092
120470
|
}
|
|
120471
|
+
function assertBindingsExist(fresh, patch) {
|
|
120472
|
+
assertFormBindingsExist(fresh, patch);
|
|
120473
|
+
assertPollBindingsExist(fresh, patch);
|
|
120474
|
+
}
|
|
120093
120475
|
function configureBlocksScopes(args) {
|
|
120094
120476
|
return Object.keys(telegramBlocksPatch(args)).length > 0 ? ["setup:write", "telegram:write"] : ["setup:write"];
|
|
120095
120477
|
}
|
|
@@ -120391,26 +120773,30 @@ function registerEventTools(server2) {
|
|
|
120391
120773
|
server2.registerTool("oko_configure_blocks", {
|
|
120392
120774
|
title: "Configure event blocks",
|
|
120393
120775
|
description: toolDescription({
|
|
120394
|
-
summary: "Configure supported event setup blocks, forms, and Telegram channel ids.",
|
|
120776
|
+
summary: "Configure supported event setup blocks, forms, polls, and Telegram channel ids.",
|
|
120395
120777
|
exampleCall: { "eventId": "Kx9pQ2mR7tL4vB8nW3cY", "chat": { "enabled": true }, "promoBanner": { "enabled": false }, "primaryCta": { "enabled": true, "title": "Get the slides", "buttonLabel": "Download", "buttonUrl": "https://example.com/slides" }, "videoPlaceholder": { "enabled": true, "mode": "glass", "showTimer": true, "title": "The webinar starts at 14:00", "subtitle": "Moscow time" } },
|
|
120396
120778
|
args: [
|
|
120397
120779
|
"eventId: event to edit.",
|
|
120398
120780
|
"chat/promoBanner/primaryCta/videoPlaceholder: optional block patches.",
|
|
120399
|
-
"forms: replaces configured lead forms when provided; removing a form that a button still points at is refused.",
|
|
120781
|
+
"forms: replaces configured lead forms when provided; removing a form that a button still points at is refused. Omit to leave the forms (and the polls) untouched.",
|
|
120782
|
+
"polls: replaces configured polls when provided (max 10, each with a question and 2-10 options); omit to leave the polls (and the forms) untouched. Pass every existing poll and option back with its id (oko_get_event shows them) \u2014 an option sent without an id is a NEW option, and the votes of an option are tied to its id. Removing a poll that a button still points at is refused.",
|
|
120783
|
+
"promoBanner.pollId / primaryCta.pollId: the button opens this poll in a popup (a card's pollId is set in oko_configure_cards). A button has one action: link, form or poll. null removes the binding.",
|
|
120400
120784
|
"telegramChannelIds: replaces configured Telegram channels when provided; needs the telegram:write scope AND the notification permission on the event, and is refused before anything is written when either is missing.",
|
|
120401
|
-
"confirm: only for a call that turns on a paid block (promoBanner, primaryCta, forms) on a PUBLISHED page \u2014 see the note on paid blocks."
|
|
120785
|
+
"confirm: only for a call that turns on a paid block (promoBanner, primaryCta, forms, polls) on a PUBLISHED page \u2014 see the note on paid blocks."
|
|
120402
120786
|
],
|
|
120403
|
-
returns: "event id and the list of Firestore fields updated; after a paid block, also payment (charged, balanceAfter) and a hint.",
|
|
120787
|
+
returns: "event id and the list of Firestore fields updated; after polls, the polls as stored (their ids, and the ids of their options); after a paid block, also payment (charged, balanceAfter) and a hint.",
|
|
120404
120788
|
examples: ["Use after oko_create_webinar to enable chat, bind a CTA/form, and configure other audience blocks."],
|
|
120405
120789
|
notes: [
|
|
120406
|
-
"\u{1F534} Paid blocks after publication: the promo banner, the CTA and the
|
|
120790
|
+
"\u{1F534} Paid blocks after publication: the promo banner, the CTA, the forms and the polls cost 10 lumens each (the cards block is paid in oko_configure_cards; forms are ONE block however many there are, and so are polls \u2014 the polls are their own block, never part of the forms) when they are turned on AFTER the page is published. On a draft they are free \u2014 they enter the bill at publication. On a published page, a call that turns one on without confirm writes NOTHING and answers with the quote: the blocks, the lumens, the wallet that pays and who that is. Show the organizer, and repeat the call with confirm: true after they agree; the wallet is charged once for the whole set and only then is the patch written. Turning a block off and on again in the same show costs nothing more, and a block already paid for is not charged again. Refusals carry errorCode and whatToDo: INSUFFICIENT_TOKENS (the wallet that pays \u2014 the workspace owner's \u2014 is short), ENTITLEMENT_REQUIRED (that owner has no live subscription), PAYER_UNRESOLVED, EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH; nothing is written or charged then. The key needs the events:write scope besides setup:write for this. Unpublishing the page later does NOT return these top-up charges.",
|
|
120407
120791
|
'\u{1F534} "Turn off the chat" is ambiguous, and the two meanings are different actions. chat.enabled=false HIDES the chat panel from the page for everyone \u2014 the history is not deleted (messages are kept for 90 days) and comes back when the block is enabled again \u2014 but during or after a webinar that is almost never what is asked for. Stopping new messages is a different tool: oko_set_chat_mode with mode="off" leaves the panel and the history on the page and only closes the input. When the organizer says "turn off the chat", "close the chat", "stop the chat", "mute the chat", "disable chat", assume they mean CLOSING INTAKE (oko_set_chat_mode off), and ask before hiding the panel. The same trap in reverse: "turn the chat back on" after a pause means mode="public", not this field.',
|
|
120408
120792
|
"A block toggle and its image are independent: enabling a block here works with an image already uploaded via oko_upload_image, and uploading an image never enables the block by itself.",
|
|
120409
120793
|
"promoBanner has no text field by design: the banner is image+link. blocks.promoBanner.text seen in event documents is legacy and is never rendered on the event page.",
|
|
120410
120794
|
'videoPlaceholder.enabled only allows the placeholder; viewers see it after oko_set_block_visibility with block "placeholder" and visible true (a live override, stored outside the event document). Chat, CTA, banner and cards appear as soon as they are enabled here.',
|
|
120411
120795
|
"Setup toggle vs live override: enabled=false removes a block from the page for everyone and persists (its uploaded image stays); oko_set_block_visibility hides or shows an enabled block temporarily and is the tool for the live show. Uploading an image never enables a block.",
|
|
120412
120796
|
"telegramChannelIds is the only field here that is not a page setting: it decides which channel carries this event's messages, and the automatic reminder goes out under the event owner's name. Omit the field to configure blocks with setup:write alone; pass it only with a key that holds telegram:write and for an organizer who may send notifications for this event.",
|
|
120413
|
-
"forms and the buttons bound to them travel together in both directions: binding a button to a form that does not exist is refused, and so is replacing forms in a way that removes a form the banner, the CTA or a card button still points at. Unbind in the same call with promoBanner/primaryCta formId:null; card bindings are cleared with oko_configure_cards first. A binding that was already broken before the call is not touched and does not block it."
|
|
120797
|
+
"forms and the buttons bound to them travel together in both directions: binding a button to a form that does not exist is refused, and so is replacing forms in a way that removes a form the banner, the CTA or a card button still points at. Unbind in the same call with promoBanner/primaryCta formId:null; card bindings are cleared with oko_configure_cards first. A binding that was already broken before the call is not touched and does not block it. Polls follow the same rule with pollId.",
|
|
120798
|
+
"forms and polls are separate lists: a call with forms never touches polls, and a call with polls never touches forms (leave either argument out to keep that list as it is).",
|
|
120799
|
+
"Polls: a poll opens from a button (promoBanner.pollId, primaryCta.pollId, a card's pollId) and, with the control room, can be launched for everyone from oko_control_poll. Viewers see the results only after the organizer shows them (oko_control_poll show_results). Removing a poll takes it off the page; the votes already cast stay in the data and in the session reports. The vote counts are in oko_get_live_state (live) and oko_get_session_report (per session)."
|
|
120414
120800
|
]
|
|
120415
120801
|
}),
|
|
120416
120802
|
inputSchema: {
|
|
@@ -120422,12 +120808,13 @@ function registerEventTools(server2) {
|
|
|
120422
120808
|
closeOnSessionEnd: external_exports.boolean().optional().describe("Session trigger: close message intake when the analytics session ends (default false)."),
|
|
120423
120809
|
visibility: external_exports.enum(["always", "hidden"]).optional()
|
|
120424
120810
|
}).optional(),
|
|
120425
|
-
promoBanner: external_exports.object({ enabled: external_exports.boolean().optional(), link: external_exports.string().max(1e3).optional(), visibility: external_exports.enum(["always", "hidden"]).optional(), formId: external_exports.string().nullable().optional() }).optional(),
|
|
120426
|
-
primaryCta: external_exports.object({ enabled: external_exports.boolean().optional(), visibility: external_exports.enum(["always", "hidden"]).optional(), title: external_exports.string().nullable().optional(), text: external_exports.string().nullable().optional(), buttonLabel: external_exports.string().nullable().optional(), buttonUrl: external_exports.string().nullable().optional(), formId: external_exports.string().nullable().optional() }).optional(),
|
|
120811
|
+
promoBanner: external_exports.object({ enabled: external_exports.boolean().optional(), link: external_exports.string().max(1e3).optional(), visibility: external_exports.enum(["always", "hidden"]).optional(), formId: external_exports.string().nullable().optional(), pollId: external_exports.string().regex(POLL_ID_PATTERN).nullable().optional().describe("Opens this poll (an id from polls) when the banner is clicked; null removes the binding.") }).optional(),
|
|
120812
|
+
primaryCta: external_exports.object({ enabled: external_exports.boolean().optional(), visibility: external_exports.enum(["always", "hidden"]).optional(), title: external_exports.string().nullable().optional(), text: external_exports.string().nullable().optional(), buttonLabel: external_exports.string().nullable().optional(), buttonUrl: external_exports.string().nullable().optional(), formId: external_exports.string().nullable().optional(), pollId: external_exports.string().regex(POLL_ID_PATTERN).nullable().optional().describe("Opens this poll (an id from polls) when the CTA button is clicked; null removes the binding.") }).optional(),
|
|
120427
120813
|
videoPlaceholder: external_exports.object({ enabled: external_exports.boolean().optional(), mode: external_exports.enum(["glass", "image"]).optional(), title: external_exports.string().nullable().optional(), subtitle: external_exports.string().nullable().optional(), showTimer: external_exports.boolean().optional() }).optional(),
|
|
120428
120814
|
forms: external_exports.array(formSchema).max(20).optional(),
|
|
120815
|
+
polls: external_exports.array(pollSchema).max(MAX_POLLS).optional().describe("Replaces the page's polls. Omit to leave them as they are; [] removes them all."),
|
|
120429
120816
|
telegramChannelIds: external_exports.array(external_exports.number().int()).max(20).optional(),
|
|
120430
|
-
confirm: external_exports.boolean().optional().describe("Needed only when the call turns on a paid block (banner, CTA, forms) on an already PUBLISHED page: without it nothing is written and the answer is the quote (lumens, who pays); true after the organizer confirmed the charge.")
|
|
120817
|
+
confirm: external_exports.boolean().optional().describe("Needed only when the call turns on a paid block (banner, CTA, forms, polls) on an already PUBLISHED page: without it nothing is written and the answer is the quote (lumens, who pays); true after the organizer confirmed the charge.")
|
|
120431
120818
|
},
|
|
120432
120819
|
outputSchema: updateOutputSchema,
|
|
120433
120820
|
annotations: writeTool
|
|
@@ -120457,6 +120844,8 @@ function registerEventTools(server2) {
|
|
|
120457
120844
|
patch["blocks.promoBanner.visibility"] = args.promoBanner.visibility;
|
|
120458
120845
|
if (args.promoBanner?.formId !== void 0)
|
|
120459
120846
|
patch["blocks.promoBanner.formId"] = args.promoBanner.formId;
|
|
120847
|
+
if (args.promoBanner?.pollId !== void 0)
|
|
120848
|
+
patch["blocks.promoBanner.pollId"] = args.promoBanner.pollId;
|
|
120460
120849
|
if (args.primaryCta?.enabled !== void 0)
|
|
120461
120850
|
patch["blocks.primaryCta.enabled"] = args.primaryCta.enabled;
|
|
120462
120851
|
if (args.primaryCta?.visibility !== void 0)
|
|
@@ -120471,6 +120860,8 @@ function registerEventTools(server2) {
|
|
|
120471
120860
|
patch["blocks.primaryCta.buttonUrl"] = normalizeCardButtonUrl(args.primaryCta.buttonUrl);
|
|
120472
120861
|
if (args.primaryCta?.formId !== void 0)
|
|
120473
120862
|
patch["blocks.primaryCta.formId"] = args.primaryCta.formId;
|
|
120863
|
+
if (args.primaryCta?.pollId !== void 0)
|
|
120864
|
+
patch["blocks.primaryCta.pollId"] = args.primaryCta.pollId;
|
|
120474
120865
|
if (args.videoPlaceholder?.enabled !== void 0)
|
|
120475
120866
|
patch["blocks.video.placeholder.enabled"] = args.videoPlaceholder.enabled;
|
|
120476
120867
|
if (args.videoPlaceholder?.mode !== void 0)
|
|
@@ -120503,6 +120894,8 @@ function registerEventTools(server2) {
|
|
|
120503
120894
|
...field2.type === "select" || field2.type === "checkbox" ? { options: [...new Set((field2.options ?? []).map((option) => option.trim()).filter(Boolean))] } : {}
|
|
120504
120895
|
}))
|
|
120505
120896
|
}));
|
|
120897
|
+
if (args.polls !== void 0)
|
|
120898
|
+
patch.polls = buildPollsForWrite(args.polls);
|
|
120506
120899
|
Object.assign(patch, telegramPatch);
|
|
120507
120900
|
const payment = await settleBlockExtras({
|
|
120508
120901
|
tool: "oko_configure_blocks",
|
|
@@ -120511,11 +120904,11 @@ function registerEventTools(server2) {
|
|
|
120511
120904
|
event: current,
|
|
120512
120905
|
patch,
|
|
120513
120906
|
confirm: args.confirm,
|
|
120514
|
-
precheck: () =>
|
|
120907
|
+
precheck: () => assertBindingsExist(current, patch)
|
|
120515
120908
|
});
|
|
120516
120909
|
if (payment.kind === "quote")
|
|
120517
120910
|
requireConfirm(args.confirm, "oko_configure_blocks", payment.preview);
|
|
120518
|
-
await writeEventPatchWithCards(args.eventId, patch, null,
|
|
120911
|
+
await writeEventPatchWithCards(args.eventId, patch, null, assertBindingsExist);
|
|
120519
120912
|
const paid = paymentFields(payment);
|
|
120520
120913
|
const hints = [...paid.hints ?? []];
|
|
120521
120914
|
if (args.chat?.visibility !== void 0) {
|
|
@@ -120544,7 +120937,20 @@ function registerEventTools(server2) {
|
|
|
120544
120937
|
if (args.videoPlaceholder?.enabled === false) {
|
|
120545
120938
|
hints.push('If a live override made the placeholder visible earlier, reset it with oko_set_block_visibility block "placeholder" visible null.');
|
|
120546
120939
|
}
|
|
120547
|
-
|
|
120940
|
+
const writtenPolls = Array.isArray(patch.polls) ? patch.polls : null;
|
|
120941
|
+
if (writtenPolls && writtenPolls.length > 0) {
|
|
120942
|
+
hints.push("A poll reaches viewers through a button that opens it: bind one with promoBanner.pollId, primaryCta.pollId or a card's pollId (oko_configure_cards). Viewers vote in a popup and see the results only after the organizer shows them (oko_control_poll show_results). Launching a poll for everyone and resetting it need the control room.");
|
|
120943
|
+
}
|
|
120944
|
+
if (writtenPolls && writtenPolls.length === 0) {
|
|
120945
|
+
hints.push("The polls were taken off the page. Votes already cast stay in the data and in the session reports.");
|
|
120946
|
+
}
|
|
120947
|
+
return textResult({
|
|
120948
|
+
id: args.eventId,
|
|
120949
|
+
updated: Object.keys(patch),
|
|
120950
|
+
...writtenPolls ? { polls: writtenPolls } : {},
|
|
120951
|
+
...paid.payment ? { payment: paid.payment } : {},
|
|
120952
|
+
...hints.length ? { hints } : {}
|
|
120953
|
+
});
|
|
120548
120954
|
});
|
|
120549
120955
|
server2.registerTool("oko_configure_cards", {
|
|
120550
120956
|
title: "Configure event cards",
|
|
@@ -120556,18 +120962,19 @@ function registerEventTools(server2) {
|
|
|
120556
120962
|
"enabled: optional Cards block enabled flag; turning it on for a PUBLISHED page is a paid top-up \u2014 see the note on paid blocks.",
|
|
120557
120963
|
"visibility: optional default visibility, always or hidden.",
|
|
120558
120964
|
"cardImageRatio: optional image ratio, one of auto, 1:1, 4:3, 3:2, 16:9, 2:3, 3:4.",
|
|
120559
|
-
"items: optional full replacement array, max 6 cards. Each item may include id, title, text, buttonLabel, buttonUrl, formId.",
|
|
120965
|
+
"items: optional full replacement array, max 6 cards. Each item may include id, title, text, buttonLabel, buttonUrl, formId, pollId.",
|
|
120560
120966
|
"confirm: only for enabled: true on a published page \u2014 see the note on paid blocks."
|
|
120561
120967
|
],
|
|
120562
120968
|
returns: "event id, updated Firestore fields, normalized cards config, generated card ids, preserved image ids, removed card ids, and warnings; after a paid top-up, also payment (charged, balanceAfter) and a hint.",
|
|
120563
120969
|
examples: [
|
|
120564
120970
|
"Use after oko_create_webinar to create cards before uploading per-card images.",
|
|
120565
120971
|
"Call oko_get_event first and pass the full desired items array when editing existing cards.",
|
|
120566
|
-
"Use formId instead of buttonUrl when a card button should open a lead form."
|
|
120972
|
+
"Use formId instead of buttonUrl when a card button should open a lead form, and pollId when it should open a poll (an id from oko_configure_blocks polls)."
|
|
120567
120973
|
],
|
|
120568
120974
|
notes: [
|
|
120569
120975
|
"\u{1F534} Paid block after publication: the Cards block costs 10 lumens (one block however many cards) when it is turned on AFTER the page is published; on a draft it is free \u2014 it enters the bill at publication. On a published page, enabled: true without confirm writes NOTHING (not the card list either) and answers with the quote: the lumens and the wallet that pays. Show the organizer, and repeat the call with confirm: true after they agree; the wallet is charged once and only then is the patch written. A block already paid for, or turned off and on again in the same show, is not charged twice. Refusals carry errorCode and whatToDo: INSUFFICIENT_TOKENS, ENTITLEMENT_REQUIRED, PAYER_UNRESOLVED, EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH; nothing is written or charged then. The key needs the events:write scope besides setup:write for this. Unpublishing the page later does NOT return top-up charges.",
|
|
120570
120976
|
"This tool replaces blocks.cards.items when items is provided; omitted text fields become null.",
|
|
120977
|
+
"A card button has ONE action: buttonUrl, formId or pollId. The bindings travel with the cards you send: pass formId and pollId back as oko_get_event shows them, or the card loses its binding. A binding the replacement drops is named in warnings (the write still goes through); a pollId that is not in the page's polls is refused.",
|
|
120571
120978
|
"Existing card images are preserved only when the same card id remains in the replacement list.",
|
|
120572
120979
|
"buttonUrl allows http(s), mailto, or tel URLs; javascript/data URLs are rejected.",
|
|
120573
120980
|
"If a removed card had an image, Storage cleanup is not automatic; this matches the web Setup behavior."
|
|
@@ -120599,6 +121006,7 @@ function registerEventTools(server2) {
|
|
|
120599
121006
|
const preservedImageCardIds = [];
|
|
120600
121007
|
let removedCardIds = [];
|
|
120601
121008
|
let removedCardsWithImages = [];
|
|
121009
|
+
let droppedBindings = [];
|
|
120602
121010
|
if (args.enabled !== void 0)
|
|
120603
121011
|
patch["blocks.cards.enabled"] = args.enabled;
|
|
120604
121012
|
if (args.visibility !== void 0)
|
|
@@ -120610,13 +121018,15 @@ function registerEventTools(server2) {
|
|
|
120610
121018
|
const built = buildCardsItems({
|
|
120611
121019
|
inputs: args.items,
|
|
120612
121020
|
existing: freshCards.items,
|
|
120613
|
-
formIds: eventFormIds(fresh)
|
|
121021
|
+
formIds: eventFormIds(fresh),
|
|
121022
|
+
pollIds: eventPollIds(fresh)
|
|
120614
121023
|
});
|
|
120615
121024
|
finalCards.items = built.items;
|
|
120616
121025
|
generatedCardIds.length = 0;
|
|
120617
121026
|
preservedImageCardIds.length = 0;
|
|
120618
121027
|
generatedCardIds.push(...built.generatedCardIds);
|
|
120619
121028
|
preservedImageCardIds.push(...built.preservedImageCardIds);
|
|
121029
|
+
droppedBindings = built.droppedBindings;
|
|
120620
121030
|
const nextIds = new Set(built.items.map((item) => item.id));
|
|
120621
121031
|
const removed = freshCards.items.filter((item) => !nextIds.has(item.id));
|
|
120622
121032
|
removedCardIds = removed.map((item) => item.id);
|
|
@@ -120641,6 +121051,9 @@ function registerEventTools(server2) {
|
|
|
120641
121051
|
}
|
|
120642
121052
|
const paid = paymentFields(payment);
|
|
120643
121053
|
const warnings = removedCardsWithImages.length > 0 ? [`Removed cards had images and their Storage files were not deleted: ${removedCardsWithImages.join(", ")}.`] : [];
|
|
121054
|
+
for (const dropped of droppedBindings) {
|
|
121055
|
+
warnings.push(`Card ${dropped.cardId} no longer opens the ${dropped.kind} "${dropped.targetId}": the replacement item did not carry ${dropped.kind === "poll" ? "pollId" : "formId"}. If that was not intended, repeat the call with ${dropped.kind === "poll" ? "pollId" : "formId"}: "${dropped.targetId}" on that card.`);
|
|
121056
|
+
}
|
|
120644
121057
|
if (finalCards.repairedIdPositions.length > 0) {
|
|
120645
121058
|
warnings.push(`Cards at position(s) ${finalCards.repairedIdPositions.join(", ")} had no usable id in Firestore; a temporary one was generated, so their previous images could not be matched and were dropped. Re-upload images for these cards.`);
|
|
120646
121059
|
if (args.items === void 0) {
|
|
@@ -120956,22 +121369,12 @@ function registerLeadTools(server2) {
|
|
|
120956
121369
|
// build/tools/unpublication.js
|
|
120957
121370
|
var TOOL2 = "oko_unpublish_event";
|
|
120958
121371
|
var NOTHING_WILL_CHANGE = "The page and its lumens stay exactly as they are now.";
|
|
120959
|
-
function
|
|
121372
|
+
function isRecord5(value) {
|
|
120960
121373
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
120961
121374
|
}
|
|
120962
121375
|
function refusal4(eventId, body) {
|
|
120963
121376
|
return { ...textResult({ tool: TOOL2, eventId, ...body }), isError: true };
|
|
120964
121377
|
}
|
|
120965
|
-
function notPublishedRefusal(eventId) {
|
|
120966
|
-
return refusal4(eventId, {
|
|
120967
|
-
errorCode: "NOT_PUBLISHED",
|
|
120968
|
-
message: `This page has no publication to withdraw. ${NOTHING_WILL_CHANGE}`,
|
|
120969
|
-
whatToDo: [
|
|
120970
|
-
"Explain that only a published page can be unpublished; a draft needs no withdrawal.",
|
|
120971
|
-
"Do not call with confirm:true: the answer will not change."
|
|
120972
|
-
]
|
|
120973
|
-
});
|
|
120974
|
-
}
|
|
120975
121378
|
function dateRequiredRefusal(eventId) {
|
|
120976
121379
|
return refusal4(eventId, {
|
|
120977
121380
|
errorCode: "DATE_REQUIRED",
|
|
@@ -121001,20 +121404,48 @@ function topUpsPaid(billing) {
|
|
|
121001
121404
|
if (!Array.isArray(additions))
|
|
121002
121405
|
return 0;
|
|
121003
121406
|
return additions.reduce((sum, item) => {
|
|
121004
|
-
const amount =
|
|
121407
|
+
const amount = isRecord5(item) ? item[ADDITION_AMOUNT_FIELD] : null;
|
|
121005
121408
|
return sum + (typeof amount === "number" && Number.isFinite(amount) && amount > 0 ? amount : 0);
|
|
121006
121409
|
}, 0);
|
|
121007
121410
|
}
|
|
121008
121411
|
function topUpsNote(topUps) {
|
|
121009
121412
|
return topUps > 0 ? `The ${topUps} lumens paid on top of the publication (blocks and control room turned on after it) are NOT returned; publishing again bills those blocks in the new show's bill.` : "";
|
|
121010
121413
|
}
|
|
121414
|
+
var REFUND_UP_TO_NOTE = "Part of the charge may not come back: lumens that would already have burned by now (their plan period is over) are forfeited, not returned. The exact figures are known only after confirmation: the answer carries `refunded` (what came back to the wallet) and `forfeited` (what did not).";
|
|
121415
|
+
function forfeitedNote(forfeited) {
|
|
121416
|
+
return forfeited > 0 ? `${forfeited} lumens of the charge were not returned (forfeited): lumens that would already have burned by now come back nowhere.` : "";
|
|
121417
|
+
}
|
|
121418
|
+
function forfeitedOf(outcome) {
|
|
121419
|
+
const value = outcome.forfeited;
|
|
121420
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
121421
|
+
}
|
|
121422
|
+
function unpublishPreviewNote(topUps) {
|
|
121423
|
+
return [
|
|
121424
|
+
"Unpublishing takes the page back to draft and returns UP TO the publication charge (`refundUpTo`) to the wallet that paid; the live layer (chat, presence, session start) closes immediately.",
|
|
121425
|
+
REFUND_UP_TO_NOTE,
|
|
121426
|
+
"If the page only needs another date inside its show window, oko_update_event moves it with no refund and no new charge \u2014 prefer that to unpublishing.",
|
|
121427
|
+
topUpsNote(topUps)
|
|
121428
|
+
].filter(Boolean).join(" ");
|
|
121429
|
+
}
|
|
121430
|
+
function unpublishAnswerHints(outcome, topUps) {
|
|
121431
|
+
const refunded = typeof outcome.refunded === "number" ? outcome.refunded : 0;
|
|
121432
|
+
const forfeited = forfeitedOf(outcome);
|
|
121433
|
+
if (refunded > 0) {
|
|
121434
|
+
return [
|
|
121435
|
+
`${refunded} lumens were refunded to payerUid ${safeString(outcome.payerUid)}.`,
|
|
121436
|
+
forfeitedNote(forfeited),
|
|
121437
|
+
...topUps > 0 ? [topUpsNote(topUps)] : []
|
|
121438
|
+
].filter(Boolean);
|
|
121439
|
+
}
|
|
121440
|
+
if (outcome.alreadyUnpublished === true)
|
|
121441
|
+
return ["The page was already a draft \u2014 nothing was refunded now."];
|
|
121442
|
+
return [forfeitedNote(forfeited)].filter(Boolean);
|
|
121443
|
+
}
|
|
121011
121444
|
function previewUnpublish(eventId, event, nowMs) {
|
|
121012
|
-
const billing =
|
|
121445
|
+
const billing = isRecord5(event.billing) ? event.billing : {};
|
|
121013
121446
|
const state2 = billing.state === "published" || billing.state === "held" ? billing.state : "unpublished";
|
|
121014
121447
|
if (state2 === "unpublished")
|
|
121015
121448
|
return { kind: "already" };
|
|
121016
|
-
if (state2 !== "published" && state2 !== "held")
|
|
121017
|
-
return { kind: "refuse", result: notPublishedRefusal(eventId) };
|
|
121018
121449
|
const window2 = windowFromEvent(event, nowMs);
|
|
121019
121450
|
if (state2 === "held") {
|
|
121020
121451
|
return { kind: "refuse", result: windowOpenRefusal(eventId, "after", window2?.window.windowStart ?? null) };
|
|
@@ -121067,20 +121498,21 @@ function registerUnpublicationTools(server2) {
|
|
|
121067
121498
|
server2.registerTool("oko_unpublish_event", {
|
|
121068
121499
|
title: "Unpublish event page",
|
|
121069
121500
|
description: toolDescription({
|
|
121070
|
-
summary: `Take a published OKO event page back to draft and refund its publication charge. Works only before the page's show window opens; once it has opened (or the page shows "held"/\u041F\u0440\u043E\u0432\u0435\u0434\u0435\u043D\u043E), the page stays published for good. Without confirm nothing is written: the call returns a preview \u2014 what will happen and how many lumens come back to which wallet, or the refusal if it cannot be done now. Built from the event document, not a server round trip
|
|
121501
|
+
summary: `Take a published OKO event page back to draft and refund its publication charge. Works only before the page's show window opens; once it has opened (or the page shows "held"/\u041F\u0440\u043E\u0432\u0435\u0434\u0435\u043D\u043E), the page stays published for good. Without confirm nothing is written: the call returns a preview \u2014 what will happen and UP TO how many lumens come back (refundUpTo) to which wallet, or the refusal if it cannot be done now. Built from the event document, not a server round trip, so it cannot tell how much of the charge was forfeited (lumens that would already have burned): the exact refunded and forfeited figures come only after confirm. With confirm: true the page becomes a draft and the refund (up to the charge) is paid in one step.`,
|
|
121071
121502
|
args: [
|
|
121072
121503
|
"eventId: event to unpublish.",
|
|
121073
121504
|
"confirm: true only after the organizer has seen the preview and agreed."
|
|
121074
121505
|
],
|
|
121075
|
-
returns: "status unpublished with alreadyUnpublished, refunded, balanceAfter, mirrorsUpdated, plus title, links and hints.",
|
|
121076
|
-
examples: ["Call without confirm, show the organizer
|
|
121506
|
+
returns: "status unpublished with alreadyUnpublished, refunded (what came back), forfeited (what did not, only when above zero), balanceAfter, mirrorsUpdated, plus title, links and hints. The preview (no confirm) carries refundUpTo, not refunded.",
|
|
121507
|
+
examples: ["Call without confirm, show the organizer UP TO how much comes back and to whom (never promise the whole charge), and call again with confirm: true after explicit consent; then tell the exact refunded and forfeited."],
|
|
121077
121508
|
notes: [
|
|
121078
121509
|
"Refunds go to the wallet that paid for this publication (billing.payerUid), which may not be the organizer behind this key in a shared workspace.",
|
|
121510
|
+
"The refund is UP TO the charge: the share of it drawn from lumens that would already have burned (their plan period is over) is forfeited. The preview cannot see that, so it says refundUpTo; the answer after confirm says refunded and forfeited.",
|
|
121079
121511
|
"Only the publication charge comes back. Lumens paid AFTER publication for blocks and the control room turned on later (oko_configure_blocks, oko_configure_cards with confirm) are NOT returned: the preview names them as topUpsNotRefunded, and publishing again bills those blocks in the new show's bill.",
|
|
121080
121512
|
"Publishing again afterwards (oko_publish_event) is a new show and a new charge, with a fresh show window from the new date.",
|
|
121081
|
-
"
|
|
121513
|
+
"A date INSIDE the page's show window moves with oko_update_event directly, with no refund and no new charge \u2014 prefer that when the charge may be partly forfeited. A date outside the window: unpublish (refund, up to the charge), change the date with oko_update_event, publish again (new charge); oko_update_event refuses the move otherwise (EVENT_DATE_LOCKED).",
|
|
121082
121514
|
"One rule for every page: an old page that paid nothing is unpublished the same way before its window opens (the refund is 0 lumens) and refused the same way once it has taken place (WINDOW_OPEN, windowState after).",
|
|
121083
|
-
"Refusals carry errorCode and whatToDo:
|
|
121515
|
+
"Refusals carry errorCode and whatToDo: DATE_REQUIRED, WINDOW_OPEN, EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH. A page that is already a draft is not a refusal: the call answers alreadyUnpublished."
|
|
121084
121516
|
]
|
|
121085
121517
|
}),
|
|
121086
121518
|
inputSchema: {
|
|
@@ -121115,13 +121547,13 @@ function registerUnpublicationTools(server2) {
|
|
|
121115
121547
|
requireConfirm(confirm, TOOL2, {
|
|
121116
121548
|
eventId,
|
|
121117
121549
|
title,
|
|
121118
|
-
|
|
121550
|
+
refundUpTo: preview.tokensCharged,
|
|
121119
121551
|
topUpsNotRefunded: preview.topUps,
|
|
121120
121552
|
payerUid: preview.payerUid,
|
|
121121
121553
|
showId: preview.showId,
|
|
121122
121554
|
windowStart: preview.windowStart,
|
|
121123
121555
|
windowEnd: preview.windowEnd,
|
|
121124
|
-
note:
|
|
121556
|
+
note: unpublishPreviewNote(preview.topUps)
|
|
121125
121557
|
});
|
|
121126
121558
|
}
|
|
121127
121559
|
}
|
|
@@ -121135,9 +121567,7 @@ function registerUnpublicationTools(server2) {
|
|
|
121135
121567
|
throw new ToolRefusal(named);
|
|
121136
121568
|
throw error3;
|
|
121137
121569
|
}
|
|
121138
|
-
const
|
|
121139
|
-
const topUps = topUpsPaid(isRecord4(event.billing) ? event.billing : {});
|
|
121140
|
-
const hints = refunded > 0 ? [`${refunded} lumens were refunded to payerUid ${safeString(outcome.payerUid)}.`, ...topUps > 0 ? [topUpsNote(topUps)] : []] : outcome.alreadyUnpublished === true ? ["The page was already a draft \u2014 nothing was refunded now."] : [];
|
|
121570
|
+
const hints = unpublishAnswerHints(outcome, topUpsPaid(isRecord5(event.billing) ? event.billing : {}));
|
|
121141
121571
|
return textResult({
|
|
121142
121572
|
...outcome,
|
|
121143
121573
|
title,
|
|
@@ -121151,7 +121581,8 @@ function registerUnpublicationTools(server2) {
|
|
|
121151
121581
|
var TRASH_OUTPUT_SCHEMA = external_exports.object({
|
|
121152
121582
|
trashed: external_exports.string(),
|
|
121153
121583
|
purgeAfter: external_exports.number().nullable(),
|
|
121154
|
-
refunded: external_exports.number().nonnegative().optional()
|
|
121584
|
+
refunded: external_exports.number().nonnegative().optional(),
|
|
121585
|
+
forfeited: external_exports.number().nonnegative().optional()
|
|
121155
121586
|
}).passthrough();
|
|
121156
121587
|
function trashRefundWarning(event, nowMs) {
|
|
121157
121588
|
const billing = event.billing && typeof event.billing === "object" && !Array.isArray(event.billing) ? event.billing : null;
|
|
@@ -121160,9 +121591,17 @@ function trashRefundWarning(event, nowMs) {
|
|
|
121160
121591
|
const window2 = windowFromEvent(event, nowMs);
|
|
121161
121592
|
if (window2 !== null && window2.state !== "before")
|
|
121162
121593
|
return null;
|
|
121163
|
-
const charge = typeof billing.tokensCharged === "number" ? `${billing.tokensCharged} lumens` : "
|
|
121594
|
+
const charge = typeof billing.tokensCharged === "number" ? `${billing.tokensCharged} lumens` : "what the publication paid";
|
|
121164
121595
|
const topUps = topUpsNote(topUpsPaid(billing));
|
|
121165
|
-
return `This page is published. Moving it to the trash also unpublishes it and
|
|
121596
|
+
return `This page is published. Moving it to the trash also unpublishes it and returns up to ${charge} to the wallet that paid, because its show window has not opened yet. ${REFUND_UP_TO_NOTE}${topUps ? ` ${topUps}` : ""}`;
|
|
121597
|
+
}
|
|
121598
|
+
function trashAnswerHints(data) {
|
|
121599
|
+
const refunded = typeof data.refunded === "number" ? data.refunded : 0;
|
|
121600
|
+
const forfeited = forfeitedOf(data);
|
|
121601
|
+
return [
|
|
121602
|
+
...refunded > 0 ? [`${refunded} lumens were refunded to the wallet that paid for this page's publication: it was still inside its show window when it was trashed.`] : [],
|
|
121603
|
+
...forfeited > 0 ? [forfeitedNote(forfeited)] : []
|
|
121604
|
+
];
|
|
121166
121605
|
}
|
|
121167
121606
|
function registerLifecycleTools(server2) {
|
|
121168
121607
|
const moveToTrash = async (toolName, { eventId, confirm }) => {
|
|
@@ -121177,7 +121616,7 @@ function registerLifecycleTools(server2) {
|
|
|
121177
121616
|
const trash = httpsCallable(functions, "trashEvent");
|
|
121178
121617
|
const data = (await trash({ eventId })).data;
|
|
121179
121618
|
const refunded = typeof data.refunded === "number" ? data.refunded : 0;
|
|
121180
|
-
const hints =
|
|
121619
|
+
const hints = trashAnswerHints(data);
|
|
121181
121620
|
return textResult({ ...data, refunded, ...hints.length ? { hints } : {} });
|
|
121182
121621
|
};
|
|
121183
121622
|
server2.registerTool("oko_delete_event", {
|
|
@@ -121189,7 +121628,7 @@ function registerLifecycleTools(server2) {
|
|
|
121189
121628
|
examples: ["Use after showing the exact event title and asking the organizer to confirm moving it to trash."],
|
|
121190
121629
|
notes: [
|
|
121191
121630
|
"Recoverable for 30 days with oko_restore_event. This does not create an event lifecycle status.",
|
|
121192
|
-
"A published, paid page still inside its show window is also unpublished and its charge refunded; the preview (without confirm) names it in `warning`, and the answer's `refunded` field says how much came back."
|
|
121631
|
+
"A published, paid page still inside its show window is also unpublished and UP TO its charge refunded; the preview (without confirm) names it in `warning`, and the answer's `refunded` field says how much came back (`forfeited`, when above zero, how much did not)."
|
|
121193
121632
|
]
|
|
121194
121633
|
}),
|
|
121195
121634
|
inputSchema: { eventId: eventIdField, confirm: external_exports.boolean().optional().describe("Required true to move the event to trash.") },
|
|
@@ -121205,7 +121644,7 @@ function registerLifecycleTools(server2) {
|
|
|
121205
121644
|
examples: ["Use after showing the exact event title and asking the organizer to confirm moving it to trash."],
|
|
121206
121645
|
notes: [
|
|
121207
121646
|
"End any active analytics session first. Restore with oko_restore_event before the purge deadline.",
|
|
121208
|
-
"A published, paid page still inside its show window is also unpublished and its charge refunded; the preview (without confirm) names it in `warning`, and the answer's `refunded` field says how much came back."
|
|
121647
|
+
"A published, paid page still inside its show window is also unpublished and UP TO its charge refunded; the preview (without confirm) names it in `warning`, and the answer's `refunded` field says how much came back (`forfeited`, when above zero, how much did not)."
|
|
121209
121648
|
]
|
|
121210
121649
|
}),
|
|
121211
121650
|
inputSchema: { eventId: eventIdField, confirm: external_exports.boolean().optional().describe("Required true to move the event to trash.") },
|
|
@@ -121269,7 +121708,7 @@ function registerLifecycleTools(server2) {
|
|
|
121269
121708
|
returns: "new event id, public/setup/analytics URLs, source event id, and workspaceId.",
|
|
121270
121709
|
examples: ["Use to create a reusable copy of a configured webinar before editing the new copy."],
|
|
121271
121710
|
notes: [
|
|
121272
|
-
"Copies title, video, meta, blocks, forms, and Telegram config; does not copy analytics sessions, leads, or the publication \u2014 the copy is a new page, born a draft.",
|
|
121711
|
+
"Copies title, video, meta, blocks, forms, polls, and Telegram config; does not copy analytics sessions, leads, poll votes (the copy starts round 1 with no votes), or the publication \u2014 the copy is a new page, born a draft.",
|
|
121273
121712
|
"Live overrides are NOT copied: the copy starts with chat mode public and the placeholder hidden, whatever the source had. Set them again with oko_set_chat_mode and oko_set_block_visibility on the new event.",
|
|
121274
121713
|
"Requires only events:write plus createEvents in the target workspace. To move (not copy) an event, use oko_move_event with the same scope.",
|
|
121275
121714
|
"Telegram config is copied only when the key holds telegram:write AND the organizer may send notifications for the source event; otherwise the copy starts with Telegram off and a hint says so. Deciding which channel carries an event is a send permission, not a setup one."
|
|
@@ -121397,6 +121836,275 @@ function killSwitchHint(state2) {
|
|
|
121397
121836
|
return null;
|
|
121398
121837
|
}
|
|
121399
121838
|
|
|
121839
|
+
// build/tools/polls.js
|
|
121840
|
+
var TOOL3 = "oko_control_poll";
|
|
121841
|
+
function refusal5(eventId, pollId, body) {
|
|
121842
|
+
return { ...textResult({ tool: TOOL3, eventId, ...pollId ? { pollId } : {}, ...body }), isError: true };
|
|
121843
|
+
}
|
|
121844
|
+
var POLL_ACTION_EFFECT = {
|
|
121845
|
+
close: "Voting closes: viewers who open the poll are told it is closed, and a popup that was launched for everyone is put out.",
|
|
121846
|
+
open: "Voting opens again: viewers can vote in the current round.",
|
|
121847
|
+
show_results: "Viewers see the results of the current round (the bars and the shares, refreshed every couple of seconds). Without this only the organizer sees the count.",
|
|
121848
|
+
hide_results: "Viewers no longer see the results; only the organizer sees the count.",
|
|
121849
|
+
launch: "The poll pops up for every viewer who has not voted in this round (and for those who join while it is launched); a viewer who closes it does not see it again until the next launch. Voting is opened if it was closed.",
|
|
121850
|
+
stop: "The popup is put out for everyone. Viewers can still open the poll from its button.",
|
|
121851
|
+
reset: "A new round starts: the count is zero, every viewer may vote again, and the popup and the results are put out. The votes of the earlier rounds stay in the data and in the session reports."
|
|
121852
|
+
};
|
|
121853
|
+
function needsControlRoom(action) {
|
|
121854
|
+
return CONTROL_ROOM_POLL_ACTIONS.includes(action);
|
|
121855
|
+
}
|
|
121856
|
+
var CONTROL_ROOM_NOTE = `launch and reset use the live control room of this show (${CONTROL_ROOM_LUMENS} lumens, Pro plan and above), which the organizer buys with the "Enable control room" button in the event's analytics \u2014 no MCP tool buys it. When the control room is not open for the show, OKO refuses with CONTROL_ROOM_REQUIRED and changes nothing. close, open, show_results, hide_results and stop need no control room on any plan.`;
|
|
121857
|
+
function planPollCommand(args) {
|
|
121858
|
+
const { eventId, event, pollId, action, state: state2 } = args;
|
|
121859
|
+
const poll = parseEventPolls(event.polls).find((item) => item.id === pollId);
|
|
121860
|
+
if (!poll) {
|
|
121861
|
+
const incomplete = pollIdSet(event.polls).has(pollId);
|
|
121862
|
+
throw new ToolRefusal(refusal5(eventId, pollId, incomplete ? {
|
|
121863
|
+
errorCode: "POLL_NOT_READY",
|
|
121864
|
+
message: "This poll is on the page but is not complete (a question and 2 to 10 options are needed), so OKO does not serve it and cannot command it. Nothing was changed.",
|
|
121865
|
+
whatToDo: [
|
|
121866
|
+
"Complete the poll with oko_configure_blocks polls (pass the poll with its id, a question and at least two options), then repeat.",
|
|
121867
|
+
"Do not retry as is: the answer will not change until the poll is complete."
|
|
121868
|
+
]
|
|
121869
|
+
} : {
|
|
121870
|
+
errorCode: "POLL_NOT_FOUND",
|
|
121871
|
+
message: "The page has no poll with this id. Nothing was changed.",
|
|
121872
|
+
whatToDo: [
|
|
121873
|
+
"Read the ids with oko_get_event (polls[].id) or create the poll with oko_configure_blocks polls, then repeat.",
|
|
121874
|
+
"Do not retry as is: the answer will not change until the poll exists."
|
|
121875
|
+
]
|
|
121876
|
+
}));
|
|
121877
|
+
}
|
|
121878
|
+
return {
|
|
121879
|
+
poll,
|
|
121880
|
+
preview: {
|
|
121881
|
+
eventId,
|
|
121882
|
+
pollId,
|
|
121883
|
+
question: poll.question,
|
|
121884
|
+
action,
|
|
121885
|
+
effect: POLL_ACTION_EFFECT[action],
|
|
121886
|
+
needsControlRoom: needsControlRoom(action),
|
|
121887
|
+
// A state that could not be read is `null`, never the defaults: «round 1, open» would be a guess.
|
|
121888
|
+
currentState: state2,
|
|
121889
|
+
...state2 === null ? { currentStateNote: `The state of the poll could not be read just now${args.stateUnreadable ? ` (${args.stateUnreadable})` : ""}; the command is judged by the server all the same.` } : {},
|
|
121890
|
+
note: needsControlRoom(action) ? `Nothing has been sent yet. ${CONTROL_ROOM_NOTE}` : "Nothing has been sent yet."
|
|
121891
|
+
}
|
|
121892
|
+
};
|
|
121893
|
+
}
|
|
121894
|
+
function controlPollRefusalResult(eventId, pollId, error3) {
|
|
121895
|
+
if (!error3 || typeof error3 !== "object")
|
|
121896
|
+
return null;
|
|
121897
|
+
const { code, message, details } = error3;
|
|
121898
|
+
if (typeof code !== "string" || !code.startsWith("functions/"))
|
|
121899
|
+
return null;
|
|
121900
|
+
const info = asRecord(details);
|
|
121901
|
+
const text = safeString(message);
|
|
121902
|
+
if (info.reason === "control_room_required") {
|
|
121903
|
+
return refusal5(eventId, pollId, {
|
|
121904
|
+
errorCode: "CONTROL_ROOM_REQUIRED",
|
|
121905
|
+
message: "OKO refused this command: launching a poll for everyone and resetting its count use the live control room, and this show has not bought it. Nothing was changed.",
|
|
121906
|
+
whatToDo: [
|
|
121907
|
+
`Tell the organizer: the control room of a show costs ${CONTROL_ROOM_LUMENS} lumens and needs the Pro plan or higher; they buy it with the "Enable control room" button in the event's analytics. No MCP tool buys it.`,
|
|
121908
|
+
"Offer what needs no control room: show_results (viewers see the bars), close, open, hide_results, stop \u2014 and the poll stays available from its button for everyone.",
|
|
121909
|
+
"Do not retry as is: the answer will not change until the control room is bought."
|
|
121910
|
+
]
|
|
121911
|
+
});
|
|
121912
|
+
}
|
|
121913
|
+
if (info.reason === "not_published") {
|
|
121914
|
+
return refusal5(eventId, pollId, {
|
|
121915
|
+
errorCode: "PAGE_NOT_PUBLISHED",
|
|
121916
|
+
message: "OKO refused this command: launching and resetting need a published page, because a draft has no viewers. Nothing was changed.",
|
|
121917
|
+
whatToDo: [
|
|
121918
|
+
"Publish the page first: oko_publish_event without confirm shows the bill, then again with confirm:true.",
|
|
121919
|
+
"Do not retry as is: the answer will not change until the page is published."
|
|
121920
|
+
]
|
|
121921
|
+
});
|
|
121922
|
+
}
|
|
121923
|
+
if (code === "functions/permission-denied") {
|
|
121924
|
+
if (/live permission/i.test(text))
|
|
121925
|
+
return livePermissionRejectionResult(TOOL3, { eventId });
|
|
121926
|
+
if (/scope|no longer grants/i.test(text))
|
|
121927
|
+
return scopeRejectionResult(TOOL3, "live:control");
|
|
121928
|
+
return null;
|
|
121929
|
+
}
|
|
121930
|
+
if (code === "functions/not-found") {
|
|
121931
|
+
return /poll/i.test(text) ? refusal5(eventId, pollId, {
|
|
121932
|
+
errorCode: "POLL_NOT_FOUND",
|
|
121933
|
+
message: "The page has no poll with this id (or the poll is not complete, so OKO does not serve it). Nothing was changed.",
|
|
121934
|
+
whatToDo: ["Read the polls with oko_get_event (polls[]) and repeat with a complete poll's id."]
|
|
121935
|
+
}) : refusal5(eventId, null, {
|
|
121936
|
+
errorCode: "EVENT_NOT_FOUND",
|
|
121937
|
+
message: "Event not found.",
|
|
121938
|
+
whatToDo: ["Check the id: oko_list_my_events lists the events this organizer can see."]
|
|
121939
|
+
});
|
|
121940
|
+
}
|
|
121941
|
+
if (code === "functions/failed-precondition" && /trash/i.test(text))
|
|
121942
|
+
return eventInTrashRefusal(eventId, TOOL3);
|
|
121943
|
+
if (code === "functions/aborted") {
|
|
121944
|
+
return refusal5(eventId, pollId, {
|
|
121945
|
+
errorCode: "POLL_CHANGED_AT_THE_SAME_MOMENT",
|
|
121946
|
+
message: "Another command reached the same poll at the same moment, so this one was not applied. Nothing was lost.",
|
|
121947
|
+
whatToDo: [
|
|
121948
|
+
"Read the poll's state with oko_get_live_state (polls[]) and repeat the command only if it is still needed."
|
|
121949
|
+
]
|
|
121950
|
+
});
|
|
121951
|
+
}
|
|
121952
|
+
return null;
|
|
121953
|
+
}
|
|
121954
|
+
function pollsLiveFrom(args) {
|
|
121955
|
+
const { event, states, tallies } = args;
|
|
121956
|
+
const raw = Array.isArray(event.polls) ? event.polls : [];
|
|
121957
|
+
const served = parseEventPolls(event.polls);
|
|
121958
|
+
const hints = [];
|
|
121959
|
+
if (served.length < Math.min(raw.length, 10)) {
|
|
121960
|
+
hints.push(`${Math.min(raw.length, 10) - served.length} poll(s) on the page are not served (incomplete, or a repeated id): they are not listed here. oko_validate_event_setup names them.`);
|
|
121961
|
+
}
|
|
121962
|
+
if (served.length === 0)
|
|
121963
|
+
return { polls: [], read: "none", hints };
|
|
121964
|
+
const nodes = states.kind === "read" && states.node && typeof states.node === "object" && !Array.isArray(states.node) ? states.node : {};
|
|
121965
|
+
let unreadable = 0;
|
|
121966
|
+
const polls = served.map((poll) => {
|
|
121967
|
+
const state2 = states.kind === "read" ? Object.hasOwn(nodes, poll.id) ? readPollState(nodes[poll.id]) : { ...DEFAULT_POLL_CTRL } : null;
|
|
121968
|
+
const tallyRead = tallies.get(poll.id);
|
|
121969
|
+
if (state2 === null || !tallyRead || tallyRead.kind === "unreadable") {
|
|
121970
|
+
unreadable += 1;
|
|
121971
|
+
return { pollId: poll.id, question: poll.question, multiple: poll.multiple, state: state2, tally: null };
|
|
121972
|
+
}
|
|
121973
|
+
const tally = readPollTally(tallyRead.node);
|
|
121974
|
+
const known = new Set(poll.options.map((option) => option.id));
|
|
121975
|
+
const removed = Object.entries(tally.options).filter(([id]) => !known.has(id)).map(([id, count]) => ({ id, count }));
|
|
121976
|
+
return {
|
|
121977
|
+
pollId: poll.id,
|
|
121978
|
+
question: poll.question,
|
|
121979
|
+
multiple: poll.multiple,
|
|
121980
|
+
state: state2,
|
|
121981
|
+
tally: {
|
|
121982
|
+
round: state2.round,
|
|
121983
|
+
total: tally.total,
|
|
121984
|
+
options: poll.options.map((option) => ({ id: option.id, text: option.text, count: Object.hasOwn(tally.options, option.id) ? tally.options[option.id] ?? 0 : 0 })),
|
|
121985
|
+
...removed.length > 0 ? { removedOptions: removed } : {}
|
|
121986
|
+
}
|
|
121987
|
+
};
|
|
121988
|
+
});
|
|
121989
|
+
const read = unreadable === 0 ? "ok" : unreadable === polls.length ? "unreadable" : "partial";
|
|
121990
|
+
if (read !== "ok") {
|
|
121991
|
+
const reasons = [
|
|
121992
|
+
...states.kind === "unreadable" ? [states.reason] : [],
|
|
121993
|
+
...[...tallies.values()].flatMap((item) => item.kind === "unreadable" ? [item.reason] : [])
|
|
121994
|
+
];
|
|
121995
|
+
hints.push(`${unreadable} of ${polls.length} poll(s) could not be read from the show (${[...new Set(reasons)].join("; ") || "no answer"}): their state or count is null, which is NOT zero. Repeat the call; do not report a count from a null.`);
|
|
121996
|
+
}
|
|
121997
|
+
hints.push("polls[].tally is the live count of the CURRENT round of each poll, shared by the whole page across sessions. The server raises it at every vote and recounts it from the votes at the end of a session, so it can be a vote short in between; the figures of one session are in oko_get_session_report.");
|
|
121998
|
+
return { polls, read, hints };
|
|
121999
|
+
}
|
|
122000
|
+
async function readPollsLive(eventId, event) {
|
|
122001
|
+
const served = parseEventPolls(event.polls);
|
|
122002
|
+
if (served.length === 0)
|
|
122003
|
+
return pollsLiveFrom({ event, states: { kind: "read", node: null }, tallies: /* @__PURE__ */ new Map() });
|
|
122004
|
+
let states;
|
|
122005
|
+
try {
|
|
122006
|
+
states = { kind: "read", node: (await get(ref(rtdb, `events/${eventId}/pollState`))).val() };
|
|
122007
|
+
} catch (error3) {
|
|
122008
|
+
states = { kind: "unreadable", reason: error3 instanceof Error ? error3.message : String(error3) };
|
|
122009
|
+
}
|
|
122010
|
+
const tallies = /* @__PURE__ */ new Map();
|
|
122011
|
+
if (states.kind === "read") {
|
|
122012
|
+
const nodes = states.node && typeof states.node === "object" && !Array.isArray(states.node) ? states.node : {};
|
|
122013
|
+
await Promise.all(served.map(async (poll) => {
|
|
122014
|
+
const round = Object.hasOwn(nodes, poll.id) ? readPollState(nodes[poll.id]).round : DEFAULT_POLL_CTRL.round;
|
|
122015
|
+
try {
|
|
122016
|
+
tallies.set(poll.id, { kind: "read", node: (await get(ref(rtdb, `events/${eventId}/pollTally/${poll.id}/${round}`))).val() });
|
|
122017
|
+
} catch (error3) {
|
|
122018
|
+
tallies.set(poll.id, { kind: "unreadable", reason: error3 instanceof Error ? error3.message : String(error3) });
|
|
122019
|
+
}
|
|
122020
|
+
}));
|
|
122021
|
+
}
|
|
122022
|
+
return pollsLiveFrom({ event, states, tallies });
|
|
122023
|
+
}
|
|
122024
|
+
async function readPollStateForPreview(eventId, pollId) {
|
|
122025
|
+
try {
|
|
122026
|
+
const node = (await get(ref(rtdb, `events/${eventId}/pollState/${pollId}`))).val();
|
|
122027
|
+
return { state: readPollState(node) };
|
|
122028
|
+
} catch (error3) {
|
|
122029
|
+
return { state: null, stateUnreadable: error3 instanceof Error ? error3.message : String(error3) };
|
|
122030
|
+
}
|
|
122031
|
+
}
|
|
122032
|
+
function pollCommandHints(action, state2) {
|
|
122033
|
+
const hints = [POLL_ACTION_EFFECT[action]];
|
|
122034
|
+
if (action === "reset")
|
|
122035
|
+
hints.push(`The poll is now in round ${state2.round}.`);
|
|
122036
|
+
if (action === "launch" || action === "open")
|
|
122037
|
+
hints.push("Voting is open.");
|
|
122038
|
+
return hints;
|
|
122039
|
+
}
|
|
122040
|
+
var callControlPoll = async (request) => {
|
|
122041
|
+
const call2 = httpsCallable(functions, "controlPoll");
|
|
122042
|
+
return (await call2(request)).data;
|
|
122043
|
+
};
|
|
122044
|
+
async function sendPollCommand(request, call2 = callControlPoll) {
|
|
122045
|
+
let answer;
|
|
122046
|
+
try {
|
|
122047
|
+
answer = await call2(request);
|
|
122048
|
+
} catch (error3) {
|
|
122049
|
+
const named = controlPollRefusalResult(request.eventId, request.pollId, error3);
|
|
122050
|
+
if (named)
|
|
122051
|
+
throw new ToolRefusal(named);
|
|
122052
|
+
throw error3;
|
|
122053
|
+
}
|
|
122054
|
+
return readPollState(answer.state);
|
|
122055
|
+
}
|
|
122056
|
+
function registerPollTools(server2) {
|
|
122057
|
+
server2.registerTool("oko_control_poll", {
|
|
122058
|
+
title: "Control a poll",
|
|
122059
|
+
description: toolDescription({
|
|
122060
|
+
summary: "Command one poll of an event during the show: close or reopen voting, show or hide the results to viewers, launch it as a popup for everyone, stop the popup, or reset the count (a new round). Without confirm nothing is sent: the call asks for confirmation and its preview says what viewers will get.",
|
|
122061
|
+
exampleCall: { eventId: "Kx9pQ2mR7tL4vB8nW3cY", pollId: "p-1b2c3d4e", action: "show_results", confirm: true },
|
|
122062
|
+
args: [
|
|
122063
|
+
"eventId: event whose poll to command.",
|
|
122064
|
+
"pollId: the poll's id (oko_get_event \u2192 polls[].id).",
|
|
122065
|
+
`action: ${POLL_ACTIONS.join(" | ")}.`,
|
|
122066
|
+
"confirm: must be true \u2014 every command changes what viewers see or can do right now."
|
|
122067
|
+
],
|
|
122068
|
+
returns: "eventId, pollId, action and the poll's state after the command (closed, live, liveAt, resultsShown, round), plus hints.",
|
|
122069
|
+
examples: [
|
|
122070
|
+
"Use show_results when the organizer says to show the results; hide_results to take them down.",
|
|
122071
|
+
"Use launch to pop the poll up for everyone (control room); reset to start a new round with the count at zero (control room).",
|
|
122072
|
+
"Read the current count with oko_get_live_state (polls[])."
|
|
122073
|
+
],
|
|
122074
|
+
notes: [
|
|
122075
|
+
`Needs the live:control scope and the right to control the show (control live) on the event. ${CONTROL_ROOM_NOTE}`,
|
|
122076
|
+
"close puts out a launched popup and stops votes (open lets them in again); show_results and hide_results decide whether VIEWERS see the bars \u2014 the organizer always sees the count; stop only puts the popup out.",
|
|
122077
|
+
"reset starts a new round: viewers may vote again and the count restarts at zero. Nothing is deleted \u2014 the votes of the earlier rounds stay in the data and in the session reports.",
|
|
122078
|
+
"Refusals carry errorCode and whatToDo: CONTROL_ROOM_REQUIRED, PAGE_NOT_PUBLISHED (launch and reset on a draft), POLL_NOT_FOUND, POLL_NOT_READY, POLL_CHANGED_AT_THE_SAME_MOMENT, LIVE_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH. Nothing was changed after any of them; relay them and do not retry as is."
|
|
122079
|
+
]
|
|
122080
|
+
}),
|
|
122081
|
+
inputSchema: {
|
|
122082
|
+
eventId: eventIdField,
|
|
122083
|
+
pollId: external_exports.string().regex(POLL_ID_PATTERN).describe("Id of a poll of the event (oko_get_event \u2192 polls[].id)."),
|
|
122084
|
+
action: external_exports.enum(POLL_ACTIONS).describe("The command."),
|
|
122085
|
+
confirm: external_exports.boolean().optional().describe("Required true to send the command.")
|
|
122086
|
+
},
|
|
122087
|
+
outputSchema: basicEventOutputSchema,
|
|
122088
|
+
// Not idempotent: a second `reset` is another round and a second `launch` is another popup (`liveAt` moves).
|
|
122089
|
+
annotations: writeTool
|
|
122090
|
+
}, async ({ eventId, pollId, action, confirm }) => {
|
|
122091
|
+
const { uid } = await authed(["live:control"]);
|
|
122092
|
+
const event = await readEvent(eventId, uid, "live");
|
|
122093
|
+
const plan = planPollCommand({ eventId, event, pollId, action, ...await readPollStateForPreview(eventId, pollId) });
|
|
122094
|
+
requireConfirm(confirm, "oko_control_poll", plan.preview);
|
|
122095
|
+
const state2 = await sendPollCommand({ eventId, pollId, action });
|
|
122096
|
+
return textResult({
|
|
122097
|
+
eventId,
|
|
122098
|
+
pollId,
|
|
122099
|
+
action,
|
|
122100
|
+
question: plan.poll.question,
|
|
122101
|
+
state: state2,
|
|
122102
|
+
hints: pollCommandHints(action, state2),
|
|
122103
|
+
...eventLinks(eventId)
|
|
122104
|
+
});
|
|
122105
|
+
});
|
|
122106
|
+
}
|
|
122107
|
+
|
|
121400
122108
|
// build/tools/live.js
|
|
121401
122109
|
var CHAT_LIVE_SETTINGS = ["mode", "slowModeMs", "automodEnabled", "allowViewerLinks"];
|
|
121402
122110
|
async function writeChatControl(eventId, mode) {
|
|
@@ -121408,7 +122116,7 @@ function registerLiveTools(server2) {
|
|
|
121408
122116
|
description: toolDescription({
|
|
121409
122117
|
summary: "Read main RTDB live controls, aggregate analytics, and chat settings for an event.",
|
|
121410
122118
|
args: ["eventId: event to inspect."],
|
|
121411
|
-
returns: "eventId, agg metrics, ctrl live overrides, chat settings (mode, slowModeMs, automodEnabled, allowViewerLinks, plus accepting \u2014 whether message intake is open right now, as the server enforces it), and analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read. ctrl.chatMode is the live control for message intake: public, off, or absent (then the event's starting state blocks.chat.intake applies and accepting shows the result). ctrl.<block>Visible is an override; when it is absent the block follows its setup default \u2014 oko_get_event \u2192 live gives the resolved visibleNow per block.",
|
|
122119
|
+
returns: "eventId, agg metrics, ctrl live overrides, chat settings (mode, slowModeMs, automodEnabled, allowViewerLinks, plus accepting \u2014 whether message intake is open right now, as the server enforces it), polls (one entry per poll the page serves: its state \u2014 closed, live, liveAt, resultsShown, round \u2014 and the live count of the current round per option; a figure that could not be read is null, never zero) with pollsRead ok|partial|unreadable|none, and analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read. ctrl.chatMode is the live control for message intake: public, off, or absent (then the event's starting state blocks.chat.intake applies and accepting shows the result). ctrl.<block>Visible is an override; when it is absent the block follows its setup default \u2014 oko_get_event \u2192 live gives the resolved visibleNow per block.",
|
|
121412
122120
|
examples: ["Use during a webinar before changing live block visibility or starting/ending sessions."],
|
|
121413
122121
|
notes: [
|
|
121414
122122
|
"Live state does not carry the event team: to see who has access to an event and with which permissions, use oko_list_workspace_access on the event's workspace."
|
|
@@ -121419,11 +122127,12 @@ function registerLiveTools(server2) {
|
|
|
121419
122127
|
annotations: readOnlyTool
|
|
121420
122128
|
}, async ({ eventId }) => {
|
|
121421
122129
|
const { uid } = await authed(["analytics:read"]);
|
|
121422
|
-
await readEvent(eventId, uid, "analytics");
|
|
121423
|
-
const [aggSnap, ctrlSnap, killSwitch, ...chatSnaps] = await Promise.all([
|
|
122130
|
+
const event = await readEvent(eventId, uid, "analytics");
|
|
122131
|
+
const [aggSnap, ctrlSnap, killSwitch, pollsLive, ...chatSnaps] = await Promise.all([
|
|
121424
122132
|
get(ref(rtdb, `events/${eventId}/agg`)),
|
|
121425
122133
|
get(ref(rtdb, `events/${eventId}/ctrl`)),
|
|
121426
122134
|
readKillSwitch(eventId),
|
|
122135
|
+
readPollsLive(eventId, event),
|
|
121427
122136
|
...CHAT_LIVE_SETTINGS.map((field2) => get(ref(chatRtdb, `settings/${eventId}/${field2}`)))
|
|
121428
122137
|
]);
|
|
121429
122138
|
const chat = {};
|
|
@@ -121439,12 +122148,15 @@ function registerLiveTools(server2) {
|
|
|
121439
122148
|
const killSwitchNote = killSwitchHint(killSwitch);
|
|
121440
122149
|
if (killSwitchNote)
|
|
121441
122150
|
liveStateHints.push(killSwitchNote);
|
|
122151
|
+
liveStateHints.push(...pollsLive.hints);
|
|
121442
122152
|
return textResult({
|
|
121443
122153
|
eventId,
|
|
121444
122154
|
agg: aggSnap.val() ?? {},
|
|
121445
122155
|
analyticsPaused: killSwitchFlag(killSwitch),
|
|
121446
122156
|
ctrl: ctrlSnap.val() ?? {},
|
|
121447
122157
|
chat,
|
|
122158
|
+
polls: pollsLive.polls,
|
|
122159
|
+
pollsRead: pollsLive.read,
|
|
121448
122160
|
hints: liveStateHints
|
|
121449
122161
|
});
|
|
121450
122162
|
});
|
|
@@ -121783,9 +122495,9 @@ async function assertSessionHasNothingToEnd(eventId, sessionId) {
|
|
|
121783
122495
|
read = { ok: false };
|
|
121784
122496
|
readFailure = error3 instanceof Error ? error3.message : String(error3);
|
|
121785
122497
|
}
|
|
121786
|
-
const
|
|
121787
|
-
if (
|
|
121788
|
-
throw new Error(readFailure ? `${
|
|
122498
|
+
const refusal7 = sessionRepairRefusal(sessionRepairAction({ liveSessionBaseline: sessionBaselineFromRead(read) }));
|
|
122499
|
+
if (refusal7)
|
|
122500
|
+
throw new Error(readFailure ? `${refusal7} (read failed: ${readFailure})` : refusal7);
|
|
121789
122501
|
}
|
|
121790
122502
|
async function attemptCleanup(path, run) {
|
|
121791
122503
|
try {
|
|
@@ -121923,9 +122635,9 @@ function extendSessionRefusalResult(eventId, error3) {
|
|
|
121923
122635
|
return null;
|
|
121924
122636
|
const info = details && typeof details === "object" ? details : {};
|
|
121925
122637
|
const text = safeString(message);
|
|
121926
|
-
const
|
|
122638
|
+
const refusal7 = (body) => ({ ...textResult({ tool: "oko_extend_session", eventId, ...body }), isError: true });
|
|
121927
122639
|
if (info.reason === "session_not_running") {
|
|
121928
|
-
return
|
|
122640
|
+
return refusal7({
|
|
121929
122641
|
errorCode: "SESSION_NOT_RUNNING",
|
|
121930
122642
|
message: text || "No analytics session is running on this page, so there is nothing to extend.",
|
|
121931
122643
|
whatToDo: [
|
|
@@ -121934,14 +122646,14 @@ function extendSessionRefusalResult(eventId, error3) {
|
|
|
121934
122646
|
});
|
|
121935
122647
|
}
|
|
121936
122648
|
if (info.reason === "planned_end_invalid") {
|
|
121937
|
-
return
|
|
122649
|
+
return refusal7({
|
|
121938
122650
|
errorCode: "PLANNED_END_INVALID",
|
|
121939
122651
|
message: text || "The new end must be a moment in the future.",
|
|
121940
122652
|
whatToDo: ["Pass endsAt later than now, or addMinutes to move the current end forward."]
|
|
121941
122653
|
});
|
|
121942
122654
|
}
|
|
121943
122655
|
if (info.reason === "outside_show_window") {
|
|
121944
|
-
return
|
|
122656
|
+
return refusal7({
|
|
121945
122657
|
errorCode: "OUTSIDE_SHOW_WINDOW",
|
|
121946
122658
|
message: text || "The show window of this page is over: a session can no longer be extended.",
|
|
121947
122659
|
whatToDo: ["End the session with oko_end_session; to run the show on another date, duplicate the page with oko_duplicate_event."]
|
|
@@ -122217,7 +122929,7 @@ function registerSessionTools(server2) {
|
|
|
122217
122929
|
"timelineLimit: max timeline buckets per session when includeTimeline=true.",
|
|
122218
122930
|
"includeSnapshots: include baseline/final snapshots; default false."
|
|
122219
122931
|
],
|
|
122220
|
-
returns: "sessions plus count, limit, hasMore/has_more, nextCursor/next_cursor, and analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read. Heavy timeline/snapshot fields are omitted unless requested.",
|
|
122932
|
+
returns: "sessions plus count, limit, hasMore/has_more, nextCursor/next_cursor, and analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read. Heavy timeline/snapshot fields are omitted unless requested. A session carries polls (the poll rounds voted in it) when viewers voted.",
|
|
122221
122933
|
examples: ["Use default options for dashboard-like summaries; request includeTimeline only for chart/debug tasks."]
|
|
122222
122934
|
}),
|
|
122223
122935
|
inputSchema: {
|
|
@@ -122280,7 +122992,10 @@ function registerSessionTools(server2) {
|
|
|
122280
122992
|
"includeSnapshots: include baseline/final snapshots; default false."
|
|
122281
122993
|
],
|
|
122282
122994
|
returns: "event/session ids, report URL, markdown summary, event summary, and compact session object. For a session that has not ended yet, also analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read.",
|
|
122283
|
-
examples: ["Use after a webinar when the user asks for the finished session report without manually opening the report page."]
|
|
122995
|
+
examples: ["Use after a webinar when the user asks for the finished session report without manually opening the report page."],
|
|
122996
|
+
notes: [
|
|
122997
|
+
"Polls: when viewers voted during the session, the markdown has a Polls section and session.polls lists each poll round with its options and counts (votes cast IN THIS SESSION; a round that ran through several sessions is split between their reports; shares are of the voters, so with several answers allowed they can pass 100%). dataQualityWarnings says when the vote count of a session failed (pollsUnmeasured): then an empty polls section means not measured, not nobody voted."
|
|
122998
|
+
]
|
|
122284
122999
|
}),
|
|
122285
123000
|
inputSchema: {
|
|
122286
123001
|
eventId: eventIdField,
|
|
@@ -122333,7 +123048,7 @@ function registerSessionTools(server2) {
|
|
|
122333
123048
|
|
|
122334
123049
|
// build/tools/telegramBilling.js
|
|
122335
123050
|
var TELEGRAM_POST_PRICE_LUMENS = 5;
|
|
122336
|
-
var
|
|
123051
|
+
var TOOL4 = "oko_publish_telegram_post";
|
|
122337
123052
|
function telegramPostCost(scheduled) {
|
|
122338
123053
|
return {
|
|
122339
123054
|
lumens: TELEGRAM_POST_PRICE_LUMENS,
|
|
@@ -122343,26 +123058,26 @@ function telegramPostCost(scheduled) {
|
|
|
122343
123058
|
when: scheduled ? "Scheduling is free. At the scheduled time the platform checks that the payer's plan includes Telegram posts and the wallet holds enough lumens; otherwise the post is refused then (deliveryError plan_required or no_lumens) and nothing is charged." : "The plan and the balance are checked before the send. A refused post is not sent and nothing is charged."
|
|
122344
123059
|
};
|
|
122345
123060
|
}
|
|
122346
|
-
function
|
|
122347
|
-
return { ...textResult({ tool:
|
|
123061
|
+
function refusal6(body) {
|
|
123062
|
+
return { ...textResult({ tool: TOOL4, ...body }), isError: true };
|
|
122348
123063
|
}
|
|
122349
|
-
function
|
|
123064
|
+
function isRecord6(value) {
|
|
122350
123065
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
122351
123066
|
}
|
|
122352
123067
|
function telegramPostRefusalResult(eventId, postId, error3, callerUid) {
|
|
122353
|
-
if (!
|
|
123068
|
+
if (!isRecord6(error3))
|
|
122354
123069
|
return null;
|
|
122355
123070
|
const { code, message, details } = error3;
|
|
122356
123071
|
if (typeof code !== "string" || !code.startsWith("functions/"))
|
|
122357
123072
|
return null;
|
|
122358
|
-
const info =
|
|
123073
|
+
const info = isRecord6(details) ? details : {};
|
|
122359
123074
|
const text = safeString(message);
|
|
122360
123075
|
const afterwards = "After the cause is fixed, create a new post with oko_publish_telegram_post and delete this failed one with oko_cancel_telegram_post (confirm:true) so the history shows one post.";
|
|
122361
123076
|
if (info.errorCode === "ENTITLEMENT_REQUIRED") {
|
|
122362
123077
|
const reason = typeof info.reason === "string" ? info.reason : null;
|
|
122363
123078
|
const inactive = reason === "expired" || reason === "grace_over" || reason === "period_ended";
|
|
122364
123079
|
const callerIsPayer = typeof info.payerUid === "string" && info.payerUid === callerUid;
|
|
122365
|
-
return
|
|
123080
|
+
return refusal6({
|
|
122366
123081
|
errorCode: "ENTITLEMENT_REQUIRED",
|
|
122367
123082
|
eventId,
|
|
122368
123083
|
postId,
|
|
@@ -122381,7 +123096,7 @@ function telegramPostRefusalResult(eventId, postId, error3, callerUid) {
|
|
|
122381
123096
|
if (info.errorCode === "INSUFFICIENT_TOKENS") {
|
|
122382
123097
|
const callerIsPayer = info.actorIsPayer === true || info.callerIsPayer === true;
|
|
122383
123098
|
const needed = typeof info.needed === "number" ? info.needed : TELEGRAM_POST_PRICE_LUMENS;
|
|
122384
|
-
return
|
|
123099
|
+
return refusal6({
|
|
122385
123100
|
errorCode: "INSUFFICIENT_TOKENS",
|
|
122386
123101
|
eventId,
|
|
122387
123102
|
postId,
|
|
@@ -122399,7 +123114,7 @@ function telegramPostRefusalResult(eventId, postId, error3, callerUid) {
|
|
|
122399
123114
|
});
|
|
122400
123115
|
}
|
|
122401
123116
|
if (typeof info.errorCode !== "string" && info.reason === "payer_unresolved") {
|
|
122402
|
-
return
|
|
123117
|
+
return refusal6({
|
|
122403
123118
|
errorCode: "PAYER_UNRESOLVED",
|
|
122404
123119
|
eventId,
|
|
122405
123120
|
postId,
|
|
@@ -122702,13 +123417,13 @@ function registerTelegramTools(server2) {
|
|
|
122702
123417
|
}
|
|
122703
123418
|
|
|
122704
123419
|
// build/tools/wallet.js
|
|
122705
|
-
var
|
|
123420
|
+
var TOOL5 = "oko_wallet";
|
|
122706
123421
|
var GRANTS_LIMIT = 50;
|
|
122707
123422
|
var LEDGER_MAX = 50;
|
|
122708
123423
|
var LEDGER_DEFAULT = 20;
|
|
122709
123424
|
var GRANT_BUCKETS = ["plan", "rollover", "topup"];
|
|
122710
123425
|
var LEDGER_KINDS = ["grant", "charge", "refund", "expire", "settlement"];
|
|
122711
|
-
function
|
|
123426
|
+
function isRecord7(value) {
|
|
122712
123427
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
122713
123428
|
}
|
|
122714
123429
|
function isSafeInt(value) {
|
|
@@ -122723,10 +123438,10 @@ function isoOrNull(value) {
|
|
|
122723
123438
|
return ms === null ? null : new Date(ms).toISOString();
|
|
122724
123439
|
}
|
|
122725
123440
|
function readWalletBalance(raw) {
|
|
122726
|
-
return
|
|
123441
|
+
return isRecord7(raw) && isSafeInt(raw.balance) ? raw.balance : null;
|
|
122727
123442
|
}
|
|
122728
123443
|
function grantForOutput(id, raw) {
|
|
122729
|
-
if (!
|
|
123444
|
+
if (!isRecord7(raw))
|
|
122730
123445
|
return null;
|
|
122731
123446
|
if (!GRANT_BUCKETS.includes(raw.bucket))
|
|
122732
123447
|
return null;
|
|
@@ -122745,13 +123460,13 @@ function grantForOutput(id, raw) {
|
|
|
122745
123460
|
};
|
|
122746
123461
|
}
|
|
122747
123462
|
function ledgerRowForOutput(id, raw) {
|
|
122748
|
-
if (!
|
|
123463
|
+
if (!isRecord7(raw))
|
|
122749
123464
|
return null;
|
|
122750
123465
|
if (!LEDGER_KINDS.includes(raw.kind))
|
|
122751
123466
|
return null;
|
|
122752
123467
|
if (!isSafeInt(raw.amount) || !isSafeInt(raw.delta) || !isSafeInt(raw.balanceAfter))
|
|
122753
123468
|
return null;
|
|
122754
|
-
const ref2 =
|
|
123469
|
+
const ref2 = isRecord7(raw.ref) ? raw.ref : {};
|
|
122755
123470
|
const text = (value) => typeof value === "string" && value ? value : null;
|
|
122756
123471
|
const sessionId = text(ref2.sessionId) ?? text(raw.sessionId);
|
|
122757
123472
|
const isSessionRow = raw.kind === "charge" && sessionId !== null;
|
|
@@ -122772,11 +123487,93 @@ function ledgerRowForOutput(id, raw) {
|
|
|
122772
123487
|
forgiven: isSessionRow ? readCount(raw.forgiven ?? ref2.forgiven) : null
|
|
122773
123488
|
};
|
|
122774
123489
|
}
|
|
123490
|
+
var PLAN_TIERS = ["core", "pro", "max", "legacy"];
|
|
123491
|
+
var PLAN_STATUSES = ["active", "past_due", "canceled_at_period_end", "expired"];
|
|
123492
|
+
var PLAN_SOURCES = ["lava", "operator", "legacy-exit", "transition-b", "smoke"];
|
|
123493
|
+
function momentMs(value) {
|
|
123494
|
+
if (value instanceof Date)
|
|
123495
|
+
return Number.isNaN(value.getTime()) ? null : value.getTime();
|
|
123496
|
+
if (isRecord7(value) && typeof value.toMillis === "function") {
|
|
123497
|
+
const ms = value.toMillis();
|
|
123498
|
+
return typeof ms === "number" && Number.isFinite(ms) ? ms : null;
|
|
123499
|
+
}
|
|
123500
|
+
return null;
|
|
123501
|
+
}
|
|
123502
|
+
function readOrganizerPlan(accessData) {
|
|
123503
|
+
if (!isRecord7(accessData))
|
|
123504
|
+
return { state: "unreadable" };
|
|
123505
|
+
const raw = accessData.plan;
|
|
123506
|
+
if (raw === void 0 || raw === null)
|
|
123507
|
+
return { state: "none" };
|
|
123508
|
+
if (!isRecord7(raw))
|
|
123509
|
+
return { state: "unreadable" };
|
|
123510
|
+
const tier = PLAN_TIERS.find((candidate) => candidate === raw.tier);
|
|
123511
|
+
const status = PLAN_STATUSES.find((candidate) => candidate === raw.status);
|
|
123512
|
+
if (!tier || !status)
|
|
123513
|
+
return { state: "unreadable" };
|
|
123514
|
+
const dates = { periodStart: null, periodEnd: null, graceUntil: null };
|
|
123515
|
+
for (const key of ["periodStart", "periodEnd", "graceUntil"]) {
|
|
123516
|
+
const value = raw[key];
|
|
123517
|
+
if (value === void 0 || value === null)
|
|
123518
|
+
continue;
|
|
123519
|
+
const ms = momentMs(value);
|
|
123520
|
+
if (ms === null)
|
|
123521
|
+
return { state: "unreadable" };
|
|
123522
|
+
dates[key] = ms;
|
|
123523
|
+
}
|
|
123524
|
+
const seats = raw.seatsIncluded;
|
|
123525
|
+
if (seats !== void 0 && seats !== null && !(Number.isInteger(seats) && seats > 0))
|
|
123526
|
+
return { state: "unreadable" };
|
|
123527
|
+
const iso2 = (ms) => ms === null ? null : new Date(ms).toISOString();
|
|
123528
|
+
return {
|
|
123529
|
+
state: "plan",
|
|
123530
|
+
plan: {
|
|
123531
|
+
tier,
|
|
123532
|
+
status,
|
|
123533
|
+
source: PLAN_SOURCES.find((candidate) => candidate === raw.source) ?? null,
|
|
123534
|
+
periodEnd: iso2(dates.periodEnd),
|
|
123535
|
+
graceUntil: iso2(dates.graceUntil)
|
|
123536
|
+
}
|
|
123537
|
+
};
|
|
123538
|
+
}
|
|
123539
|
+
function planAnswer(read) {
|
|
123540
|
+
const url = billingUrl();
|
|
123541
|
+
const reading = read.kind === "access" && read.exists ? readOrganizerPlan(read.data) : { state: "unreadable" };
|
|
123542
|
+
const standing = `This tool only reads. A plan or lumens are bought, changed or cancelled only in the OKO web cabinet (${url}): send the organizer there, and never ask for card details or a password.`;
|
|
123543
|
+
if (reading.state === "unreadable") {
|
|
123544
|
+
return {
|
|
123545
|
+
plan: null,
|
|
123546
|
+
planRead: "unreadable",
|
|
123547
|
+
hints: [
|
|
123548
|
+
`The plan could not be read (the platform refused the read, or the stored plan is malformed). Do not guess it: the organizer sees the real plan in the OKO web cabinet (${url}).`,
|
|
123549
|
+
standing
|
|
123550
|
+
]
|
|
123551
|
+
};
|
|
123552
|
+
}
|
|
123553
|
+
if (reading.state === "none") {
|
|
123554
|
+
return {
|
|
123555
|
+
plan: null,
|
|
123556
|
+
planRead: "ok",
|
|
123557
|
+
hints: [`This organizer has no plan: the paid features and spending lumens stay unavailable until a plan is bought in the OKO web cabinet (${url}). This tool cannot buy one.`, standing]
|
|
123558
|
+
};
|
|
123559
|
+
}
|
|
123560
|
+
const { plan } = reading;
|
|
123561
|
+
const hints = [];
|
|
123562
|
+
if (plan.status === "past_due") {
|
|
123563
|
+
hints.push(`The last renewal payment failed. The plan keeps working until graceUntil${plan.graceUntil ? ` (${plan.graceUntil})` : ""}; the organizer renews it in the OKO web cabinet (${url}) before then.`);
|
|
123564
|
+
} else if (plan.status === "canceled_at_period_end") {
|
|
123565
|
+
hints.push(`The subscription is cancelled: the plan works until periodEnd${plan.periodEnd ? ` (${plan.periodEnd})` : ""} and does not renew. Any change to it is made only in the OKO web cabinet (${url}).`);
|
|
123566
|
+
} else if (plan.status === "expired") {
|
|
123567
|
+
hints.push(`The plan has ended. A new plan is bought only in the OKO web cabinet (${url}).`);
|
|
123568
|
+
}
|
|
123569
|
+
hints.push(standing);
|
|
123570
|
+
return { plan, planRead: "ok", hints };
|
|
123571
|
+
}
|
|
122775
123572
|
function walletReadRefusal() {
|
|
122776
123573
|
return {
|
|
122777
123574
|
...textResult({
|
|
122778
123575
|
errorCode: "PLATFORM_REJECTED_READ",
|
|
122779
|
-
tool:
|
|
123576
|
+
tool: TOOL5,
|
|
122780
123577
|
message: "OKO's security rules refused to read this organizer's wallet although the key carries wallet:read. The organizer behind the key may be blocked or inactive, or the platform has not opened wallets to MCP keys yet.",
|
|
122781
123578
|
whatToDo: [
|
|
122782
123579
|
"Do not retry: rules do not change between attempts.",
|
|
@@ -122788,7 +123585,7 @@ function walletReadRefusal() {
|
|
|
122788
123585
|
};
|
|
122789
123586
|
}
|
|
122790
123587
|
function walletAnswer(input) {
|
|
122791
|
-
const { uid, walletExists, walletData, grantDocs, ledgerDocs, max } = input;
|
|
123588
|
+
const { uid, walletExists, walletData, grantDocs, ledgerDocs, max, planRead } = input;
|
|
122792
123589
|
let balance = null;
|
|
122793
123590
|
if (walletExists) {
|
|
122794
123591
|
balance = readWalletBalance(walletData);
|
|
@@ -122816,8 +123613,9 @@ function walletAnswer(input) {
|
|
|
122816
123613
|
unreadableLedgerRows += 1;
|
|
122817
123614
|
}
|
|
122818
123615
|
const last = page.at(-1);
|
|
122819
|
-
const lastAt =
|
|
123616
|
+
const lastAt = isRecord7(last?.data) ? last.data.at : void 0;
|
|
122820
123617
|
const nextCursor = hasMore ? timestampCursorFrom(lastAt, last?.id) : null;
|
|
123618
|
+
const planBlock = planRead ? planAnswer(planRead) : null;
|
|
122821
123619
|
const hints = [];
|
|
122822
123620
|
if (grantDocs !== null && !walletExists)
|
|
122823
123621
|
hints.push("This organizer has no wallet yet: nothing was ever granted. Plans and lumens are bought only in the OKO web cabinet.");
|
|
@@ -122825,8 +123623,12 @@ function walletAnswer(input) {
|
|
|
122825
123623
|
hints.push("Some rows did not read (unreadableGrants / unreadableLedgerRows): tell the organizer, the balance is still the wallet's own figure.");
|
|
122826
123624
|
if (hasMore && nextCursor === null)
|
|
122827
123625
|
hints.push("There are more ledger rows but the page boundary could not be named: the listing stops here.");
|
|
123626
|
+
if (planBlock)
|
|
123627
|
+
hints.push(...planBlock.hints);
|
|
122828
123628
|
return {
|
|
122829
123629
|
uid,
|
|
123630
|
+
billingUrl: billingUrl(),
|
|
123631
|
+
...planBlock ? { plan: planBlock.plan, planRead: planBlock.planRead } : {},
|
|
122830
123632
|
hasWallet: walletExists,
|
|
122831
123633
|
balance,
|
|
122832
123634
|
unit: "lumens",
|
|
@@ -122838,24 +123640,96 @@ function walletAnswer(input) {
|
|
|
122838
123640
|
hints
|
|
122839
123641
|
};
|
|
122840
123642
|
}
|
|
123643
|
+
async function readPlanDocument(uid) {
|
|
123644
|
+
try {
|
|
123645
|
+
const snap = await getDoc(doc(firestore, "organizerAccess", uid));
|
|
123646
|
+
return { kind: "access", exists: snap.exists(), data: snap.data() };
|
|
123647
|
+
} catch (error3) {
|
|
123648
|
+
if (isFirestorePermissionDenied(error3))
|
|
123649
|
+
return { kind: "refused" };
|
|
123650
|
+
throw error3;
|
|
123651
|
+
}
|
|
123652
|
+
}
|
|
123653
|
+
var WORKSPACE_TOOL = "oko_workspace_wallet";
|
|
123654
|
+
function workspaceWalletRefusal(workspaceId, body) {
|
|
123655
|
+
return { ...textResult({ tool: WORKSPACE_TOOL, workspaceId, ...body }), isError: true };
|
|
123656
|
+
}
|
|
123657
|
+
function workspaceWalletRefusalResult(workspaceId, error3) {
|
|
123658
|
+
if (!error3 || typeof error3 !== "object")
|
|
123659
|
+
return null;
|
|
123660
|
+
const { code, details } = error3;
|
|
123661
|
+
if (typeof code !== "string" || !code.startsWith("functions/"))
|
|
123662
|
+
return null;
|
|
123663
|
+
const reason = isRecord7(details) ? details.reason : null;
|
|
123664
|
+
if (reason === "wallet_view_forbidden") {
|
|
123665
|
+
return workspaceWalletRefusal(workspaceId, {
|
|
123666
|
+
errorCode: "WALLET_VIEW_FORBIDDEN",
|
|
123667
|
+
message: "OKO does not show this wallet to this organizer. The balance of the wallet that pays for a workspace is told only to the payer and to the members who may spend from it (create events, edit setup, control the live show, start sessions, send notifications).",
|
|
123668
|
+
whatToDo: [
|
|
123669
|
+
"Check the workspace id with oko_list_workspaces: it must be a workspace this organizer belongs to.",
|
|
123670
|
+
"If it is, ask the workspace owner for the right to spend from the workspace, or ask them for the balance. Do not guess a figure.",
|
|
123671
|
+
"Do not retry as is: rights do not change between attempts."
|
|
123672
|
+
]
|
|
123673
|
+
});
|
|
123674
|
+
}
|
|
123675
|
+
if (reason === "wallet_unreadable") {
|
|
123676
|
+
return workspaceWalletRefusal(workspaceId, {
|
|
123677
|
+
errorCode: "WALLET_UNREADABLE",
|
|
123678
|
+
message: "The balance could not be read just now. It is not zero: it is unknown.",
|
|
123679
|
+
whatToDo: [
|
|
123680
|
+
"Repeat the call in a moment. If it keeps failing, tell the organizer the balance is visible in the OKO cabinet under Billing.",
|
|
123681
|
+
"Do not report a balance and do not say the wallet is empty."
|
|
123682
|
+
]
|
|
123683
|
+
});
|
|
123684
|
+
}
|
|
123685
|
+
return null;
|
|
123686
|
+
}
|
|
123687
|
+
function workspaceWalletAnswer(workspaceId, answer) {
|
|
123688
|
+
const data = isRecord7(answer) ? answer : {};
|
|
123689
|
+
const hasWallet = data.hasWallet;
|
|
123690
|
+
const available = data.available;
|
|
123691
|
+
if (typeof hasWallet !== "boolean") {
|
|
123692
|
+
throw new Error("The server's answer about the workspace wallet is malformed (hasWallet is not a yes/no). This is a platform data fault: report it, do not present a balance.");
|
|
123693
|
+
}
|
|
123694
|
+
if (hasWallet && !isSafeInt(available)) {
|
|
123695
|
+
throw new Error("The workspace wallet exists but its balance is not a whole number. This is a platform data fault: report it, do not present the balance as zero.");
|
|
123696
|
+
}
|
|
123697
|
+
const payerIsCaller = data.payerIsCaller === true;
|
|
123698
|
+
const hints = [];
|
|
123699
|
+
if (!hasWallet)
|
|
123700
|
+
hints.push("The payer of this workspace has no wallet yet (nothing was ever granted): available is null, which is not zero. Plans and lumens are bought only in the OKO web cabinet.");
|
|
123701
|
+
if (!payerIsCaller)
|
|
123702
|
+
hints.push("This is the wallet of the workspace OWNER, who pays for the pages of this workspace. Only the number is told; the owner's plan, grants and ledger are theirs. Only the owner tops it up, in the OKO web cabinet.");
|
|
123703
|
+
hints.push(`This tool only reads. Plans and lumens are bought, changed and cancelled only in the OKO web cabinet (${billingUrl()}): send the organizer there, and never ask for card details or a password.`);
|
|
123704
|
+
return {
|
|
123705
|
+
workspaceId: safeString(data.workspaceId, workspaceId),
|
|
123706
|
+
hasWallet,
|
|
123707
|
+
available: hasWallet ? available : null,
|
|
123708
|
+
unit: "lumens",
|
|
123709
|
+
payerIsCaller,
|
|
123710
|
+
billingUrl: billingUrl(),
|
|
123711
|
+
hints
|
|
123712
|
+
};
|
|
123713
|
+
}
|
|
122841
123714
|
function registerWalletTools(server2) {
|
|
122842
123715
|
server2.registerTool("oko_wallet", {
|
|
122843
123716
|
title: "Read lumen wallet",
|
|
122844
123717
|
description: toolDescription({
|
|
122845
|
-
summary: "Read the lumen balance, the grants (buckets) and the ledger of the organizer's OWN
|
|
123718
|
+
summary: "Read the plan, the lumen balance, the grants (buckets) and the ledger of the organizer's OWN account, the one behind this key, with the link to the OKO cabinet's Billing page. Read-only: it cannot buy anything.",
|
|
122846
123719
|
args: [
|
|
122847
123720
|
"limit: ledger rows per page, default 20, max 50, newest first.",
|
|
122848
|
-
"cursor: nextCursor returned by the previous page. The grants come with the first page only; the balance with every page."
|
|
123721
|
+
"cursor: nextCursor returned by the previous page. The plan and the grants come with the first page only; the balance with every page."
|
|
122849
123722
|
],
|
|
122850
|
-
returns: 'hasWallet, balance (whole lumens, null when the organizer has no wallet yet), unit "lumens", grants on the first page (id, bucket plan|rollover|topup, amount, remaining, expiresAt, createdAt), ledger rows (id, kind grant|charge|refund|expire|settlement, amount, delta, balanceAfter, by, at, eventId, showId, workspaceId, postId, sessionId, minutes, forgiven), unreadableGrants/unreadableLedgerRows (rows that did not read: say so, never hide them) and cursor pagination metadata
|
|
123723
|
+
returns: `billingUrl (the cabinet's Billing page), plan on the first page (tier core|pro|max|legacy, status active|past_due|canceled_at_period_end|expired, source lava|operator|legacy-exit|transition-b|smoke or null, periodEnd and graceUntil as ISO times or null; plan is null when the organizer has no plan) with planRead ok|unreadable (unreadable: the plan could not be read, so null means unknown, not no plan), hasWallet, balance (whole lumens, null when the organizer has no wallet yet), unit "lumens", grants on the first page (id, bucket plan|rollover|topup, amount, remaining, expiresAt, createdAt), ledger rows (id, kind grant|charge|refund|expire|settlement, amount, delta, balanceAfter, by, at, eventId, showId, workspaceId, postId, sessionId, minutes, forgiven), unreadableGrants/unreadableLedgerRows (rows that did not read: say so, never hide them) and cursor pagination metadata.`,
|
|
122851
123724
|
examples: [
|
|
122852
123725
|
"Use before publishing a page or starting a session when the organizer asks how many lumens are left or what was charged.",
|
|
122853
123726
|
"Use to explain a charge or a refund: every ledger row names what paid (eventId, sessionId, postId) and the balance after it."
|
|
122854
123727
|
],
|
|
122855
123728
|
notes: [
|
|
122856
123729
|
"Needs the wallet:read scope. A key created before that scope existed gets SCOPE_REQUIRED and its scopes cannot be changed: the organizer creates a new key in the OKO cabinet.",
|
|
122857
|
-
"Shows only the wallet of the organizer behind the key. A page in a shared workspace is paid from the WORKSPACE OWNER's wallet: the publish, session and Telegram previews and refusals name that payer and tell its balance to whoever may spend from it (publish, start a session, send posts); the owner's grants and ledger are theirs to read, not this key's.",
|
|
122858
|
-
"This tool cannot buy, grant, top up or spend
|
|
123730
|
+
"Shows only the wallet of the organizer behind the key. A page in a shared workspace is paid from the WORKSPACE OWNER's wallet: the publish, session and Telegram previews and refusals name that payer and tell its balance to whoever may spend from it (publish, start a session, send posts); the owner's grants and ledger are theirs to read, not this key's. The balance of the wallet that pays for a workspace is one call away: oko_workspace_wallet.",
|
|
123731
|
+
"This tool cannot buy, change, cancel, grant, top up or spend anything. A plan and lumens are bought, changed and cancelled in the OKO web cabinet only (billingUrl): send the organizer there, and never ask for card data or a password.",
|
|
123732
|
+
"The plan shown is the organizer's own, the one behind this key. A paid action on a page of a shared workspace is judged by the PAYER's plan (the workspace owner's), which this key cannot read: the refusal of the paid tool names what is missing.",
|
|
122859
123733
|
"Refusals of the paid tools use the same codes as the web: ENTITLEMENT_REQUIRED (the plan does not include the feature) and INSUFFICIENT_TOKENS (not enough lumens); PLATFORM_REJECTED_READ here means the platform refused the read."
|
|
122860
123734
|
]
|
|
122861
123735
|
}),
|
|
@@ -122872,10 +123746,11 @@ function registerWalletTools(server2) {
|
|
|
122872
123746
|
const constraints = [orderBy("at", "desc"), limit(max + 1)];
|
|
122873
123747
|
if (cursor)
|
|
122874
123748
|
constraints.splice(1, 0, startAfter(...parseTimestampCursor(cursor)));
|
|
122875
|
-
const [walletSnap, grantsSnap, ledgerSnap] = await Promise.all([
|
|
123749
|
+
const [walletSnap, grantsSnap, ledgerSnap, planRead] = await Promise.all([
|
|
122876
123750
|
getDoc(doc(firestore, "wallets", uid)),
|
|
122877
123751
|
first ? getDocs(query2(collection(firestore, "wallets", uid, "grants"), orderBy("createdAt", "desc"), limit(GRANTS_LIMIT))) : Promise.resolve(null),
|
|
122878
|
-
getDocs(query2(collection(firestore, "wallets", uid, "ledger"), ...constraints))
|
|
123752
|
+
getDocs(query2(collection(firestore, "wallets", uid, "ledger"), ...constraints)),
|
|
123753
|
+
first ? readPlanDocument(uid) : Promise.resolve(null)
|
|
122879
123754
|
]);
|
|
122880
123755
|
return textResult(walletAnswer({
|
|
122881
123756
|
uid,
|
|
@@ -122883,7 +123758,8 @@ function registerWalletTools(server2) {
|
|
|
122883
123758
|
walletData: walletSnap.data(),
|
|
122884
123759
|
grantDocs: grantsSnap ? grantsSnap.docs.map((item) => ({ id: item.id, data: item.data() })) : null,
|
|
122885
123760
|
ledgerDocs: ledgerSnap.docs.map((item) => ({ id: item.id, data: item.data() })),
|
|
122886
|
-
max
|
|
123761
|
+
max,
|
|
123762
|
+
planRead
|
|
122887
123763
|
}));
|
|
122888
123764
|
} catch (error3) {
|
|
122889
123765
|
if (isFirestorePermissionDenied(error3))
|
|
@@ -122891,6 +123767,43 @@ function registerWalletTools(server2) {
|
|
|
122891
123767
|
throw error3;
|
|
122892
123768
|
}
|
|
122893
123769
|
});
|
|
123770
|
+
server2.registerTool("oko_workspace_wallet", {
|
|
123771
|
+
title: "Read a workspace's lumen balance",
|
|
123772
|
+
description: toolDescription({
|
|
123773
|
+
summary: "Read how many lumens the wallet that pays for a workspace holds \u2014 one number. For a shared workspace that is the WORKSPACE OWNER's wallet; for the organizer's personal workspace (its id is the organizer's own uid) it is their own. Read-only: it cannot buy anything.",
|
|
123774
|
+
exampleCall: { workspaceId: "yH4dN8sQ2wE6rT1uI9oP" },
|
|
123775
|
+
args: ["workspaceId: the workspace whose payer's balance to read (oko_list_workspaces)."],
|
|
123776
|
+
returns: `workspaceId, hasWallet, available (whole lumens; null when the payer has no wallet yet \u2014 not zero), unit "lumens", payerIsCaller (true: it is the organizer's own wallet), billingUrl and hints.`,
|
|
123777
|
+
examples: [
|
|
123778
|
+
"Use before publishing a page or starting a session in a shared workspace, to tell whether the wallet that pays can cover it.",
|
|
123779
|
+
"oko_wallet shows the organizer's OWN plan, grants and ledger; this tool shows the balance that pays for a workspace."
|
|
123780
|
+
],
|
|
123781
|
+
notes: [
|
|
123782
|
+
"Needs the wallet:read scope (presets operator and full). Allowed for the payer and for members who may spend from the payer's wallet (create events, edit setup, control the live show, start sessions, send notifications); anyone else, and a workspace that does not exist, get the same refusal WALLET_VIEW_FORBIDDEN.",
|
|
123783
|
+
"Only the number is told to a member. The owner's plan, grants and ledger stay the owner's: use oko_wallet for your own.",
|
|
123784
|
+
"An unreadable balance is the refusal WALLET_UNREADABLE, never zero.",
|
|
123785
|
+
"This tool cannot buy, change, cancel, grant, top up or spend anything. Plans and lumens are bought only in the OKO web cabinet (billingUrl): send the organizer there, and never ask for card data or a password."
|
|
123786
|
+
]
|
|
123787
|
+
}),
|
|
123788
|
+
inputSchema: {
|
|
123789
|
+
workspaceId: external_exports.string().min(1).max(128).regex(/^[^/]+$/, "A workspace id has no slash.").describe("Workspace id, as oko_list_workspaces lists it (a personal workspace's id is the organizer's uid).")
|
|
123790
|
+
},
|
|
123791
|
+
outputSchema: workspaceWalletOutputSchema,
|
|
123792
|
+
annotations: readOnlyTool
|
|
123793
|
+
}, async ({ workspaceId }) => {
|
|
123794
|
+
await authed(["wallet:read"]);
|
|
123795
|
+
const read = httpsCallable(functions, "readWorkspaceWallet");
|
|
123796
|
+
let answer;
|
|
123797
|
+
try {
|
|
123798
|
+
answer = (await read({ workspaceId })).data;
|
|
123799
|
+
} catch (error3) {
|
|
123800
|
+
const named = workspaceWalletRefusalResult(workspaceId, error3);
|
|
123801
|
+
if (named)
|
|
123802
|
+
throw new ToolRefusal(named);
|
|
123803
|
+
throw error3;
|
|
123804
|
+
}
|
|
123805
|
+
return textResult(workspaceWalletAnswer(workspaceId, answer));
|
|
123806
|
+
});
|
|
122894
123807
|
}
|
|
122895
123808
|
|
|
122896
123809
|
// build/tools/workspaceRefusal.js
|
|
@@ -122997,9 +123910,9 @@ function registerWorkspaceTools(server2) {
|
|
|
122997
123910
|
const created = await createWorkspace({ name: name7.trim() });
|
|
122998
123911
|
return textResult({ workspaceId: created.data.workspaceId, name: created.data.name ?? name7.trim(), created: true });
|
|
122999
123912
|
} catch (error3) {
|
|
123000
|
-
const
|
|
123001
|
-
if (
|
|
123002
|
-
throw new ToolRefusal(
|
|
123913
|
+
const refusal7 = workspaceRefusalResult("oko_create_workspace", error3);
|
|
123914
|
+
if (refusal7)
|
|
123915
|
+
throw new ToolRefusal(refusal7);
|
|
123003
123916
|
throw error3;
|
|
123004
123917
|
}
|
|
123005
123918
|
});
|
|
@@ -123134,12 +124047,12 @@ function registerWorkspaceTools(server2) {
|
|
|
123134
124047
|
...args.comment !== void 0 ? { comment: args.comment.trim() } : {}
|
|
123135
124048
|
});
|
|
123136
124049
|
} catch (error3) {
|
|
123137
|
-
const
|
|
124050
|
+
const refusal7 = workspaceRefusalResult("oko_update_workspace_member", error3, {
|
|
123138
124051
|
workspaceId: args.workspaceId,
|
|
123139
124052
|
uid: args.uid
|
|
123140
124053
|
});
|
|
123141
|
-
if (
|
|
123142
|
-
throw new ToolRefusal(
|
|
124054
|
+
if (refusal7)
|
|
124055
|
+
throw new ToolRefusal(refusal7);
|
|
123143
124056
|
throw error3;
|
|
123144
124057
|
}
|
|
123145
124058
|
const createdFields = ["permissions"];
|
|
@@ -123263,9 +124176,9 @@ function registerWorkspaceTools(server2) {
|
|
|
123263
124176
|
const result = await claimInvite({ code: code.trim().toUpperCase() });
|
|
123264
124177
|
return textResult({ ...serialize(result.data) });
|
|
123265
124178
|
} catch (error3) {
|
|
123266
|
-
const
|
|
123267
|
-
if (
|
|
123268
|
-
throw new ToolRefusal(
|
|
124179
|
+
const refusal7 = workspaceRefusalResult("oko_claim_workspace_invite", error3);
|
|
124180
|
+
if (refusal7)
|
|
124181
|
+
throw new ToolRefusal(refusal7);
|
|
123269
124182
|
throw error3;
|
|
123270
124183
|
}
|
|
123271
124184
|
});
|
|
@@ -123347,6 +124260,7 @@ function registerTools(server2) {
|
|
|
123347
124260
|
registerPublicationTools(server2);
|
|
123348
124261
|
registerUnpublicationTools(server2);
|
|
123349
124262
|
registerLiveTools(server2);
|
|
124263
|
+
registerPollTools(server2);
|
|
123350
124264
|
registerSessionTools(server2);
|
|
123351
124265
|
registerChatTools(server2);
|
|
123352
124266
|
registerLeadTools(server2);
|