code icon Code

Fetch Slack History

Fetch message history from a Slack channel or DM

Source Code

import fs from 'fs';
import path from 'path';

const [channelId, limit = '50', outputPath] = process.argv.slice(2);
const maxMessages = Math.min(parseInt(limit) || 50, 200);

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

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

console.log(`Fetching up to ${maxMessages} messages from ${channelId}...`);

try {
  // Fetch channel info and users in parallel
  const [channelInfo, userMap] = await Promise.all([
    (async () => {
      const params = new URLSearchParams({ channel: channelId });
      const res = await fetch('https://slack.com/api/conversations.info?' + params, {
        headers: { 'Authorization': 'Bearer PLACEHOLDER_TOKEN' }
      });
      const data = await res.json();
      if (!data.ok) return { id: channelId, name: null };
      const c = data.channel;
      return {
        id: c.id,
        name: c.name || null,
        is_private: c.is_private,
        is_im: c.is_im,
        is_mpim: c.is_mpim,
      };
    })(),

    (async () => {
      const users = new Map();
      let cursor = null;
      do {
        const params = new URLSearchParams({ limit: '200' });
        if (cursor) params.set('cursor', cursor);
        const res = await fetch('https://slack.com/api/users.list?' + params, {
          headers: { 'Authorization': 'Bearer PLACEHOLDER_TOKEN' }
        });
        const data = await res.json();
        if (!data.ok) break;
        for (const u of data.members || []) {
          users.set(u.id, {
            id: u.id,
            name: u.profile?.display_name || u.real_name || u.name,
            real_name: u.real_name,
          });
        }
        cursor = data.response_metadata?.next_cursor;
      } while (cursor);
      return users;
    })(),
  ]);

  console.log(`Channel: ${channelInfo.name ? '#' + channelInfo.name : channelId}`);
  console.log(`  ${userMap.size} users loaded`);

  // Fetch messages
  const messages = [];
  let cursor = null;

  while (messages.length < maxMessages) {
    const remaining = maxMessages - messages.length;
    const params = new URLSearchParams({
      channel: channelId,
      limit: Math.min(remaining, 100).toString(),
    });
    if (cursor) params.set('cursor', cursor);

    const res = await fetch('https://slack.com/api/conversations.history?' + params, {
      headers: { 'Authorization': 'Bearer PLACEHOLDER_TOKEN' }
    });
    const data = await res.json();

    if (!data.ok) throw new Error(`conversations.history failed: ${data.error}`);
    messages.push(...(data.messages || []));
    cursor = data.response_metadata?.next_cursor;

    if (!cursor || data.messages?.length === 0) break;
  }

  console.log(`Fetched ${messages.length} messages`);

  // Transform messages
  const transformed = messages.slice(0, maxMessages).map(msg => {
    const user = userMap.get(msg.user) || { name: msg.user || 'unknown' };
    return {
      ts: msg.ts,
      user_id: msg.user,
      user_name: user.name,
      text: msg.text,
      thread_ts: msg.thread_ts,
      reply_count: msg.reply_count,
      reactions: msg.reactions?.map(r => ({ name: r.name, count: r.count })),
      attachments: msg.attachments?.length || 0,
      blocks: msg.blocks?.length || 0,
      time: new Date(parseFloat(msg.ts) * 1000).toISOString(),
    };
  });

  // Write results
  const dir = path.dirname(outputPath);
  if (dir && dir !== '.') fs.mkdirSync(dir, { recursive: true });

  const output = {
    channel: channelInfo,
    fetchedAt: new Date().toISOString(),
    count: transformed.length,
    messages: transformed,
  };

  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));

  console.log(`\n✓ Messages written to: ${outputPath}`);

  if (transformed.length > 0) {
    const oldest = transformed[transformed.length - 1];
    const newest = transformed[0];
    console.log(`  Time range: ${oldest.time.split('T')[0]} to ${newest.time.split('T')[0]}`);

    console.log(`\n  Recent messages:`);
    transformed.slice(0, 5).forEach(m => {
      const preview = m.text?.slice(0, 60) || '(no text)';
      console.log(`    ${m.user_name}: ${preview}${m.text?.length > 60 ? '...' : ''}`);
    });
  }

  console.log(JSON.stringify({
    success: true,
    outputPath,
    channel: channelInfo.name || channelId,
    count: transformed.length,
  }));

} catch (error) {
  console.error('Failed to fetch history:', error.message);
  throw error;
}
                  import fs from 'fs';
import path from 'path';

const [channelId, limit = '50', outputPath] = process.argv.slice(2);
const maxMessages = Math.min(parseInt(limit) || 50, 200);

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

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

console.log(`Fetching up to ${maxMessages} messages from ${channelId}...`);

try {
  // Fetch channel info and users in parallel
  const [channelInfo, userMap] = await Promise.all([
    (async () => {
      const params = new URLSearchParams({ channel: channelId });
      const res = await fetch('https://slack.com/api/conversations.info?' + params, {
        headers: { 'Authorization': 'Bearer PLACEHOLDER_TOKEN' }
      });
      const data = await res.json();
      if (!data.ok) return { id: channelId, name: null };
      const c = data.channel;
      return {
        id: c.id,
        name: c.name || null,
        is_private: c.is_private,
        is_im: c.is_im,
        is_mpim: c.is_mpim,
      };
    })(),

    (async () => {
      const users = new Map();
      let cursor = null;
      do {
        const params = new URLSearchParams({ limit: '200' });
        if (cursor) params.set('cursor', cursor);
        const res = await fetch('https://slack.com/api/users.list?' + params, {
          headers: { 'Authorization': 'Bearer PLACEHOLDER_TOKEN' }
        });
        const data = await res.json();
        if (!data.ok) break;
        for (const u of data.members || []) {
          users.set(u.id, {
            id: u.id,
            name: u.profile?.display_name || u.real_name || u.name,
            real_name: u.real_name,
          });
        }
        cursor = data.response_metadata?.next_cursor;
      } while (cursor);
      return users;
    })(),
  ]);

  console.log(`Channel: ${channelInfo.name ? '#' + channelInfo.name : channelId}`);
  console.log(`  ${userMap.size} users loaded`);

  // Fetch messages
  const messages = [];
  let cursor = null;

  while (messages.length < maxMessages) {
    const remaining = maxMessages - messages.length;
    const params = new URLSearchParams({
      channel: channelId,
      limit: Math.min(remaining, 100).toString(),
    });
    if (cursor) params.set('cursor', cursor);

    const res = await fetch('https://slack.com/api/conversations.history?' + params, {
      headers: { 'Authorization': 'Bearer PLACEHOLDER_TOKEN' }
    });
    const data = await res.json();

    if (!data.ok) throw new Error(`conversations.history failed: ${data.error}`);
    messages.push(...(data.messages || []));
    cursor = data.response_metadata?.next_cursor;

    if (!cursor || data.messages?.length === 0) break;
  }

  console.log(`Fetched ${messages.length} messages`);

  // Transform messages
  const transformed = messages.slice(0, maxMessages).map(msg => {
    const user = userMap.get(msg.user) || { name: msg.user || 'unknown' };
    return {
      ts: msg.ts,
      user_id: msg.user,
      user_name: user.name,
      text: msg.text,
      thread_ts: msg.thread_ts,
      reply_count: msg.reply_count,
      reactions: msg.reactions?.map(r => ({ name: r.name, count: r.count })),
      attachments: msg.attachments?.length || 0,
      blocks: msg.blocks?.length || 0,
      time: new Date(parseFloat(msg.ts) * 1000).toISOString(),
    };
  });

  // Write results
  const dir = path.dirname(outputPath);
  if (dir && dir !== '.') fs.mkdirSync(dir, { recursive: true });

  const output = {
    channel: channelInfo,
    fetchedAt: new Date().toISOString(),
    count: transformed.length,
    messages: transformed,
  };

  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));

  console.log(`\n✓ Messages written to: ${outputPath}`);

  if (transformed.length > 0) {
    const oldest = transformed[transformed.length - 1];
    const newest = transformed[0];
    console.log(`  Time range: ${oldest.time.split('T')[0]} to ${newest.time.split('T')[0]}`);

    console.log(`\n  Recent messages:`);
    transformed.slice(0, 5).forEach(m => {
      const preview = m.text?.slice(0, 60) || '(no text)';
      console.log(`    ${m.user_name}: ${preview}${m.text?.length > 60 ? '...' : ''}`);
    });
  }

  console.log(JSON.stringify({
    success: true,
    outputPath,
    channel: channelInfo.name || channelId,
    count: transformed.length,
  }));

} catch (error) {
  console.error('Failed to fetch history:', error.message);
  throw error;
}