From 18e4e4677c5b49d460f2aa11a29e56e2ccc3a578 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Mar 2026 17:56:45 -0700 Subject: [PATCH] refactor: move googlechat to setup wizard --- extensions/googlechat/src/channel.ts | 67 +--- extensions/googlechat/src/onboarding.ts | 225 -------------- .../googlechat/src/setup-surface.test.ts | 68 +++++ extensions/googlechat/src/setup-surface.ts | 288 ++++++++++++++++++ 4 files changed, 359 insertions(+), 289 deletions(-) delete mode 100644 extensions/googlechat/src/onboarding.ts create mode 100644 extensions/googlechat/src/setup-surface.test.ts create mode 100644 extensions/googlechat/src/setup-surface.ts diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index 3ae992d3e9e..ef8e92d8ce2 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -7,8 +7,6 @@ import { formatNormalizedAllowFromEntries, } from "openclaw/plugin-sdk/compat"; import { - applyAccountNameToChannelSection, - applySetupAccountConfigPatch, buildComputedAccountStatusSnapshot, buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, @@ -16,9 +14,7 @@ import { getChatChannelMeta, listDirectoryGroupEntriesFromMapKeys, listDirectoryUserEntriesFromAllowFrom, - migrateBaseNameToDefaultAccount, missingTargetError, - normalizeAccountId, PAIRING_APPROVED_MESSAGE, resolveChannelMediaMaxBytes, resolveGoogleChatGroupRequireMention, @@ -40,8 +36,8 @@ import { import { googlechatMessageActions } from "./actions.js"; import { sendGoogleChatMessage, uploadGoogleChatAttachment, probeGoogleChat } from "./api.js"; import { resolveGoogleChatWebhookPath, startGoogleChatMonitor } from "./monitor.js"; -import { googlechatOnboardingAdapter } from "./onboarding.js"; import { getGoogleChatRuntime } from "./runtime.js"; +import { googlechatSetupAdapter, googlechatSetupWizard } from "./setup-surface.js"; import { isGoogleChatSpaceTarget, isGoogleChatUserTarget, @@ -136,7 +132,8 @@ const googlechatActions: ChannelMessageActionAdapter = { export const googlechatPlugin: ChannelPlugin = { id: "googlechat", meta: { ...meta }, - onboarding: googlechatOnboardingAdapter, + setup: googlechatSetupAdapter, + setupWizard: googlechatSetupWizard, pairing: { idLabel: "googlechatUserId", normalizeAllowEntry: (entry) => formatAllowFromEntry(entry), @@ -272,64 +269,6 @@ export const googlechatPlugin: ChannelPlugin = { }, }, actions: googlechatActions, - setup: { - resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), - applyAccountName: ({ cfg, accountId, name }) => - applyAccountNameToChannelSection({ - cfg: cfg, - channelKey: "googlechat", - accountId, - name, - }), - validateInput: ({ accountId, input }) => { - if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) { - return "GOOGLE_CHAT_SERVICE_ACCOUNT env vars can only be used for the default account."; - } - if (!input.useEnv && !input.token && !input.tokenFile) { - return "Google Chat requires --token (service account JSON) or --token-file."; - } - return null; - }, - applyAccountConfig: ({ cfg, accountId, input }) => { - const namedConfig = applyAccountNameToChannelSection({ - cfg: cfg, - channelKey: "googlechat", - accountId, - name: input.name, - }); - const next = - accountId !== DEFAULT_ACCOUNT_ID - ? migrateBaseNameToDefaultAccount({ - cfg: namedConfig, - channelKey: "googlechat", - }) - : namedConfig; - const patch = input.useEnv - ? {} - : input.tokenFile - ? { serviceAccountFile: input.tokenFile } - : input.token - ? { serviceAccount: input.token } - : {}; - const audienceType = input.audienceType?.trim(); - const audience = input.audience?.trim(); - const webhookPath = input.webhookPath?.trim(); - const webhookUrl = input.webhookUrl?.trim(); - const configPatch = { - ...patch, - ...(audienceType ? { audienceType } : {}), - ...(audience ? { audience } : {}), - ...(webhookPath ? { webhookPath } : {}), - ...(webhookUrl ? { webhookUrl } : {}), - }; - return applySetupAccountConfigPatch({ - cfg: next, - channelKey: "googlechat", - accountId, - patch: configPatch, - }); - }, - }, outbound: { deliveryMode: "direct", chunker: (text, limit) => getGoogleChatRuntime().channel.text.chunkMarkdownText(text, limit), diff --git a/extensions/googlechat/src/onboarding.ts b/extensions/googlechat/src/onboarding.ts deleted file mode 100644 index f7708dd30b9..00000000000 --- a/extensions/googlechat/src/onboarding.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type { OpenClawConfig, DmPolicy } from "openclaw/plugin-sdk/googlechat"; -import { - DEFAULT_ACCOUNT_ID, - applySetupAccountConfigPatch, - addWildcardAllowFrom, - formatDocsLink, - mergeAllowFromEntries, - resolveAccountIdForConfigure, - splitOnboardingEntries, - type ChannelOnboardingAdapter, - type ChannelOnboardingDmPolicy, - type WizardPrompter, - migrateBaseNameToDefaultAccount, -} from "openclaw/plugin-sdk/googlechat"; -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"; - -function setGoogleChatDmPolicy(cfg: OpenClawConfig, policy: DmPolicy) { - const allowFrom = - policy === "open" - ? addWildcardAllowFrom(cfg.channels?.["googlechat"]?.dm?.allowFrom) - : undefined; - return { - ...cfg, - channels: { - ...cfg.channels, - googlechat: { - ...cfg.channels?.["googlechat"], - dm: { - ...cfg.channels?.["googlechat"]?.dm, - policy, - ...(allowFrom ? { allowFrom } : {}), - }, - }, - }, - }; -} - -async function promptAllowFrom(params: { - cfg: OpenClawConfig; - prompter: WizardPrompter; -}): Promise { - const current = params.cfg.channels?.["googlechat"]?.dm?.allowFrom ?? []; - const entry = await params.prompter.text({ - message: "Google Chat allowFrom (users/ or raw email; avoid users/)", - 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); - return { - ...params.cfg, - channels: { - ...params.cfg.channels, - googlechat: { - ...params.cfg.channels?.["googlechat"], - enabled: true, - dm: { - ...params.cfg.channels?.["googlechat"]?.dm, - 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: { - cfg: OpenClawConfig; - prompter: WizardPrompter; - accountId: string; -}): Promise { - const { cfg, prompter, accountId } = params; - const envReady = - accountId === DEFAULT_ACCOUNT_ID && - (Boolean(process.env[ENV_SERVICE_ACCOUNT]) || Boolean(process.env[ENV_SERVICE_ACCOUNT_FILE])); - 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: {} }); - } - } - - 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({ - cfg, - channelKey: channel, - accountId, - patch: { serviceAccountFile: String(path).trim() }, - }); - } - - const json = await prompter.text({ - message: "Service account JSON (single line)", - placeholder: '{"type":"service_account", ... }', - validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), - }); - return applySetupAccountConfigPatch({ - cfg, - channelKey: channel, - accountId, - patch: { serviceAccount: String(json).trim() }, - }); -} - -async function promptAudience(params: { - cfg: OpenClawConfig; - prompter: WizardPrompter; - accountId: string; -}): Promise { - const account = resolveGoogleChatAccount({ - cfg: params.cfg, - accountId: params.accountId, - }); - const currentType = account.config.audienceType ?? "app-url"; - const currentAudience = account.config.audience ?? ""; - const audienceType = await params.prompter.select({ - 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", - }); - 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({ - cfg: params.cfg, - channelKey: channel, - 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, - statusLines: [`Google Chat: ${configured ? "configured" : "needs service account"}`], - selectionHint: configured ? "configured" : "needs auth", - }; - }, - configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => { - const defaultAccountId = resolveDefaultGoogleChatAccountId(cfg); - const accountId = await resolveAccountIdForConfigure({ - cfg, - prompter, - label: "Google Chat", - accountOverride: accountOverrides["googlechat"], - shouldPromptAccountIds, - listAccountIds: listGoogleChatAccountIds, - defaultAccountId, - }); - - 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 }; - }, -}; diff --git a/extensions/googlechat/src/setup-surface.test.ts b/extensions/googlechat/src/setup-surface.test.ts new file mode 100644 index 00000000000..ab09435f67e --- /dev/null +++ b/extensions/googlechat/src/setup-surface.test.ts @@ -0,0 +1,68 @@ +import type { OpenClawConfig, WizardPrompter } from "openclaw/plugin-sdk/googlechat"; +import { describe, expect, it, vi } from "vitest"; +import { buildChannelOnboardingAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; +import { createRuntimeEnv } from "../../test-utils/runtime-env.js"; +import { googlechatPlugin } from "./channel.js"; + +const selectFirstOption = async (params: { options: Array<{ value: T }> }): Promise => { + const first = params.options[0]; + if (!first) { + throw new Error("no options"); + } + return first.value; +}; + +function createPrompter(overrides: Partial): WizardPrompter { + return { + intro: vi.fn(async () => {}), + outro: vi.fn(async () => {}), + note: vi.fn(async () => {}), + select: selectFirstOption as WizardPrompter["select"], + multiselect: vi.fn(async () => []), + text: vi.fn(async () => "") as WizardPrompter["text"], + confirm: vi.fn(async () => false), + progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })), + ...overrides, + }; +} + +const googlechatConfigureAdapter = buildChannelOnboardingAdapterFromSetupWizard({ + plugin: googlechatPlugin, + wizard: googlechatPlugin.setupWizard!, +}); + +describe("googlechat setup wizard", () => { + it("configures service-account auth and webhook audience", async () => { + const prompter = createPrompter({ + text: vi.fn(async ({ message }: { message: string }) => { + if (message === "Service account JSON path") { + return "/tmp/googlechat-service-account.json"; + } + if (message === "App URL") { + return "https://example.com/googlechat"; + } + throw new Error(`Unexpected prompt: ${message}`); + }) as WizardPrompter["text"], + }); + + const runtime = createRuntimeEnv(); + + const result = await googlechatConfigureAdapter.configure({ + cfg: {} as OpenClawConfig, + runtime, + prompter, + options: {}, + accountOverrides: {}, + shouldPromptAccountIds: false, + forceAllowFrom: false, + }); + + expect(result.accountId).toBe("default"); + expect(result.cfg.channels?.googlechat?.enabled).toBe(true); + expect(result.cfg.channels?.googlechat?.serviceAccountFile).toBe( + "/tmp/googlechat-service-account.json", + ); + expect(result.cfg.channels?.googlechat?.audienceType).toBe("app-url"); + expect(result.cfg.channels?.googlechat?.audience).toBe("https://example.com/googlechat"); + }); +}); diff --git a/extensions/googlechat/src/setup-surface.ts b/extensions/googlechat/src/setup-surface.ts new file mode 100644 index 00000000000..e812561f674 --- /dev/null +++ b/extensions/googlechat/src/setup-surface.ts @@ -0,0 +1,288 @@ +import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js"; +import { + addWildcardAllowFrom, + mergeAllowFromEntries, + setTopLevelChannelDmPolicyWithAllowFrom, + splitOnboardingEntries, +} from "../../../src/channels/plugins/onboarding/helpers.js"; +import { + applyAccountNameToChannelSection, + applySetupAccountConfigPatch, + migrateBaseNameToDefaultAccount, +} from "../../../src/channels/plugins/setup-helpers.js"; +import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; +import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; +import type { DmPolicy } from "../../../src/config/types.js"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js"; +import { formatDocsLink } from "../../../src/terminal/links.js"; +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"; +const USE_ENV_FLAG = "__googlechatUseEnv"; +const AUTH_METHOD_FLAG = "__googlechatAuthMethod"; + +function setGoogleChatDmPolicy(cfg: OpenClawConfig, policy: DmPolicy) { + const allowFrom = + policy === "open" ? addWildcardAllowFrom(cfg.channels?.googlechat?.dm?.allowFrom) : undefined; + return { + ...cfg, + channels: { + ...cfg.channels, + googlechat: { + ...cfg.channels?.googlechat, + dm: { + ...cfg.channels?.googlechat?.dm, + policy, + ...(allowFrom ? { allowFrom } : {}), + }, + }, + }, + }; +} + +async function promptAllowFrom(params: { + cfg: OpenClawConfig; + prompter: Parameters>[0]["prompter"]; +}): Promise { + const current = params.cfg.channels?.googlechat?.dm?.allowFrom ?? []; + const entry = await params.prompter.text({ + message: "Google Chat allowFrom (users/ or raw email; avoid users/)", + 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); + return { + ...params.cfg, + channels: { + ...params.cfg.channels, + googlechat: { + ...params.cfg.channels?.googlechat, + enabled: true, + dm: { + ...params.cfg.channels?.googlechat?.dm, + policy: "allowlist", + allowFrom: unique, + }, + }, + }, + }; +} + +const googlechatDmPolicy: 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, +}; + +export const googlechatSetupAdapter: ChannelSetupAdapter = { + resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), + applyAccountName: ({ cfg, accountId, name }) => + applyAccountNameToChannelSection({ + cfg, + channelKey: channel, + accountId, + name, + }), + validateInput: ({ accountId, input }) => { + if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) { + return "GOOGLE_CHAT_SERVICE_ACCOUNT env vars can only be used for the default account."; + } + if (!input.useEnv && !input.token && !input.tokenFile) { + return "Google Chat requires --token (service account JSON) or --token-file."; + } + return null; + }, + applyAccountConfig: ({ cfg, accountId, input }) => { + const namedConfig = applyAccountNameToChannelSection({ + cfg, + channelKey: channel, + accountId, + name: input.name, + }); + const next = + accountId !== DEFAULT_ACCOUNT_ID + ? migrateBaseNameToDefaultAccount({ + cfg: namedConfig, + channelKey: channel, + }) + : namedConfig; + const patch = input.useEnv + ? {} + : input.tokenFile + ? { serviceAccountFile: input.tokenFile } + : input.token + ? { serviceAccount: input.token } + : {}; + const audienceType = input.audienceType?.trim(); + const audience = input.audience?.trim(); + const webhookPath = input.webhookPath?.trim(); + const webhookUrl = input.webhookUrl?.trim(); + return applySetupAccountConfigPatch({ + cfg: next, + channelKey: channel, + accountId, + patch: { + ...patch, + ...(audienceType ? { audienceType } : {}), + ...(audience ? { audience } : {}), + ...(webhookPath ? { webhookPath } : {}), + ...(webhookUrl ? { webhookUrl } : {}), + }, + }); + }, +}; + +export const googlechatSetupWizard: ChannelSetupWizard = { + channel, + status: { + configuredLabel: "configured", + unconfiguredLabel: "needs service account", + configuredHint: "configured", + unconfiguredHint: "needs auth", + resolveConfigured: ({ cfg }) => + listGoogleChatAccountIds(cfg).some( + (accountId) => resolveGoogleChatAccount({ cfg, accountId }).credentialSource !== "none", + ), + resolveStatusLines: ({ cfg }) => { + const configured = listGoogleChatAccountIds(cfg).some( + (accountId) => resolveGoogleChatAccount({ cfg, accountId }).credentialSource !== "none", + ); + return [`Google Chat: ${configured ? "configured" : "needs service account"}`]; + }, + }, + introNote: { + title: "Google Chat setup", + lines: [ + "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", "googlechat")}`, + ], + }, + prepare: async ({ cfg, accountId, credentialValues, prompter }) => { + const envReady = + accountId === DEFAULT_ACCOUNT_ID && + (Boolean(process.env[ENV_SERVICE_ACCOUNT]) || Boolean(process.env[ENV_SERVICE_ACCOUNT_FILE])); + if (envReady) { + const useEnv = await prompter.confirm({ + message: "Use GOOGLE_CHAT_SERVICE_ACCOUNT env vars?", + initialValue: true, + }); + if (useEnv) { + return { + cfg: applySetupAccountConfigPatch({ + cfg, + channelKey: channel, + accountId, + patch: {}, + }), + credentialValues: { + ...credentialValues, + [USE_ENV_FLAG]: "1", + }, + }; + } + } + + 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", + }); + + return { + credentialValues: { + ...credentialValues, + [USE_ENV_FLAG]: "0", + [AUTH_METHOD_FLAG]: String(method), + }, + }; + }, + credentials: [], + textInputs: [ + { + inputKey: "tokenFile", + message: "Service account JSON path", + placeholder: "/path/to/service-account.json", + shouldPrompt: ({ credentialValues }) => + credentialValues[USE_ENV_FLAG] !== "1" && credentialValues[AUTH_METHOD_FLAG] === "file", + validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), + normalizeValue: ({ value }) => String(value).trim(), + applySet: async ({ cfg, accountId, value }) => + applySetupAccountConfigPatch({ + cfg, + channelKey: channel, + accountId, + patch: { serviceAccountFile: value }, + }), + }, + { + inputKey: "token", + message: "Service account JSON (single line)", + placeholder: '{"type":"service_account", ... }', + shouldPrompt: ({ credentialValues }) => + credentialValues[USE_ENV_FLAG] !== "1" && credentialValues[AUTH_METHOD_FLAG] === "inline", + validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), + normalizeValue: ({ value }) => String(value).trim(), + applySet: async ({ cfg, accountId, value }) => + applySetupAccountConfigPatch({ + cfg, + channelKey: channel, + accountId, + patch: { serviceAccount: value }, + }), + }, + ], + finalize: async ({ cfg, accountId, prompter }) => { + const account = resolveGoogleChatAccount({ + cfg, + accountId, + }); + const audienceType = await prompter.select({ + message: "Webhook audience type", + options: [ + { value: "app-url", label: "App URL (recommended)" }, + { value: "project-number", label: "Project number" }, + ], + initialValue: account.config.audienceType === "project-number" ? "project-number" : "app-url", + }); + const audience = await prompter.text({ + message: audienceType === "project-number" ? "Project number" : "App URL", + placeholder: + audienceType === "project-number" ? "1234567890" : "https://your.host/googlechat", + initialValue: account.config.audience || undefined, + validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), + }); + return { + cfg: migrateBaseNameToDefaultAccount({ + cfg: applySetupAccountConfigPatch({ + cfg, + channelKey: channel, + accountId, + patch: { + audienceType, + audience: String(audience).trim(), + }, + }), + channelKey: channel, + }), + }; + }, + dmPolicy: googlechatDmPolicy, +};