@inetafrica/open-claudia 2.13.1 → 2.14.1
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/CHANGELOG.md +10 -0
- package/core/channel-wizard.js +169 -57
- package/core/config.js +7 -1
- package/core/handlers.js +1 -1
- package/core/kazee-probe.js +15 -6
- package/core/router.js +6 -0
- package/package.json +1 -1
- package/setup.js +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.14.1
|
|
4
|
+
- **The Kazee host URL is no longer asked — it's fixed infrastructure, like Telegram's `api.telegram.org`.** v2.14.0 fixed the probe but still made Step 1 ask the operator to type the host; the Kazee host never varies, so prompting for it is pure friction (and a chance to get it wrong). `core/config.js` now defines `KAZEE_DEFAULT_URL = "https://chat.inet.africa"` and `loadChannels` falls back to it whenever `KAZEE_URL` is unset. `/channel add kazee` is now **two steps, token-first**: Step 1 asks only for the bot token, Step 2 is the listen-to-capture owner flow. `setup.js` likewise drops its URL prompt (default inlined, since `config.js` exits before an `.env` exists). An explicit `KAZEE_URL` in `.env` still overrides, so nothing breaks for a deployment that set one.
|
|
5
|
+
- **Agent-space / OpenClaw:** no new deps, no new required env, no schema change; Docker image builds identically. Pure UX fix to the onboarding wizard.
|
|
6
|
+
|
|
7
|
+
## v2.14.0
|
|
8
|
+
- **`/channel add kazee` now finishes cleanly — the validation step was probing the wrong path.** The live adapter mounts every REST route under `{root}/api`, but the setup probe hit `{root}/me`. So the correct host (`https://chat.example.com`) always 404'd at validation, while the only URL that *passed* (`…/api`) then made the adapter build `…/api/api` and break. There was no single URL a user could type to succeed. `kazee-probe` now normalises either form to the root and probes the real `{root}/api/me` (new exported `normalizeKazeeRoot`); the wizard and `setup.js` both store the normalised root. Step 1 now asks for the host and states "I add the /api path myself."
|
|
9
|
+
- **The bot's own user id is no longer thrown away.** Validation already fetches it from `/me`, but the wizard never persisted `KAZEE_BOT_USER_ID` or passed it to the adapter — so a wizard-added channel booted without self-echo filtering or slash-command registration (and `loadChannels` warned on every restart). The wizard and `setup.js` now save it and hand it to the adapter.
|
|
10
|
+
- **Owner is captured by listening, not by pasting a Mongo id (Step 3 redesign).** After URL+token validate, the wizard brings Kazee **live immediately** (owner still blank) and asks the operator to send any message from their Kazee account. A new cross-channel hook — `router.tryCaptureOwner` → `channel-wizard.tryCaptureOwner` — intercepts that inbound Kazee message (it lands on a different channel than the operator's, so the operator-keyed `isAwaiting()` gate can't see it), then asks back on the operator's channel *"Is this you — the owner?"* with **Yes / No, keep listening / Skip** buttons. Yes grabs the sender's Kazee user id; owner recognition then flows through the existing env path (`identity.isConfiguredOwnerChannel` reads `KAZEE_OWNER_USER_ID`). Pasting an id or "skip" still works as a fallback; Cancel now tears the half-added adapter back down and rolls the `.env` edits back.
|
|
11
|
+
- **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. Purely a UX + correctness fix to the Kazee onboarding wizard — inert for deployments that never run `/channel add kazee`.
|
|
12
|
+
|
|
3
13
|
## v2.13.1
|
|
4
14
|
- **Guardrail no longer mistakes the owner for an external speaker on `/link`'d or env-unified channels (Phase 3 fix).** v2.13.0's relationship classifier resolved ownership through `people.findByHandle` alone — the handles physically listed on the owner's person record — while the queue/state/run-lock resolve it through `identity.canonicalForChannel`, which also honours explicit `/link`s (`identities.json`) and the configured-owner env path. A channel the owner had unified by `/link` or env-config (e.g. a second Kazee handle) was therefore *owner* to the run-lock (shared queue → "Queued.") but *external* to the guardrail (default-deny → the stranger reply), so the bot could greet its own owner as a limited-access outsider. The classifier now resolves ownership through **canonical identity — the same source of truth the rest of the bot uses**: a channel is the owner's when it resolves to the owner canonical **or** its person record is `isOwner`. New `identity.channelIsOwner()`; `relationship.speakerFor` consults it before default-deny. Every guard consumer (external-mode system-prompt block, `io`/`loopback` egress gates, runner turn-guard, CLI guards) funnels through this one function, so the fix lands everywhere at once.
|
|
5
15
|
- **R1 preserved — a note still can never confer owner.** `channelIsOwner` is driven only by the canonical map, which is written **exclusively by owner/operator actions** (`/link`, `.env`), never by a note; and the owner stays anchored to a real owner record via `ownerCanonical → people.isOwner`. The default-deny posture for genuinely external or unmapped channels is unchanged.
|
package/core/channel-wizard.js
CHANGED
|
@@ -5,10 +5,17 @@
|
|
|
5
5
|
// enters the wizard their next plain-text message becomes the wizard's
|
|
6
6
|
// input. A Cancel button (payload "chw:cancel") bails out at any step.
|
|
7
7
|
//
|
|
8
|
+
// Owner capture is cross-channel: once URL+token validate, we bring the
|
|
9
|
+
// Kazee adapter live and listen for an inbound message ON KAZEE, then ask
|
|
10
|
+
// the operator (back on the channel they ran /channel from) to confirm
|
|
11
|
+
// "is this you?". router.tryCaptureOwner() feeds that inbound message in —
|
|
12
|
+
// it arrives on a different channel than the operator's, so isAwaiting()
|
|
13
|
+
// (keyed to the operator) never sees it via handleText().
|
|
14
|
+
//
|
|
8
15
|
// validateKazee() is exported separately so setup.js can reuse the same
|
|
9
16
|
// /me probe without going through the chat-driven flow.
|
|
10
17
|
|
|
11
|
-
const { saveEnvKey, config } = require("./config");
|
|
18
|
+
const { saveEnvKey, config, KAZEE_DEFAULT_URL } = require("./config");
|
|
12
19
|
const { send, deleteMessage } = require("./io");
|
|
13
20
|
const { publicCommands } = require("./commands");
|
|
14
21
|
const registry = require("./adapter-registry");
|
|
@@ -31,22 +38,46 @@ function endSession(canonicalUserId) {
|
|
|
31
38
|
|
|
32
39
|
const cancelKeyboard = { inline_keyboard: [[{ text: "Cancel", callback_data: "chw:cancel" }]] };
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
// Prompts issued while listening for (and confirming) the owner's Kazee
|
|
42
|
+
// message must land on the channel the operator ran /channel from — not the
|
|
43
|
+
// Kazee chat a trigger arrived on. Reach it through the stored origin
|
|
44
|
+
// adapter, bypassing the ambient io.send() scope.
|
|
45
|
+
async function sayToOrigin(session, text, opts) {
|
|
46
|
+
try {
|
|
47
|
+
if (session?.origin?.adapter && session.origin.channelId) {
|
|
48
|
+
return await session.origin.adapter.send(session.origin.channelId, text, opts);
|
|
49
|
+
}
|
|
50
|
+
} catch (e) { console.error("channel-wizard sayToOrigin:", e.message); }
|
|
51
|
+
return send(text, opts); // fallback: ambient scope
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function start(canonicalUserId, kind, envelope) {
|
|
35
55
|
if (kind !== "kazee") {
|
|
36
56
|
await send(`Channel type "${kind}" not supported yet.`);
|
|
37
57
|
return;
|
|
38
58
|
}
|
|
39
|
-
|
|
59
|
+
const origin = envelope?.adapter && envelope?.channelId
|
|
60
|
+
? { adapter: envelope.adapter, channelId: envelope.channelId }
|
|
61
|
+
: null;
|
|
62
|
+
const url = config.KAZEE_URL || KAZEE_DEFAULT_URL;
|
|
63
|
+
sessions.set(canonicalUserId, { kind: "kazee", step: "awaiting-token", data: { url }, origin });
|
|
40
64
|
await send(
|
|
41
|
-
"Add a Kazee channel.\n\nStep 1 of
|
|
65
|
+
"Add a Kazee channel.\n\nStep 1 of 2: paste the Kazee bot token. I'll delete your message after reading it.",
|
|
42
66
|
{ keyboard: cancelKeyboard },
|
|
43
67
|
);
|
|
44
68
|
}
|
|
45
69
|
|
|
46
70
|
async function cancel(canonicalUserId, { silent } = {}) {
|
|
47
|
-
|
|
71
|
+
const session = sessions.get(canonicalUserId);
|
|
72
|
+
if (!session) return;
|
|
48
73
|
sessions.delete(canonicalUserId);
|
|
49
|
-
|
|
74
|
+
// If we already brought the adapter live for owner-capture, tear it back
|
|
75
|
+
// down and roll the .env edits back so a cancel leaves no half-added channel.
|
|
76
|
+
if (session.data?.live) {
|
|
77
|
+
try { await registry.removeAdapter("kazee"); } catch (e) {}
|
|
78
|
+
revertEnv(session.data.previous);
|
|
79
|
+
}
|
|
80
|
+
if (!silent) await sayToOrigin(session, "Channel setup cancelled.");
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
async function handleText(envelope) {
|
|
@@ -54,20 +85,6 @@ async function handleText(envelope) {
|
|
|
54
85
|
if (!session) return;
|
|
55
86
|
const text = (envelope.text || "").trim();
|
|
56
87
|
|
|
57
|
-
if (session.step === "awaiting-url") {
|
|
58
|
-
if (!/^https?:\/\//i.test(text)) {
|
|
59
|
-
await send("That doesn't look like a URL. Try again, or hit Cancel.", { keyboard: cancelKeyboard });
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
session.data.url = text.replace(/\/$/, "");
|
|
63
|
-
session.step = "awaiting-token";
|
|
64
|
-
await send(
|
|
65
|
-
"Step 2 of 3: Kazee bot token. I'll delete your message after reading.",
|
|
66
|
-
{ keyboard: cancelKeyboard },
|
|
67
|
-
);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
88
|
if (session.step === "awaiting-token") {
|
|
72
89
|
await deleteMessage(envelope.messageId);
|
|
73
90
|
if (!text) {
|
|
@@ -83,83 +100,177 @@ async function handleText(envelope) {
|
|
|
83
100
|
await send(`Validation failed: ${check.error}\n\nRun /channel add kazee to try again.`);
|
|
84
101
|
return;
|
|
85
102
|
}
|
|
103
|
+
session.data.url = check.url || session.data.url.replace(/\/+$/, "");
|
|
86
104
|
session.data.botUserId = check.botUserId || "";
|
|
87
|
-
|
|
105
|
+
|
|
106
|
+
// Bring the adapter live now so it can receive the owner's message.
|
|
107
|
+
const live = await bringLive(session);
|
|
108
|
+
if (!live.ok) {
|
|
109
|
+
sessions.delete(envelope.canonicalUserId);
|
|
110
|
+
await send(`Could not start the Kazee adapter: ${live.error}\n\nReverted .env. Run /channel add kazee to try again.`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
session.step = "awaiting-owner-message";
|
|
88
114
|
await send(
|
|
89
|
-
|
|
115
|
+
"Connected to Kazee ✅\n\nStep 2 of 2: open Kazee and send me any message — I'll grab your account and confirm it here.\n\n(Or send \"skip\" to leave the owner unset.)",
|
|
90
116
|
{ keyboard: cancelKeyboard },
|
|
91
117
|
);
|
|
92
118
|
return;
|
|
93
119
|
}
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
121
|
+
// Text fallback for step 3 — the operator can still type a user id or
|
|
122
|
+
// "skip" on their own channel instead of sending a Kazee message.
|
|
123
|
+
if (session.step === "awaiting-owner-message" || session.step === "confirming-owner") {
|
|
124
|
+
if (text.toLowerCase() === "skip") {
|
|
125
|
+
await finalizeOwner(envelope.canonicalUserId, session, "");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (/^[a-f0-9]{24}$/i.test(text)) {
|
|
129
|
+
await finalizeOwner(envelope.canonicalUserId, session, text);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (session.step === "awaiting-owner-message") {
|
|
133
|
+
await send(
|
|
134
|
+
"Still listening. Send a message from your Kazee account, paste a user id, or send \"skip\".",
|
|
135
|
+
{ keyboard: cancelKeyboard },
|
|
136
|
+
);
|
|
137
|
+
}
|
|
100
138
|
return;
|
|
101
139
|
}
|
|
102
140
|
}
|
|
103
141
|
|
|
142
|
+
// Fired by router.tryCaptureOwner() for inbound messages arriving ON the
|
|
143
|
+
// freshly-added Kazee adapter while a session is listening. Returns true
|
|
144
|
+
// when it consumed the message (so the router stops normal processing).
|
|
145
|
+
async function tryCaptureOwner(envelope) {
|
|
146
|
+
const adapterId = envelope.adapter?.id;
|
|
147
|
+
if (!adapterId) return false;
|
|
148
|
+
for (const [, session] of sessions) {
|
|
149
|
+
if (session.step !== "awaiting-owner-message") continue;
|
|
150
|
+
if (session.data.adapterId !== adapterId) continue;
|
|
151
|
+
const userId = String(envelope.userId || envelope.from?.id || "");
|
|
152
|
+
if (!userId) return true; // consume malformed input, keep listening
|
|
153
|
+
const name = envelope.from?.name || envelope.from?.username || "";
|
|
154
|
+
session.data.candidate = { userId, name };
|
|
155
|
+
session.step = "confirming-owner";
|
|
156
|
+
// Light ack on the Kazee side so the sender knows it landed.
|
|
157
|
+
try { await envelope.adapter.send(envelope.channelId, "Got your message 👋 — finishing setup on my end."); } catch (e) {}
|
|
158
|
+
const who = name ? `${name} (${userId})` : userId;
|
|
159
|
+
await sayToOrigin(
|
|
160
|
+
session,
|
|
161
|
+
`Got a Kazee message from ${who}.\n\nIs this you — the owner?`,
|
|
162
|
+
{ keyboard: { inline_keyboard: [
|
|
163
|
+
[{ text: "Yes, that's me", callback_data: "chw:owner-yes" }],
|
|
164
|
+
[{ text: "No, keep listening", callback_data: "chw:owner-no" }],
|
|
165
|
+
[{ text: "Skip (no owner)", callback_data: "chw:owner-skip" }],
|
|
166
|
+
] } },
|
|
167
|
+
);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
104
173
|
async function handleAction(envelope) {
|
|
105
174
|
const payload = envelope.action?.payload || envelope.action?.buttonId || "";
|
|
106
|
-
if (payload
|
|
107
|
-
|
|
175
|
+
if (!String(payload).startsWith("chw:")) return false;
|
|
176
|
+
const session = sessions.get(envelope.canonicalUserId);
|
|
177
|
+
|
|
178
|
+
if (payload === "chw:cancel") {
|
|
179
|
+
await cancel(envelope.canonicalUserId);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (!session) return true; // stale button — swallow so it doesn't fall through
|
|
183
|
+
|
|
184
|
+
if (payload === "chw:owner-yes") {
|
|
185
|
+
const cand = session.data.candidate;
|
|
186
|
+
if (!cand?.userId) {
|
|
187
|
+
session.step = "awaiting-owner-message";
|
|
188
|
+
await sayToOrigin(session, "Lost that candidate — send another message from Kazee.", { keyboard: cancelKeyboard });
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
await finalizeOwner(envelope.canonicalUserId, session, cand.userId);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (payload === "chw:owner-no") {
|
|
195
|
+
session.data.candidate = null;
|
|
196
|
+
session.step = "awaiting-owner-message";
|
|
197
|
+
await sayToOrigin(session, "OK — still listening. Send a message from the owner's Kazee account.", { keyboard: cancelKeyboard });
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (payload === "chw:owner-skip") {
|
|
201
|
+
await finalizeOwner(envelope.canonicalUserId, session, "");
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
108
204
|
return true;
|
|
109
205
|
}
|
|
110
206
|
|
|
111
|
-
|
|
112
|
-
const { url, token, ownerUserId } = session.data;
|
|
207
|
+
// ── env plumbing ────────────────────────────────────────────────────
|
|
113
208
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const previous = {
|
|
209
|
+
function snapshotEnv() {
|
|
210
|
+
return {
|
|
117
211
|
KAZEE_URL: config.KAZEE_URL || "",
|
|
118
212
|
KAZEE_BOT_TOKEN: config.KAZEE_BOT_TOKEN || "",
|
|
213
|
+
KAZEE_BOT_USER_ID: config.KAZEE_BOT_USER_ID || "",
|
|
119
214
|
KAZEE_OWNER_USER_ID: config.KAZEE_OWNER_USER_ID || "",
|
|
120
215
|
CHANNELS: config.CHANNELS || "telegram",
|
|
121
216
|
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function revertEnv(previous) {
|
|
220
|
+
if (!previous) return;
|
|
221
|
+
for (const [k, v] of Object.entries(previous)) {
|
|
222
|
+
saveEnvKey(k, v);
|
|
223
|
+
config[k] = v;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Persist URL/token/botUserId, add "kazee" to CHANNELS, and boot the adapter
|
|
228
|
+
// (owner still blank). Snapshots the prior env for rollback on failure/cancel.
|
|
229
|
+
async function bringLive(session) {
|
|
230
|
+
const { url, token, botUserId } = session.data;
|
|
231
|
+
const previous = snapshotEnv();
|
|
232
|
+
session.data.previous = previous;
|
|
122
233
|
|
|
123
234
|
saveEnvKey("KAZEE_URL", url);
|
|
124
235
|
saveEnvKey("KAZEE_BOT_TOKEN", token);
|
|
125
|
-
saveEnvKey("
|
|
126
|
-
|
|
236
|
+
saveEnvKey("KAZEE_BOT_USER_ID", botUserId);
|
|
127
237
|
const channels = previous.CHANNELS.split(",").map((s) => s.trim()).filter(Boolean);
|
|
128
|
-
if (!channels.some((c) => c === "kazee" || c.startsWith("kazee:")))
|
|
129
|
-
channels.push("kazee");
|
|
130
|
-
}
|
|
238
|
+
if (!channels.some((c) => c === "kazee" || c.startsWith("kazee:"))) channels.push("kazee");
|
|
131
239
|
const newChannels = channels.join(",");
|
|
132
240
|
saveEnvKey("CHANNELS", newChannels);
|
|
133
241
|
|
|
134
|
-
// Refresh in-process view so subsequent loadChannels() sees the new
|
|
135
|
-
// values without a restart.
|
|
136
242
|
config.KAZEE_URL = url;
|
|
137
243
|
config.KAZEE_BOT_TOKEN = token;
|
|
138
|
-
config.
|
|
244
|
+
config.KAZEE_BOT_USER_ID = botUserId;
|
|
139
245
|
config.CHANNELS = newChannels;
|
|
140
246
|
|
|
141
247
|
const result = await registry.addAdapter(
|
|
142
|
-
{ id: "kazee", type: "kazee", opts: { url, token, ownerUserId } },
|
|
248
|
+
{ id: "kazee", type: "kazee", opts: { url, token, ownerUserId: "", botUserId } },
|
|
143
249
|
{ publicCommands: publicCommands() },
|
|
144
250
|
);
|
|
145
|
-
|
|
146
251
|
if (!result.ok) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
saveEnvKey("KAZEE_BOT_TOKEN", previous.KAZEE_BOT_TOKEN);
|
|
150
|
-
saveEnvKey("KAZEE_OWNER_USER_ID", previous.KAZEE_OWNER_USER_ID);
|
|
151
|
-
saveEnvKey("CHANNELS", previous.CHANNELS);
|
|
152
|
-
config.KAZEE_URL = previous.KAZEE_URL;
|
|
153
|
-
config.KAZEE_BOT_TOKEN = previous.KAZEE_BOT_TOKEN;
|
|
154
|
-
config.KAZEE_OWNER_USER_ID = previous.KAZEE_OWNER_USER_ID;
|
|
155
|
-
config.CHANNELS = previous.CHANNELS;
|
|
156
|
-
sessions.delete(canonicalUserId);
|
|
157
|
-
await send(`Could not start the Kazee adapter: ${result.error}\n\nReverted .env. Run /channel add kazee to try again.`);
|
|
158
|
-
return;
|
|
252
|
+
revertEnv(previous);
|
|
253
|
+
return result;
|
|
159
254
|
}
|
|
255
|
+
session.data.adapterId = "kazee";
|
|
256
|
+
session.data.live = true;
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
160
259
|
|
|
260
|
+
// Owner decided (id or blank). Persist it, flip the live adapter's owner,
|
|
261
|
+
// and close the session.
|
|
262
|
+
async function finalizeOwner(canonicalUserId, session, ownerUserId) {
|
|
263
|
+
saveEnvKey("KAZEE_OWNER_USER_ID", ownerUserId);
|
|
264
|
+
config.KAZEE_OWNER_USER_ID = ownerUserId;
|
|
265
|
+
const adapter = registry.findAdapter("kazee");
|
|
266
|
+
if (adapter) adapter.ownerUserId = ownerUserId;
|
|
161
267
|
sessions.delete(canonicalUserId);
|
|
162
|
-
await
|
|
268
|
+
await sayToOrigin(
|
|
269
|
+
session,
|
|
270
|
+
ownerUserId
|
|
271
|
+
? "Kazee channel added and live. You're set as the owner — message me from Kazee and I'll know it's you. 🎉"
|
|
272
|
+
: "Kazee channel added and live. No owner set — run /channel add kazee again if you want to set one.",
|
|
273
|
+
);
|
|
163
274
|
}
|
|
164
275
|
|
|
165
276
|
module.exports = {
|
|
@@ -170,5 +281,6 @@ module.exports = {
|
|
|
170
281
|
cancel,
|
|
171
282
|
handleText,
|
|
172
283
|
handleAction,
|
|
284
|
+
tryCaptureOwner,
|
|
173
285
|
validateKazee,
|
|
174
286
|
};
|
package/core/config.js
CHANGED
|
@@ -163,6 +163,11 @@ const FULL_PATH = [
|
|
|
163
163
|
),
|
|
164
164
|
].filter(Boolean).join(path.delimiter);
|
|
165
165
|
|
|
166
|
+
// Kazee runs on one fixed host — like Telegram's api.telegram.org it's
|
|
167
|
+
// infrastructure, not a per-user setting. Hard-coded so onboarding only asks
|
|
168
|
+
// for the bot token (the actual secret). Override with KAZEE_URL if ever needed.
|
|
169
|
+
const KAZEE_DEFAULT_URL = "https://chat.inet.africa";
|
|
170
|
+
|
|
166
171
|
// Channels: declarative list of {id, type, opts}. Defaults to telegram for
|
|
167
172
|
// backwards compat with installs predating CHANNELS.
|
|
168
173
|
function loadChannels() {
|
|
@@ -185,7 +190,7 @@ function loadChannels() {
|
|
|
185
190
|
opts: { token: TOKEN, ownerChatId: CHAT_ID || "", chatIds: CHAT_IDS },
|
|
186
191
|
});
|
|
187
192
|
} else if (type === "kazee") {
|
|
188
|
-
const url = config.KAZEE_URL;
|
|
193
|
+
const url = config.KAZEE_URL || KAZEE_DEFAULT_URL;
|
|
189
194
|
const token = config.KAZEE_BOT_TOKEN;
|
|
190
195
|
const ownerUserId = config.KAZEE_OWNER_USER_ID || "";
|
|
191
196
|
const botUserId = config.KAZEE_BOT_USER_ID || "";
|
|
@@ -238,6 +243,7 @@ module.exports = {
|
|
|
238
243
|
saveEnvKey,
|
|
239
244
|
loadChannels,
|
|
240
245
|
botSubprocessEnv,
|
|
246
|
+
KAZEE_DEFAULT_URL,
|
|
241
247
|
BOT_DIR,
|
|
242
248
|
CONFIG_DIR,
|
|
243
249
|
TOKEN, CHAT_IDS, CHAT_ID,
|
package/core/handlers.js
CHANGED
|
@@ -1664,7 +1664,7 @@ register({
|
|
|
1664
1664
|
if (addMatch) {
|
|
1665
1665
|
const kind = addMatch[1].toLowerCase();
|
|
1666
1666
|
if (kind !== "kazee") return send(`Channel type "${kind}" not supported.`);
|
|
1667
|
-
await wizard.start(env.canonicalUserId, "kazee");
|
|
1667
|
+
await wizard.start(env.canonicalUserId, "kazee", env);
|
|
1668
1668
|
return;
|
|
1669
1669
|
}
|
|
1670
1670
|
|
package/core/kazee-probe.js
CHANGED
|
@@ -6,10 +6,19 @@ const https = require("https");
|
|
|
6
6
|
const http = require("http");
|
|
7
7
|
const { URL } = require("url");
|
|
8
8
|
|
|
9
|
+
// The adapter mounts every REST route under {root}/api and talks socket.io
|
|
10
|
+
// at {root}. Callers may hand us either the bare root host or a URL that
|
|
11
|
+
// already ends in /api — normalise both to the root so we always probe the
|
|
12
|
+
// real {root}/api/me and the caller can store one canonical KAZEE_URL.
|
|
13
|
+
function normalizeKazeeRoot(url) {
|
|
14
|
+
return String(url || "").trim().replace(/\/+$/, "").replace(/\/api$/i, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
function validateKazee({ url, token }) {
|
|
10
18
|
return new Promise((resolve) => {
|
|
19
|
+
const root = normalizeKazeeRoot(url);
|
|
11
20
|
let u;
|
|
12
|
-
try { u = new URL(
|
|
21
|
+
try { u = new URL(root + "/api/me"); }
|
|
13
22
|
catch (e) { return resolve({ ok: false, error: `Bad URL: ${e.message}` }); }
|
|
14
23
|
const lib = u.protocol === "https:" ? https : http;
|
|
15
24
|
const req = lib.request({
|
|
@@ -26,16 +35,16 @@ function validateKazee({ url, token }) {
|
|
|
26
35
|
try {
|
|
27
36
|
const body = chunks ? JSON.parse(chunks) : {};
|
|
28
37
|
const botUserId = body?.user?._id || body?.user?.id || body?._id || "";
|
|
29
|
-
resolve({ ok: true, botUserId });
|
|
38
|
+
resolve({ ok: true, botUserId, url: root });
|
|
30
39
|
} catch (e) {
|
|
31
|
-
resolve({ ok: true, botUserId: "" });
|
|
40
|
+
resolve({ ok: true, botUserId: "", url: root });
|
|
32
41
|
}
|
|
33
42
|
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
|
34
43
|
resolve({ ok: false, error: "Token rejected (401/403). Check that it's a valid Kazee bot token." });
|
|
35
44
|
} else if (res.statusCode === 404) {
|
|
36
|
-
resolve({ ok: false, error: "GET /me returned 404. Check
|
|
45
|
+
resolve({ ok: false, error: "GET /api/me returned 404. Check the URL points at your Kazee host (e.g. https://chat.example.com)." });
|
|
37
46
|
} else {
|
|
38
|
-
resolve({ ok: false, error: `GET /me → HTTP ${res.statusCode}` });
|
|
47
|
+
resolve({ ok: false, error: `GET /api/me → HTTP ${res.statusCode}` });
|
|
39
48
|
}
|
|
40
49
|
});
|
|
41
50
|
});
|
|
@@ -45,4 +54,4 @@ function validateKazee({ url, token }) {
|
|
|
45
54
|
});
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
module.exports = { validateKazee };
|
|
57
|
+
module.exports = { validateKazee, normalizeKazeeRoot };
|
package/core/router.js
CHANGED
|
@@ -63,6 +63,12 @@ async function onMessage(envelope) {
|
|
|
63
63
|
// owner's canonical bucket on first contact, before any state is read.
|
|
64
64
|
try { autoLinkOwnerChannel(envelope.adapter?.type, envelope.channelId); } catch (e) {}
|
|
65
65
|
|
|
66
|
+
// A channel wizard listening for the owner's message on a freshly-added
|
|
67
|
+
// adapter consumes that inbound message here: it arrives on a different
|
|
68
|
+
// channel than the operator's, so the isAwaiting() gate below (keyed to
|
|
69
|
+
// the operator) never sees it.
|
|
70
|
+
if (await channelWizard.tryCaptureOwner(envelope)) return;
|
|
71
|
+
|
|
66
72
|
// While a channel wizard is open, capture text input before slash
|
|
67
73
|
// dispatch — except for /cancel, which always bails out.
|
|
68
74
|
if (channelWizard.isAwaiting(envelope.canonicalUserId)) {
|
package/package.json
CHANGED
package/setup.js
CHANGED
|
@@ -492,6 +492,10 @@ const STEPS = ["prerequisites", "telegram", "auth", "channels", "workspace", "va
|
|
|
492
492
|
|
|
493
493
|
const { validateKazee } = require("./core/kazee-probe");
|
|
494
494
|
|
|
495
|
+
// Fixed Kazee host — mirrors KAZEE_DEFAULT_URL in core/config.js. Kept inline
|
|
496
|
+
// because config.js exits when no .env exists yet (setup runs before that).
|
|
497
|
+
const KAZEE_DEFAULT_URL = "https://chat.inet.africa";
|
|
498
|
+
|
|
495
499
|
async function main() {
|
|
496
500
|
// Check if this is an auth subcommand
|
|
497
501
|
const args = process.argv.slice(2);
|
|
@@ -673,12 +677,11 @@ async function main() {
|
|
|
673
677
|
console.log("\n Extra Channels (optional)\n");
|
|
674
678
|
const addKazee = await ask(" Add a Kazee channel now? (y/n) [n]: ");
|
|
675
679
|
if (addKazee.toLowerCase() === "y") {
|
|
680
|
+
const url = KAZEE_DEFAULT_URL; // fixed host — never prompted, like Telegram's api host
|
|
676
681
|
while (true) {
|
|
677
|
-
const url = (await ask(" Kazee bot API URL (e.g. https://chat.example.com/api): ")).trim();
|
|
678
|
-
if (!/^https?:\/\//i.test(url)) { console.log(" That doesn't look like a URL."); continue; }
|
|
679
682
|
const token = (await ask(" Kazee bot token: ")).trim();
|
|
680
683
|
if (!token) { console.log(" Empty token."); continue; }
|
|
681
|
-
console.log(" Validating against Kazee /me…");
|
|
684
|
+
console.log(" Validating against Kazee /api/me…");
|
|
682
685
|
const check = await validateKazee({ url, token });
|
|
683
686
|
if (!check.ok) {
|
|
684
687
|
console.log(` Validation failed: ${check.error}`);
|
|
@@ -687,7 +690,7 @@ async function main() {
|
|
|
687
690
|
continue;
|
|
688
691
|
}
|
|
689
692
|
const ownerUserId = (await ask(" Kazee owner user id (blank to skip): ")).trim();
|
|
690
|
-
state.data.kazee = { url: url.replace(
|
|
693
|
+
state.data.kazee = { url: check.url || url.replace(/\/+$/, ""), token, ownerUserId, botUserId: check.botUserId || "" };
|
|
691
694
|
console.log(" Kazee channel configured.");
|
|
692
695
|
break;
|
|
693
696
|
}
|
|
@@ -756,6 +759,7 @@ async function main() {
|
|
|
756
759
|
if (d.kazee) {
|
|
757
760
|
envLines.push(`KAZEE_URL=${d.kazee.url}`);
|
|
758
761
|
envLines.push(`KAZEE_BOT_TOKEN=${d.kazee.token}`);
|
|
762
|
+
envLines.push(`KAZEE_BOT_USER_ID=${d.kazee.botUserId || ""}`);
|
|
759
763
|
envLines.push(`KAZEE_OWNER_USER_ID=${d.kazee.ownerUserId || ""}`);
|
|
760
764
|
}
|
|
761
765
|
const env = envLines.join("\n");
|