refactor: reuse text runtime in google chat

This commit is contained in:
Peter Steinberger
2026-04-20 23:47:15 +01:00
parent 77a6187a70
commit 28d6aa5514
6 changed files with 60 additions and 84 deletions

View File

@@ -8,6 +8,7 @@ import {
} from "openclaw/plugin-sdk/account-resolution";
import { safeParseJsonWithSchema, safeParseWithSchema } from "openclaw/plugin-sdk/extension-shared";
import { isSecretRef } from "openclaw/plugin-sdk/secret-input";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { z } from "zod";
import type { GoogleChatAccountConfig } from "./types.config.js";
@@ -27,14 +28,6 @@ const ENV_SERVICE_ACCOUNT = "GOOGLE_CHAT_SERVICE_ACCOUNT";
const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE";
const JsonRecordSchema = z.record(z.string(), z.unknown());
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
const {
listAccountIds: listGoogleChatAccountIds,
resolveDefaultAccountId: resolveDefaultGoogleChatAccountId,

View File

@@ -1,4 +1,5 @@
import { GoogleAuth, OAuth2Client } from "google-auth-library";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { fetchWithSsrFGuard } from "../runtime-api.js";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
@@ -16,10 +17,6 @@ const verifyClient = new OAuth2Client();
let cachedCerts: { fetchedAt: number; certs: Record<string, string> } | null = null;
function normalizeLowercaseStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function buildAuthKey(account: ResolvedGoogleChatAccount): string {
if (account.credentialsFile) {
return `file:${account.credentialsFile}`;

View File

@@ -1,4 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type { WebhookInFlightLimiter } from "openclaw/plugin-sdk/webhook-request-guards";
import { readJsonWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
import {
@@ -14,10 +15,6 @@ import type {
GoogleChatUser,
} from "./types.js";
function normalizeLowercaseStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function extractBearerToken(header: unknown): string {
const authHeader = Array.isArray(header)
? typeof header[0] === "string"
@@ -104,6 +101,19 @@ function parseGoogleChatInboundPayload(
return { ok: true, event, addOnBearerToken };
}
async function isAuthorizedGoogleChatTarget(
target: WebhookTarget,
bearer: string,
): Promise<boolean> {
const verification = await verifyGoogleChatRequest({
bearer,
audienceType: target.audienceType,
audience: target.audience,
expectedAddOnPrincipal: target.account.config.appPrincipal,
});
return verification.ok;
}
export function createGoogleChatWebhookRequestHandler(params: {
webhookTargets: Map<string, WebhookTarget[]>;
webhookInFlightLimiter: WebhookInFlightLimiter;
@@ -149,15 +159,7 @@ export function createGoogleChatWebhookRequestHandler(params: {
selectedTarget = await resolveWebhookTargetWithAuthOrReject({
targets,
res,
isMatch: async (target) => {
const verification = await verifyGoogleChatRequest({
bearer: headerBearer,
audienceType: target.audienceType,
audience: target.audience,
expectedAddOnPrincipal: target.account.config.appPrincipal,
});
return verification.ok;
},
isMatch: (target) => isAuthorizedGoogleChatTarget(target, headerBearer),
});
if (!selectedTarget) {
return true;
@@ -184,15 +186,7 @@ export function createGoogleChatWebhookRequestHandler(params: {
selectedTarget = await resolveWebhookTargetWithAuthOrReject({
targets,
res,
isMatch: async (target) => {
const verification = await verifyGoogleChatRequest({
bearer: parsed.addOnBearerToken,
audienceType: target.audienceType,
audience: target.audience,
expectedAddOnPrincipal: target.account.config.appPrincipal,
});
return verification.ok;
},
isMatch: (target) => isAuthorizedGoogleChatTarget(target, parsed.addOnBearerToken),
});
if (!selectedTarget) {
return true;

View File

@@ -1,6 +1,4 @@
function normalizeLowercaseStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
function normalizeUserId(raw?: string | null): string {
const trimmed = typeof raw === "string" ? raw.trim() : "";

View File

@@ -11,6 +11,10 @@ import {
type ChannelSetupDmPolicy,
type ChannelSetupWizard,
} from "openclaw/plugin-sdk/setup";
import {
normalizeOptionalString,
normalizeStringifiedOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { resolveDefaultGoogleChatAccountId, resolveGoogleChatAccount } from "./accounts.js";
const channel = "googlechat" as const;
@@ -19,23 +23,8 @@ const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE";
const USE_ENV_FLAG = "__googlechatUseEnv";
const AUTH_METHOD_FLAG = "__googlechatAuthMethod";
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
function normalizeStringifiedOptionalString(value: unknown): string | undefined {
if (typeof value === "string") {
return normalizeOptionalString(value);
}
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
return normalizeOptionalString(String(value));
}
return undefined;
}
type GoogleChatTextInput = NonNullable<ChannelSetupWizard["textInputs"]>[number];
type GoogleChatTextInputKey = GoogleChatTextInput["inputKey"];
const promptAllowFrom = createPromptParsedAllowFromForAccount({
defaultAccountId: resolveDefaultGoogleChatAccountId,
@@ -104,6 +93,32 @@ const googlechatDmPolicy: ChannelSetupDmPolicy = {
export { googlechatSetupAdapter } from "./setup-core.js";
function createServiceAccountTextInput(params: {
inputKey: GoogleChatTextInputKey;
message: string;
placeholder: string;
authMethod: "file" | "inline";
patchKey: "serviceAccountFile" | "serviceAccount";
}): GoogleChatTextInput {
return {
inputKey: params.inputKey,
message: params.message,
placeholder: params.placeholder,
shouldPrompt: ({ credentialValues }) =>
credentialValues[USE_ENV_FLAG] !== "1" &&
credentialValues[AUTH_METHOD_FLAG] === params.authMethod,
validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"),
normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "",
applySet: async ({ cfg, accountId, value }) =>
applySetupAccountConfigPatch({
cfg,
channelKey: channel,
accountId,
patch: { [params.patchKey]: value },
}),
};
}
export const googlechatSetupWizard: ChannelSetupWizard = {
channel,
status: createStandardChannelSetupStatus({
@@ -169,38 +184,20 @@ export const googlechatSetupWizard: ChannelSetupWizard = {
},
credentials: [],
textInputs: [
{
createServiceAccountTextInput({
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 }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"),
normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "",
applySet: async ({ cfg, accountId, value }) =>
applySetupAccountConfigPatch({
cfg,
channelKey: channel,
accountId,
patch: { serviceAccountFile: value },
}),
},
{
authMethod: "file",
patchKey: "serviceAccountFile",
}),
createServiceAccountTextInput({
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 }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"),
normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "",
applySet: async ({ cfg, accountId, value }) =>
applySetupAccountConfigPatch({
cfg,
channelKey: channel,
accountId,
patch: { serviceAccount: value },
}),
},
authMethod: "inline",
patchKey: "serviceAccount",
}),
],
finalize: async ({ cfg, accountId, prompter }) => {
const account = resolveGoogleChatAccount({

View File

@@ -1,10 +1,7 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
import { findGoogleChatDirectMessage } from "./api.js";
function normalizeLowercaseStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
export function normalizeGoogleChatTarget(raw?: string | null): string | undefined {
const trimmed = raw?.trim();
if (!trimmed) {