← back to Ventura Claw

server/connectors/slack.js

59 lines

// VenturaClaw — Slack connector (REAL API)
// Reads SLACK_BOT_TOKEN from env. Inject via /secrets skill; pm2 restart --update-env.
// Docs: https://docs.slack.dev/apis/web-api/

const SLACK_API = "https://slack.com/api";

function token(creds) {
  const t = creds?.SLACK_BOT_TOKEN || process.env.SLACK_BOT_TOKEN;
  if (!t) throw new Error("SLACK_BOT_TOKEN not set (user creds or env)");
  return t;
}

async function call(method, body, creds) {
  const r = await fetch(`${SLACK_API}/${method}`, {
    method: "POST",
    headers: { "Authorization": `Bearer ${token(creds)}`, "Content-Type": "application/json; charset=utf-8" },
    body: JSON.stringify(body || {})
  });
  const d = await r.json();
  if (!d.ok) throw new Error(`slack.${method} failed: ${d.error || "unknown"}`);
  return d;
}
async function callGet(method, params, creds) {
  const qs = new URLSearchParams(params || {}).toString();
  const r = await fetch(`${SLACK_API}/${method}${qs ? "?" + qs : ""}`, {
    headers: { "Authorization": `Bearer ${token(creds)}` }
  });
  const d = await r.json();
  if (!d.ok) throw new Error(`slack.${method} failed: ${d.error || "unknown"}`);
  return d;
}

const slack = {
  meta: { id: "slack", name: "Slack", category: "comms", docsUrl: "https://docs.slack.dev/apis/web-api/", auth: "oauth2", realImpl: true },
  fields: [
    { key: "SLACK_BOT_TOKEN", label: "Bot User OAuth Token", type: "password", required: true, hint: "starts with xoxb-... — Slack App > OAuth & Permissions" }
  ],
  configured(creds) { return !!(creds?.SLACK_BOT_TOKEN || process.env.SLACK_BOT_TOKEN); },
  async health(creds) {
    if (!this.configured(creds)) return { ok: false, reason: "no_token" };
    try {
      const d = await callGet("auth.test", null, creds);
      return { ok: true, team: d.team, user: d.user, bot_id: d.bot_id, url: d.url };
    } catch (e) { return { ok: false, reason: e.message }; }
  },
  actions: {
    async "chat.postMessage"({ channel, text, blocks, thread_ts }, creds) {
      if (!channel || !text) throw new Error("channel and text required");
      return call("chat.postMessage", { channel, text, blocks, thread_ts }, creds);
    },
    async "channels.create"({ name, is_private }, creds) { return call("conversations.create", { name, is_private: !!is_private }, creds); },
    async "channels.list"(_, creds) { return callGet("conversations.list", { types: "public_channel,private_channel", limit: "200" }, creds); },
    async "reactions.add"({ channel, timestamp, name }, creds) { return call("reactions.add", { channel, timestamp, name }, creds); },
    async "users.lookup"({ email }, creds) { return callGet("users.lookupByEmail", { email }, creds); }
  }
};

module.exports = slack;