code icon Code

Send Slack Message

Post a message to a Slack channel or DM, optionally with Block Kit formatting

Source Code

import fs from "fs";

const [channelId, message, blocksPath = ""] = process.argv.slice(2);

if (!channelId) {
  console.error("Error: channelId is required");
  process.exit(1);
}

if (!message) {
  console.error("Error: message is required (used as fallback text)");
  process.exit(1);
}

// Extract JSON from content - handles both raw JSON and markdown with code blocks
function extractBlocksJson(content) {
  const trimmed = content.trim();

  // If it starts with { or [, it's raw JSON
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
    return JSON.parse(trimmed);
  }

  // Otherwise, look for JSON in markdown code blocks
  // Match ```json ... ``` or ``` ... ``` containing JSON
  const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
  if (codeBlockMatch) {
    const codeContent = codeBlockMatch[1].trim();
    if (codeContent.startsWith("{") || codeContent.startsWith("[")) {
      return JSON.parse(codeContent);
    }
  }

  // Try to find any JSON object in the content
  const jsonMatch = trimmed.match(/(\{[\s\S]*"blocks"[\s\S]*\})/);
  if (jsonMatch) {
    return JSON.parse(jsonMatch[1]);
  }

  throw new Error("No valid Block Kit JSON found in file");
}

console.log(`Sending message to ${channelId}...`);

try {
  const body = {
    channel: channelId,
    text: message,
  };

  // Load blocks from file if provided
  if (blocksPath && blocksPath.trim() && blocksPath !== "undefined") {
    try {
      const fileContent = fs.readFileSync(blocksPath, "utf-8");
      const parsed = extractBlocksJson(fileContent);
      // Accept either { blocks: [...] } or just [...]
      body.blocks = Array.isArray(parsed) ? parsed : parsed.blocks;
      console.log(
        `  Including ${body.blocks.length} Block Kit blocks from ${blocksPath}`
      );
    } catch (err) {
      if (err.code === "ENOENT") {
        console.error(`Warning: Blocks file not found: ${blocksPath}`);
      } else {
        console.error(`Warning: Failed to parse blocks: ${err.message}`);
      }
      console.log("  Sending plain text only");
    }
  }

  const res = await fetch("https://slack.com/api/chat.postMessage", {
    method: "POST",
    headers: {
      Authorization: "Bearer PLACEHOLDER_TOKEN",
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    throw new Error(`Slack API HTTP error: ${res.status}`);
  }

  const data = await res.json();

  if (!data.ok) {
    console.error(`Slack API error: ${data.error}`);
    if (data.error === "channel_not_found") {
      console.error("  The channel ID may be invalid or the bot lacks access");
    } else if (data.error === "not_in_channel") {
      console.error("  The bot needs to be added to this channel first");
    } else if (data.error === "invalid_blocks") {
      console.error("  The Block Kit JSON is malformed");
      console.error("  Test at: https://app.slack.com/block-kit-builder");
    }
    throw new Error(`Slack API error: ${data.error}`);
  }

  const timestamp = data.ts;
  const sentChannel = data.channel;

  console.log(`\n✓ Message sent successfully`);
  console.log(`  Channel: ${sentChannel}`);
  console.log(`  Timestamp: ${timestamp}`);
  console.log(
    `  Time: ${new Date(parseFloat(timestamp) * 1000).toISOString()}`
  );
  console.log(
    `  Format: ${
      body.blocks ? `Block Kit (${body.blocks.length} blocks)` : "Plain text"
    }`
  );

  console.log(
    JSON.stringify({
      success: true,
      channel: sentChannel,
      ts: timestamp,
      hasBlocks: !!body.blocks,
    })
  );
} catch (error) {
  console.error("Failed to send message:", error.message);
  throw error;
}
                  import fs from "fs";

const [channelId, message, blocksPath = ""] = process.argv.slice(2);

if (!channelId) {
  console.error("Error: channelId is required");
  process.exit(1);
}

if (!message) {
  console.error("Error: message is required (used as fallback text)");
  process.exit(1);
}

// Extract JSON from content - handles both raw JSON and markdown with code blocks
function extractBlocksJson(content) {
  const trimmed = content.trim();

  // If it starts with { or [, it's raw JSON
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
    return JSON.parse(trimmed);
  }

  // Otherwise, look for JSON in markdown code blocks
  // Match ```json ... ``` or ``` ... ``` containing JSON
  const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
  if (codeBlockMatch) {
    const codeContent = codeBlockMatch[1].trim();
    if (codeContent.startsWith("{") || codeContent.startsWith("[")) {
      return JSON.parse(codeContent);
    }
  }

  // Try to find any JSON object in the content
  const jsonMatch = trimmed.match(/(\{[\s\S]*"blocks"[\s\S]*\})/);
  if (jsonMatch) {
    return JSON.parse(jsonMatch[1]);
  }

  throw new Error("No valid Block Kit JSON found in file");
}

console.log(`Sending message to ${channelId}...`);

try {
  const body = {
    channel: channelId,
    text: message,
  };

  // Load blocks from file if provided
  if (blocksPath && blocksPath.trim() && blocksPath !== "undefined") {
    try {
      const fileContent = fs.readFileSync(blocksPath, "utf-8");
      const parsed = extractBlocksJson(fileContent);
      // Accept either { blocks: [...] } or just [...]
      body.blocks = Array.isArray(parsed) ? parsed : parsed.blocks;
      console.log(
        `  Including ${body.blocks.length} Block Kit blocks from ${blocksPath}`
      );
    } catch (err) {
      if (err.code === "ENOENT") {
        console.error(`Warning: Blocks file not found: ${blocksPath}`);
      } else {
        console.error(`Warning: Failed to parse blocks: ${err.message}`);
      }
      console.log("  Sending plain text only");
    }
  }

  const res = await fetch("https://slack.com/api/chat.postMessage", {
    method: "POST",
    headers: {
      Authorization: "Bearer PLACEHOLDER_TOKEN",
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    throw new Error(`Slack API HTTP error: ${res.status}`);
  }

  const data = await res.json();

  if (!data.ok) {
    console.error(`Slack API error: ${data.error}`);
    if (data.error === "channel_not_found") {
      console.error("  The channel ID may be invalid or the bot lacks access");
    } else if (data.error === "not_in_channel") {
      console.error("  The bot needs to be added to this channel first");
    } else if (data.error === "invalid_blocks") {
      console.error("  The Block Kit JSON is malformed");
      console.error("  Test at: https://app.slack.com/block-kit-builder");
    }
    throw new Error(`Slack API error: ${data.error}`);
  }

  const timestamp = data.ts;
  const sentChannel = data.channel;

  console.log(`\n✓ Message sent successfully`);
  console.log(`  Channel: ${sentChannel}`);
  console.log(`  Timestamp: ${timestamp}`);
  console.log(
    `  Time: ${new Date(parseFloat(timestamp) * 1000).toISOString()}`
  );
  console.log(
    `  Format: ${
      body.blocks ? `Block Kit (${body.blocks.length} blocks)` : "Plain text"
    }`
  );

  console.log(
    JSON.stringify({
      success: true,
      channel: sentChannel,
      ts: timestamp,
      hasBlocks: !!body.blocks,
    })
  );
} catch (error) {
  console.error("Failed to send message:", error.message);
  throw error;
}