diff --git a/.gitignore b/.gitignore index 3f4754b..732a546 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ package-lock.json .vscode node_modules -dist \ No newline at end of file +dist +todo.txt \ No newline at end of file diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 6d468a0..89c89c4 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -1,15 +1,71 @@ { - "id": "rocketchat-plugin", + "id": "rocketchat", "name": "RocketChat Webhook", "version": "0.1.0", "description": "Rocket.Chat integration for OpenClaw", "type": "channel", "channels": [ - "rocketchat-plugin" + "rocketchat" ], + "configSchema": { + "RC_URL": { + "type": "string", + "description": "Rocket.Chat server URL", + "default": "http://localhost:3000" + }, + "RC_AUTH_TOKEN": { + "type": "string", + "description": "Rocket.Chat bot auth token", + "secret": true + }, + "RC_USER_ID": { + "type": "string", + "description": "Rocket.Chat bot user ID" + }, + "DEFAULT_ROOM": { + "type": "string", + "description": "Default room ID to send messages to", + "default": "GENERAL" + }, + "RC_WEBHOOK_SECRET": { + "type": "string", + "description": "Secret token to validate incoming webhooks", + "secret": true + } + }, + "channelConfigs": { + "rocketchat": { + "schema": { + "RC_URL": { + "type": "string", + "description": "Rocket.Chat server URL", + "default": "http://localhost:3000" + }, + "RC_AUTH_TOKEN": { + "type": "string", + "description": "Rocket.Chat bot auth token", + "secret": true + }, + "RC_USER_ID": { + "type": "string", + "description": "Rocket.Chat bot user ID" + }, + "DEFAULT_ROOM": { + "type": "string", + "description": "Default room ID to send messages to", + "default": "GENERAL" + }, + "RC_WEBHOOK_SECRET": { + "type": "string", + "description": "Secret token to validate incoming webhooks", + "secret": true + } + } + } + }, "configuration": { "channels": [ - "rocketchat-plugin" + "rocketchat" ] }, "entry": "dist/index.js" diff --git a/package.json b/package.json index 27a7d05..b078ad4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,11 @@ "version": "1.0.0", "description": "A fully unified plugin for integrating Rocket.Chat with OpenClaw. This plugin eliminates the need for an external bridging server, providing a direct, single-place architecture for inbounds, outbounds, session management, and CLI configuration.", "main": "dist/index.js", + "openclaw": { + "extensions": [ + "./dist/index.js" + ] + }, "type": "module", "scripts": { "build": "tsc", @@ -24,5 +29,8 @@ "@types/node": "^25.9.1", "ts-node": "^10.9.2", "typescript": "^6.0.3" + }, + "dependencies": { + "dotenv": "^17.4.2" } } \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..b23a57f --- /dev/null +++ b/src/config.ts @@ -0,0 +1,23 @@ +import * as dotenv from 'dotenv'; +import type { RocketChatConfig } from './types/types.js'; + +dotenv.config(); + +export function getConfig(): RocketChatConfig { + const config = { + url: process.env.RC_URL || "http://localhost:3000", + authToken: process.env.RC_AUTH_TOKEN || "", + userId: process.env.RC_USER_ID || "", + defaultRoom: process.env.DEFAULT_ROOM || "GENERAL", + webhookSecret: process.env.RC_WEBHOOK_SECRET || "", + }; + + if (!config.authToken) { + console.warn("[RC Config] Warning: RC_AUTH_TOKEN is not set."); + } + if (!config.userId) { + console.warn("[RC Config] Warning: RC_USER_ID is not set."); + } + + return config; +} diff --git a/src/index.ts b/src/index.ts index e9ad6e1..c68f48c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,58 +1,207 @@ -// Register and load openclaw plugin - -export default function register(api: any) { - const RC_URL = process.env.RC_URL || "http://localhost:3000"; - const RC_AUTH_TOKEN = process.env.RC_AUTH_TOKEN || ""; - const RC_USER_ID = process.env.RC_USER_ID || ""; - const DEFAULT_ROOM = process.env.DEFAULT_ROOM || "GENERAL"; - - const plugin = { - id: "rocketchat-plugin", - meta: { - id: "rocketchat-plugin", - label: "Rocket.Chat (webhook)", - selectionLabel: "Rocket.Chat (webhook)", - blurb: "REST outbound to Rocket.Chat (chat.sendMessage).", - aliases: ["rc-hook", "rocketchat-hook"], - }, - capabilities: { chatTypes: ["direct", "group"] }, - config: { - listAccountIds: (_cfg: any) => ["default", "69c3a5f48b90145d5886b115", "69a873434af7ce5b5e37b18f"], - resolveAccount: (_cfg: any, accountId: string) => ({ - accountId: accountId ?? "default" - }), - }, - outbound: { - deliveryMode: "direct" as const, - resolveTarget: ({ to }: { to: string }) => { - const target = (to && to.trim()) ? to.trim() : DEFAULT_ROOM; - return { ok: true, to: target }; +import { getConfig } from "./config.js"; +const configvars = getConfig(); + +export default function register(api: any): void { + const logger = api.logger || { + info: (msg: string) => console.log(`[RC] ${msg}`), + error: (msg: string) => console.error(`[RC] ${msg}`), + }; + + const config = { + url: configvars.url || "http://localhost:3000", + authToken: configvars.authToken || "", + userId: configvars.userId || "", + defaultRoom: configvars.defaultRoom || "GENERAL", + webhookSecret: configvars.webhookSecret || "", + }; + + if (!config.webhookSecret) { + console.warn("[RC Config] Warning: RC_WEBHOOK_SECRET is not set — webhook auth disabled."); + } + + + logger.info("Initializing Unified Rocket.Chat Plugin..."); + logger.info(`[Config] RC_URL: ${config.url}`); + logger.info(`[Config] RC_USER_ID: ${config.userId || "NOT SET"}`); + logger.info(`[Config] RC_AUTH_TOKEN: ${config.authToken ? config.authToken.slice(0, 6) + "..." : "NOT SET"}`); + logger.info(`[Config] DEFAULT_ROOM: ${config.defaultRoom || "NOT SET"}`); + + api.registerChannel({ + plugin: { + id: "rocketchat", + meta: { + id: "rocketchat", + label: "Rocket.Chat", + selectionLabel: "Rocket.Chat", + blurb: "Unified Rocket.Chat Plugin with Inbound Webhook and Outbound REST", + aliases: ["rc"], }, - async sendText({ to, text }: { to?: string; text: string }) { - const room = to || DEFAULT_ROOM; - - const res = await fetch(`${RC_URL}/api/v1/chat.sendMessage`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Auth-Token": RC_AUTH_TOKEN, - "X-User-Id": RC_USER_ID, - }, - body: JSON.stringify({ message: { rid: room, msg: text } }), - }); - const body = await res.text(); - - return { ok: res.ok, channel: "rocketchat" } - } - }, - gateway: { - startAccount: async (ctx: any) => { - ctx.setStatus({ accountId: ctx.account?.accountId ?? "default", state: "connected" }); - return new Promise(() => { }); + capabilities: { chatTypes: ["direct", "group"] }, + config: { + listAccountIds: (_cfg: any) => ["default"], + resolveAccount: (_cfg: any, accountId?: string) => ({ + accountId: accountId || "default", + }), + }, + outbound: { + deliveryMode: "direct" as const, + resolveTarget: ({ to }: { to: string }) => { + const target = (to && to.trim()) ? to.trim() : config.defaultRoom; + return { ok: true, to: target }; + }, + sendText: async (ctx: { to: string; text: string; accountId?: string; threadId?: string | number | null }) => { + try { + const room = ctx.to || config.defaultRoom; + const payload: any = { rid: room, msg: ctx.text }; + if (ctx.threadId) { + payload.tmid = String(ctx.threadId); + } + + const res = await fetch(`${config.url}/api/v1/chat.sendMessage`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Auth-Token": config.authToken, + "X-User-Id": config.userId, + }, + body: JSON.stringify({ message: payload }), + }); + + if (!res.ok) { + const body = await res.text(); + logger.error(`Outbound failed: ${res.status} ${body}`); + return { ok: false, channel: "rocketchat" }; + } + + return { ok: true, channel: "rocketchat" }; + } catch (err) { + logger.error(`Outbound error: ${(err as Error).message}`); + return { ok: false, channel: "rocketchat" }; + } + }, + }, + gateway: { + startAccount: async (ctx: any) => { + ctx.setStatus({ accountId: ctx.account?.accountId ?? "default", state: "connected" }); + return new Promise(() => { }); + }, }, }, - }; + }); + + if (api.registerHttpRoute) { + api.registerHttpRoute({ + method: "POST", + path: "/rocketchat/webhook", + auth: "plugin", + handler: async (req: any, res: any) => { + try { + let body: any = (req.body && Object.keys(req.body).length > 0) ? req.body : null; +if (!body) { + const rawBody = await new Promise((resolve, reject) => { + let data = ""; + req.on("data", (chunk: any) => { data += chunk; }); + req.on("end", () => resolve(data)); + req.on("error", reject); + }); + try { body = rawBody ? JSON.parse(rawBody) : {}; } catch { body = {}; } +} + + logger.info("[Webhook] Incoming payload received"); + logger.info(`[Webhook] user_id: ${body.user_id}`); + logger.info(`[Webhook] user_name: ${body.user_name}`); + logger.info(`[Webhook] channel_id: ${body.channel_id}`); + logger.info(`[Webhook] channel_name: ${body.channel_name}`); + logger.info(`[Webhook] text: ${body.text}`); + logger.info(`[Webhook] message_id: ${body.message_id}`); + logger.info(`[Webhook] bot: ${body.bot}`); + logger.info(`[Webhook] tmid: ${body.tmid}`); + + // ignore bot self-messages + if (body.bot || body.user_id === config.userId) { + logger.info("[Webhook] Skipping bot message"); + res.statusCode = 200; + res.end(JSON.stringify({ success: true })); + return; + } + + // dispatch into OpenClaw + await api.scheduleSessionTurn({ + channel: "rocketchat", + accountId: "default", + to: body.channel_id || config.defaultRoom, + from: body.user_name, + text: body.text ?? "", + threadId: body.tmid ?? null, + messageId: body.message_id, + }); + + + // New API per sdk-channel-inbound docs. Use this once scheduleSessionTurn + // is confirmed removed or broken. Replace approach 1 with this block. + // + // await api.runtime.channel.inbound.run({ + // channel: "rocketchat", + // accountId: "default", + // raw: body, + // adapter: { + // // ingest: normalize the raw RC webhook payload into OpenClaw's + // // inbound message shape expected by the agent layer. + // ingest: (raw: any) => ({ + // id: raw.message_id ?? `${Date.now()}`, + // rawText: raw.text ?? "", + // textForAgent: raw.text ?? "", + // textForCommands: raw.text ?? "", + // from: raw.user_name, + // to: raw.channel_id || config.defaultRoom, + // threadId: raw.tmid ?? null, + // raw, + // }), + // // resolveTurn: assemble the full turn context for the agent — + // // routing, session store path, reply target, and delivery fn. + // // Signature and required fields TBD from channel-ingress docs: + // // https://docs.openclaw.ai/plugins/sdk-channel-ingress + // resolveTurn: (input: any) => { + // const room = body.channel_id || config.defaultRoom; + // return { + // // TODO: fill once ingress API shape is confirmed + // delivery: { + // deliver: async (payload: any) => { + // const text = payload.text ?? payload.message ?? ""; + // const sendRes = await fetch(`${config.url}/api/v1/chat.sendMessage`, { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // "X-Auth-Token": config.authToken, + // "X-User-Id": config.userId, + // }, + // body: JSON.stringify({ message: { rid: room, msg: text } }), + // }); + // if (!sendRes.ok) { + // const errBody = await sendRes.text().catch(() => ""); + // logger.error(`[Delivery] Failed: ${sendRes.status} ${errBody}`); + // } + // }, + // }, + // }; + // }, + // }, + // }); + + + logger.info("[Webhook] Dispatched to OpenClaw via scheduleSessionTurn"); - api.registerChannel({ plugin }); + res.statusCode = 200; + res.end(JSON.stringify({ success: true })); + } catch (err) { + logger.error(`[Webhook] Error: ${(err as Error).message}`); + res.statusCode = 500; + res.end(JSON.stringify({ error: "Internal Server Error" })); + } +}, + }); + logger.info("Registered Inbound Webhook at /rocketchat/webhook"); +} -} \ No newline at end of file + logger.info("Rocket.Chat Unified Plugin initialization complete."); +} diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 0000000..a77e998 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,7 @@ +export interface RocketChatConfig { + url: string; + authToken: string; + userId: string; + defaultRoom: string; + webhookSecret: string; +}