diff --git a/src/googlechat/run-webhook.ts b/src/googlechat/run-webhook.ts new file mode 100644 index 000000000..84a3adbb1 --- /dev/null +++ b/src/googlechat/run-webhook.ts @@ -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`); +}); diff --git a/src/googlechat/webhook-server.ts b/src/googlechat/webhook-server.ts new file mode 100644 index 000000000..6113e374e --- /dev/null +++ b/src/googlechat/webhook-server.ts @@ -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; +}; + +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; 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 }; +} diff --git a/start-googlechat.sh b/start-googlechat.sh new file mode 100755 index 000000000..ed3c52cfc --- /dev/null +++ b/start-googlechat.sh @@ -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"