Files
openclaw/extensions/googlechat/src/onboarding.ts

226 lines
7.1 KiB
TypeScript
Raw Normal View History

import type { OpenClawConfig, DmPolicy } from "openclaw/plugin-sdk/googlechat";
2026-01-23 16:45:37 -06:00
import {
DEFAULT_ACCOUNT_ID,
applySetupAccountConfigPatch,
2026-01-23 16:45:37 -06:00
addWildcardAllowFrom,
formatDocsLink,
mergeAllowFromEntries,
resolveAccountIdForConfigure,
splitOnboardingEntries,
2026-01-23 16:45:37 -06:00
type ChannelOnboardingAdapter,
type ChannelOnboardingDmPolicy,
type WizardPrompter,
migrateBaseNameToDefaultAccount,
} from "openclaw/plugin-sdk/googlechat";
2026-01-23 16:45:37 -06:00
import {
listGoogleChatAccountIds,
resolveDefaultGoogleChatAccountId,
resolveGoogleChatAccount,
} from "./accounts.js";
const channel = "googlechat" as const;
const ENV_SERVICE_ACCOUNT = "GOOGLE_CHAT_SERVICE_ACCOUNT";
const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE";
2026-01-30 03:15:10 +01:00
function setGoogleChatDmPolicy(cfg: OpenClawConfig, policy: DmPolicy) {
2026-01-23 16:45:37 -06:00
const allowFrom =
policy === "open"
? addWildcardAllowFrom(cfg.channels?.["googlechat"]?.dm?.allowFrom)
: undefined;
return {
...cfg,
channels: {
...cfg.channels,
2026-01-31 21:13:13 +09:00
googlechat: {
2026-01-31 22:13:48 +09:00
...cfg.channels?.["googlechat"],
2026-01-23 16:45:37 -06:00
dm: {
2026-01-31 22:13:48 +09:00
...cfg.channels?.["googlechat"]?.dm,
2026-01-23 16:45:37 -06:00
policy,
...(allowFrom ? { allowFrom } : {}),
},
},
},
};
}
async function promptAllowFrom(params: {
2026-01-30 03:15:10 +01:00
cfg: OpenClawConfig;
2026-01-23 16:45:37 -06:00
prompter: WizardPrompter;
2026-01-30 03:15:10 +01:00
}): Promise<OpenClawConfig> {
2026-01-23 16:45:37 -06:00
const current = params.cfg.channels?.["googlechat"]?.dm?.allowFrom ?? [];
const entry = await params.prompter.text({
message: "Google Chat allowFrom (users/<id> or raw email; avoid users/<email>)",
2026-01-23 16:45:37 -06:00
placeholder: "users/123456789, name@example.com",
initialValue: current[0] ? String(current[0]) : undefined,
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
});
const parts = splitOnboardingEntries(String(entry));
const unique = mergeAllowFromEntries(undefined, parts);
2026-01-23 16:45:37 -06:00
return {
...params.cfg,
channels: {
...params.cfg.channels,
2026-01-31 21:13:13 +09:00
googlechat: {
2026-01-31 22:13:48 +09:00
...params.cfg.channels?.["googlechat"],
2026-01-23 16:45:37 -06:00
enabled: true,
dm: {
2026-01-31 22:13:48 +09:00
...params.cfg.channels?.["googlechat"]?.dm,
2026-01-23 16:45:37 -06:00
policy: "allowlist",
allowFrom: unique,
},
},
},
};
}
const dmPolicy: ChannelOnboardingDmPolicy = {
label: "Google Chat",
channel,
policyKey: "channels.googlechat.dm.policy",
allowFromKey: "channels.googlechat.dm.allowFrom",
getCurrent: (cfg) => cfg.channels?.["googlechat"]?.dm?.policy ?? "pairing",
setPolicy: (cfg, policy) => setGoogleChatDmPolicy(cfg, policy),
promptAllowFrom,
};
async function promptCredentials(params: {
2026-01-30 03:15:10 +01:00
cfg: OpenClawConfig;
2026-01-23 16:45:37 -06:00
prompter: WizardPrompter;
accountId: string;
2026-01-30 03:15:10 +01:00
}): Promise<OpenClawConfig> {
2026-01-23 16:45:37 -06:00
const { cfg, prompter, accountId } = params;
const envReady =
accountId === DEFAULT_ACCOUNT_ID &&
2026-01-31 21:13:13 +09:00
(Boolean(process.env[ENV_SERVICE_ACCOUNT]) || Boolean(process.env[ENV_SERVICE_ACCOUNT_FILE]));
2026-01-23 16:45:37 -06:00
if (envReady) {
const useEnv = await prompter.confirm({
message: "Use GOOGLE_CHAT_SERVICE_ACCOUNT env vars?",
initialValue: true,
});
if (useEnv) {
return applySetupAccountConfigPatch({ cfg, channelKey: channel, accountId, patch: {} });
2026-01-23 16:45:37 -06:00
}
}
const method = await prompter.select({
message: "Google Chat auth method",
options: [
{ value: "file", label: "Service account JSON file" },
{ value: "inline", label: "Paste service account JSON" },
],
initialValue: "file",
});
if (method === "file") {
const path = await prompter.text({
message: "Service account JSON path",
placeholder: "/path/to/service-account.json",
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
});
return applySetupAccountConfigPatch({
2026-01-23 16:45:37 -06:00
cfg,
channelKey: channel,
2026-01-23 16:45:37 -06:00
accountId,
patch: { serviceAccountFile: String(path).trim() },
});
}
const json = await prompter.text({
message: "Service account JSON (single line)",
2026-01-31 21:13:13 +09:00
placeholder: '{"type":"service_account", ... }',
2026-01-23 16:45:37 -06:00
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
});
return applySetupAccountConfigPatch({
2026-01-23 16:45:37 -06:00
cfg,
channelKey: channel,
2026-01-23 16:45:37 -06:00
accountId,
patch: { serviceAccount: String(json).trim() },
});
}
async function promptAudience(params: {
2026-01-30 03:15:10 +01:00
cfg: OpenClawConfig;
2026-01-23 16:45:37 -06:00
prompter: WizardPrompter;
accountId: string;
2026-01-30 03:15:10 +01:00
}): Promise<OpenClawConfig> {
2026-01-23 16:45:37 -06:00
const account = resolveGoogleChatAccount({
cfg: params.cfg,
accountId: params.accountId,
});
const currentType = account.config.audienceType ?? "app-url";
const currentAudience = account.config.audience ?? "";
2026-01-31 22:13:48 +09:00
const audienceType = await params.prompter.select({
2026-01-23 16:45:37 -06:00
message: "Webhook audience type",
options: [
{ value: "app-url", label: "App URL (recommended)" },
{ value: "project-number", label: "Project number" },
],
initialValue: currentType === "project-number" ? "project-number" : "app-url",
2026-01-31 22:13:48 +09:00
});
2026-01-23 16:45:37 -06:00
const audience = await params.prompter.text({
message: audienceType === "project-number" ? "Project number" : "App URL",
placeholder: audienceType === "project-number" ? "1234567890" : "https://your.host/googlechat",
initialValue: currentAudience || undefined,
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
});
return applySetupAccountConfigPatch({
2026-01-23 16:45:37 -06:00
cfg: params.cfg,
channelKey: channel,
2026-01-23 16:45:37 -06:00
accountId: params.accountId,
patch: { audienceType, audience: String(audience).trim() },
});
}
async function noteGoogleChatSetup(prompter: WizardPrompter) {
await prompter.note(
[
"Google Chat apps use service-account auth and an HTTPS webhook.",
"Set the Chat API scopes in your service account and configure the Chat app URL.",
"Webhook verification requires audience type + audience value.",
`Docs: ${formatDocsLink("/channels/googlechat", "channels/googlechat")}`,
].join("\n"),
"Google Chat setup",
);
}
export const googlechatOnboardingAdapter: ChannelOnboardingAdapter = {
channel,
dmPolicy,
getStatus: async ({ cfg }) => {
const configured = listGoogleChatAccountIds(cfg).some(
(accountId) => resolveGoogleChatAccount({ cfg, accountId }).credentialSource !== "none",
);
return {
channel,
configured,
2026-01-31 21:13:13 +09:00
statusLines: [`Google Chat: ${configured ? "configured" : "needs service account"}`],
2026-01-23 16:45:37 -06:00
selectionHint: configured ? "configured" : "needs auth",
};
},
2026-01-31 21:13:13 +09:00
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
2026-01-23 16:45:37 -06:00
const defaultAccountId = resolveDefaultGoogleChatAccountId(cfg);
const accountId = await resolveAccountIdForConfigure({
cfg,
prompter,
label: "Google Chat",
accountOverride: accountOverrides["googlechat"],
shouldPromptAccountIds,
listAccountIds: listGoogleChatAccountIds,
defaultAccountId,
});
2026-01-23 16:45:37 -06:00
let next = cfg;
await noteGoogleChatSetup(prompter);
next = await promptCredentials({ cfg: next, prompter, accountId });
next = await promptAudience({ cfg: next, prompter, accountId });
const namedConfig = migrateBaseNameToDefaultAccount({
cfg: next,
channelKey: "googlechat",
});
return { cfg: namedConfig, accountId };
},
};