feat(googlechat): add webhook server and startup script

- Add run-webhook.ts for handling Google Chat messages via HTTP webhook
- Add webhook-server.ts for full Clawdbot integration
- Add start-googlechat.sh startup script for easy launching

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Justin Massa 2026-01-11 20:50:27 -06:00
parent 8bf4401bd8
commit ffb10c898f
3 changed files with 312 additions and 0 deletions

View File

@ -0,0 +1,101 @@
#!/usr/bin/env npx tsx
import express, { type Request, type Response } from "express";
import { execSync } from "child_process";
const PORT = 18792;
const app = express();
app.use(express.json());
// Health check
app.get("/health", (_req: Request, res: Response) => {
res.json({ ok: true, provider: "googlechat" });
});
// Google Chat webhook
app.post("/webhook/googlechat", async (req: Request, res: Response) => {
try {
const event = req.body;
const chat = event.chat || {};
// Detect event type from payload structure
const isAddedToSpace = !!chat.addedToSpacePayload;
const isMessage = !!chat.messagePayload;
const eventType = isAddedToSpace ? "ADDED_TO_SPACE" : isMessage ? "MESSAGE" : "UNKNOWN";
console.log(`[googlechat] Received event: ${eventType}`);
if (isAddedToSpace) {
const user = chat.user?.displayName || "there";
res.json({
hostAppDataAction: {
chatDataAction: {
createMessageAction: {
message: {
text: `Hello ${user}! I'm Clawdbot, your AI assistant. Send me a message and I'll respond!`,
},
},
},
},
});
return;
}
if (isMessage) {
const msg = chat.messagePayload.message;
const senderName = msg?.sender?.displayName || "Unknown";
const text = msg?.argumentText || msg?.text || "";
const spaceId = msg?.space?.name?.replace("spaces/", "") || "default";
console.log(`[googlechat] Message from ${senderName}: ${text}`);
let responseText: string;
try {
// Use clawdbot CLI to get AI response
// Escape the text for shell
const escapedText = text.replace(/'/g, "'\\''");
const sessionId = `googlechat:${spaceId}`;
const result = execSync(
`clawdbot agent --message '${escapedText}' --session-id '${sessionId}' --local 2>&1`,
{
timeout: 25000, // 25 second timeout (Google Chat times out at ~30s)
encoding: 'utf-8',
maxBuffer: 1024 * 1024,
}
);
responseText = result.trim() || "I processed your message but have no response.";
console.log(`[googlechat] AI Response: ${responseText.slice(0, 100)}...`);
} catch (err: any) {
console.error(`[googlechat] CLI error:`, err.message);
if (err.killed) {
responseText = "Sorry, the request timed out. Please try a simpler question.";
} else {
responseText = "Sorry, I encountered an error processing your message.";
}
}
res.json({
hostAppDataAction: {
chatDataAction: {
createMessageAction: {
message: {
text: responseText,
},
},
},
},
});
return;
}
res.json({});
} catch (error) {
console.error("[googlechat] Error:", error);
res.status(500).json({ error: "Internal error" });
}
});
app.listen(PORT, () => {
console.log(`[googlechat] Webhook server running on port ${PORT}`);
console.log(`[googlechat] Local: http://localhost:${PORT}/webhook/googlechat`);
console.log(`[googlechat] Use ngrok URL + /webhook/googlechat for Google Chat config`);
});

View File

@ -0,0 +1,155 @@
import express, { type Request, type Response } from "express";
import type { ClawdbotConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
import type { GoogleChatEvent } from "./types.js";
export type GoogleChatWebhookOptions = {
account: ResolvedGoogleChatAccount;
config: ClawdbotConfig;
runtime: RuntimeEnv;
port?: number;
onMessage?: (event: GoogleChatEvent) => Promise<void>;
};
export type NormalizedGoogleChatMessage = {
provider: "googlechat";
accountId: string;
messageId: string;
timestamp: number;
sender: {
id: string;
name: string;
email?: string;
};
chat: {
id: string;
name: string;
type: "dm" | "group";
};
thread?: {
id: string;
};
content: {
text: string;
};
raw: GoogleChatEvent;
};
function normalizeMessage(
event: GoogleChatEvent,
accountId: string,
): NormalizedGoogleChatMessage | null {
if (event.type !== "MESSAGE" || !event.message) {
return null;
}
const msg = event.message;
// Skip messages from bots
if (msg.sender.type === "BOT") {
return null;
}
const spaceId = msg.space.name.replace("spaces/", "");
const isDM = msg.space.type === "DM";
return {
provider: "googlechat",
accountId,
messageId: msg.name,
timestamp: new Date(msg.createTime).getTime(),
sender: {
id: msg.sender.name.replace("users/", ""),
name: msg.sender.displayName,
email: msg.sender.email,
},
chat: {
id: spaceId,
name: msg.space.displayName || spaceId,
type: isDM ? "dm" : "group",
},
thread: msg.thread
? {
id: msg.thread.name,
}
: undefined,
content: {
text: msg.argumentText || msg.text || "",
},
raw: event,
};
}
export async function startGoogleChatWebhookServer(
options: GoogleChatWebhookOptions,
): Promise<{ server: ReturnType<typeof express>; port: number; stop: () => void }> {
const { account, port = 18792 } = options;
const app = express();
app.use(express.json());
// Health check endpoint
app.get("/health", (_req: Request, res: Response) => {
res.json({ ok: true, provider: "googlechat", accountId: account.accountId });
});
// Google Chat webhook endpoint
app.post("/webhook/googlechat", async (req: Request, res: Response) => {
try {
const event = req.body as GoogleChatEvent;
console.log(`[googlechat:${account.accountId}] Received event: ${event.type}`);
// Handle different event types
if (event.type === "ADDED_TO_SPACE") {
// Bot was added to a space
res.json({
text: "Hello! I'm Clawdbot, your personal AI assistant. Send me a message to get started!",
});
return;
}
if (event.type === "MESSAGE") {
const normalized = normalizeMessage(event, account.accountId);
if (normalized) {
console.log(
`[googlechat:${account.accountId}] Message from ${normalized.sender.name}: ${normalized.content.text.slice(0, 50)}...`,
);
// Call the message handler if provided
if (options.onMessage) {
await options.onMessage(event);
}
// For now, echo back a confirmation
// In full integration, this would route to the agent
res.json({
text: `Got your message! (Clawdbot Google Chat integration is working)\n\nYou said: "${normalized.content.text}"`,
});
return;
}
}
// Default response for other events
res.json({});
} catch (error) {
console.error(`[googlechat:${account.accountId}] Webhook error:`, error);
res.status(500).json({ error: "Internal server error" });
}
});
// Start server
const server = app.listen(port, () => {
console.log(`[googlechat:${account.accountId}] Webhook server listening on port ${port}`);
console.log(`[googlechat:${account.accountId}] Webhook URL: http://localhost:${port}/webhook/googlechat`);
});
const stop = () => {
server.close();
console.log(`[googlechat:${account.accountId}] Webhook server stopped`);
};
return { server: app, port, stop };
}

56
start-googlechat.sh Executable file
View File

@ -0,0 +1,56 @@
#!/bin/bash
# Start Clawdbot Google Chat Integration
# Run this script after restarting your computer
cd "$(dirname "$0")"
echo "Starting Clawdbot Google Chat..."
echo ""
# Kill any existing processes
pkill -f "ngrok http 18792" 2>/dev/null
pkill -f "run-webhook" 2>/dev/null
sleep 1
# Start ngrok in background
echo "1. Starting ngrok tunnel..."
ngrok http 18792 > /tmp/ngrok.log 2>&1 &
sleep 4
# Get the ngrok URL
NGROK_URL=$(curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"https://[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$NGROK_URL" ]; then
echo " ERROR: ngrok failed to start. Check your internet connection."
exit 1
fi
echo " ngrok URL: $NGROK_URL"
echo ""
# Start webhook server
echo "2. Starting webhook server..."
npx tsx src/googlechat/run-webhook.ts > /tmp/googlechat-webhook.log 2>&1 &
sleep 3
# Verify webhook is running
if lsof -i :18792 > /dev/null 2>&1; then
echo " Webhook running on port 18792"
else
echo " ERROR: Webhook failed to start"
exit 1
fi
echo ""
echo "=========================================="
echo "Clawdbot Google Chat is READY!"
echo "=========================================="
echo ""
echo "Webhook URL: ${NGROK_URL}/webhook/googlechat"
echo ""
echo "NOTE: If the ngrok URL changed, update it in Google Chat:"
echo " https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat"
echo ""
echo "Logs: tail -f /tmp/googlechat-webhook.log"
echo ""
echo "To stop: pkill -f ngrok && pkill -f run-webhook"