Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -24,5 +29,8 @@
"@types/node": "^25.9.1",
"ts-node": "^10.9.2",
"typescript": "^6.0.3"
},
"dependencies": {
"dotenv": "^17.4.2"
}
}
23 changes: 23 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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;
}
202 changes: 150 additions & 52 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,156 @@
// 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 };
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: "http://localhost:3000",
authToken: "",
userId: "",
defaultRoom: "",
webhookSecret: "",
};

logger.info("Initializing Unified Rocket.Chat Plugin...");

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();
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);
}

return { ok: res.ok, channel: "rocketchat" }
}
},
gateway: {
startAccount: async (ctx: any) => {
ctx.setStatus({ accountId: ctx.account?.accountId ?? "default", state: "connected" });
return new Promise(() => { });
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: { mode: "none" },
handler: async (req: any, res: any) => {
try {
const body = req.body || {};

const token = req.headers["x-rocketchat-livechat-token"] || req.headers["authorization"] || body.token;
if (config.webhookSecret && token !== config.webhookSecret) {
logger.error("Webhook authentication failed. Invalid token.");
return res.status(401).json({ error: "Unauthorized" });
}

const text = body.text || "";
const roomId = body.channel_id || "GENERAL";
const senderId = body.user_id || "unknown";
const msgId = body.message_id || Date.now().toString();
const isBot = body.bot === true;

api.registerChannel({ plugin });
if (isBot || senderId === config.userId) {
return res.status(200).json({ success: true, ignored: true });
}

if (api.gateway && api.gateway.dispatchInbound) {
await api.gateway.dispatchInbound({
channel: "rocketchat",
accountId: "default",
type: "message",
message: {
id: msgId,
text: text,
from: senderId,
to: roomId,
metadata: {
channelName: body.channel_name,
userName: body.user_name,
threadId: body.tmid
}
},
raw: body,
});
logger.info(`Dispatched inbound message from ${senderId} in ${roomId}`);
} else {
logger.error("api.gateway.dispatchInbound is not available.");
}

res.status(200).json({ success: true });
} catch (err) {
logger.error(`Webhook processing error: ${(err as Error).message}`);
res.status(500).json({ error: "Internal Server Error" });
}
},
});
logger.info("Registered Inbound Webhook at /rocketchat/webhook");
} else {
logger.error("api.registerHttpRoute is not available on this OpenClaw version.");
}

if (api.registerCli) {
api.registerCli(({ program }: { program: any }) => {
const rc = program.command("rocketchat").alias("rc").description("Rocket.Chat unified plugin commands");
rc.command("status")
.description("Check Rocket.Chat webhook and integration status")
.action(() => {
console.log("Rocket.Chat plugin is loaded.");
console.log(`RC_URL: ${config.url}`);
console.log(`Webhook Secret Configured: ${!!config.webhookSecret}`);
});
}, {
commands: ["rocketchat"]
});
}

}
logger.info("Rocket.Chat Unified Plugin initialization complete.");
}
7 changes: 7 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface RocketChatConfig {
url: string;
authToken: string;
userId: string;
defaultRoom: string;
webhookSecret: string;
}