Send to Slack
Send a message to Slack, or upload a file with a comment
Source Code
import fs from "fs";
import path from "path";
const [channelId, message, filePath = "", blocksPath = "", title = ""] =
process.argv.slice(2);
// 1. Validate channelId (required always)
if (!channelId) {
console.error("Error: channelId is required");
process.exit(1);
}
// 2. Validate filePath if provided (error if doesn't exist)
if (filePath && filePath.trim()) {
if (!fs.existsSync(filePath)) {
console.error(`Error: File not found: ${filePath}`);
process.exit(1);
}
}
// 3. Determine mode: upload if filePath provided
const isUploadMode = filePath && filePath.trim();
// 4. Validate message (required for text messages, optional for uploads)
if (!message && !isUploadMode) {
console.error("Error: message is required for text messages");
process.exit(1);
}
if (isUploadMode) {
// ============================================================
// FILE UPLOAD FLOW
// ============================================================
console.log(`Uploading file to ${channelId}...`);
console.log(` File: ${filePath}`);
try {
const fileBuffer = fs.readFileSync(filePath);
const fileName = path.basename(filePath);
const fileSize = fileBuffer.length;
console.log(` Size: ${fileSize} bytes`);
// Step 1: Get upload URL from Slack
const uploadUrlFormData = new FormData();
uploadUrlFormData.append("filename", fileName);
uploadUrlFormData.append("length", fileSize.toString());
const uploadUrlResponse = await fetch(
"https://slack.com/api/files.getUploadURLExternal",
{
method: "POST",
headers: {
Authorization: "Bearer PLACEHOLDER_TOKEN",
},
body: uploadUrlFormData,
}
);
const uploadUrlData = await uploadUrlResponse.json();
if (!uploadUrlData.ok) {
console.error(`Slack API error: ${uploadUrlData.error}`);
if (uploadUrlData.error === "missing_scope") {
console.error(" The app needs files:write scope permission");
}
throw new Error(`Failed to get upload URL: ${uploadUrlData.error}`);
}
const { upload_url, file_id } = uploadUrlData;
console.log(` Got upload URL for file_id: ${file_id}`);
// Step 2: Upload file contents to the provided URL
const uploadResponse = await fetch(upload_url, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
},
body: fileBuffer,
});
if (!uploadResponse.ok) {
throw new Error(
`Failed to upload file contents: ${uploadResponse.statusText}`
);
}
console.log(" File contents uploaded");
// Step 3: Complete the upload and share to channel
const completeFormData = new FormData();
const fileData = { id: file_id };
if (title && title.trim()) {
fileData.title = title;
}
completeFormData.append("files", JSON.stringify([fileData]));
completeFormData.append("channel_id", channelId);
// Use message as initial_comment for uploads
if (message && message.trim()) {
completeFormData.append("initial_comment", message);
}
const completeResponse = await fetch(
"https://slack.com/api/files.completeUploadExternal",
{
method: "POST",
headers: {
Authorization: "Bearer PLACEHOLDER_TOKEN",
},
body: completeFormData,
}
);
const completeData = await completeResponse.json();
if (!completeData.ok) {
console.error(`Slack API error: ${completeData.error}`);
if (completeData.error === "channel_not_found") {
console.error(" The channel ID may be invalid or the bot lacks access");
}
throw new Error(`Failed to complete upload: ${completeData.error}`);
}
const uploadedFile = completeData.files?.[0];
console.log(`\nā File uploaded successfully`);
console.log(` File ID: ${uploadedFile?.id}`);
console.log(` Name: ${uploadedFile?.name}`);
console.log(` Permalink: ${uploadedFile?.permalink}`);
console.log(
JSON.stringify({
success: true,
mode: "upload",
fileId: uploadedFile?.id,
fileName: uploadedFile?.name,
permalink: uploadedFile?.permalink,
})
);
} catch (error) {
console.error("Failed to upload file:", error.message);
throw error;
}
} else {
// ============================================================
// MESSAGE FLOW
// ============================================================
// 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
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,
as_user: true,
};
// Load blocks from file if provided
if (blocksPath && blocksPath.trim() && blocksPath !== "undefined") {
try {
const fileContent = fs.readFileSync(blocksPath, "utf-8");
const parsed = extractBlocksJson(fileContent);
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,
mode: "message",
channel: sentChannel,
ts: timestamp,
hasBlocks: !!body.blocks,
})
);
} catch (error) {
console.error("Failed to send message:", error.message);
throw error;
}
}