1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Slackのチャンネルバックアップ方法(管理者以外・複数チャンネルをまとめて保存)

Posted at

概要

Slackの複数チャンネルの情報を同じGoogleスプレッドシート内に保存する手順.

背景・目的

  • 所属していたSlack Organizationを抜けることになった.
  • 自分が関わっていたチャンネルが多岐にわたるため,必要なメッセージの選別が面倒.
  • 複数チャンネルのバックアップを一括で取れるようにしたい.

バックアップ方法

Googleスプレッドシートを作成し,Slack APIを用いてその中の各シートに各チャンネルのメッセージを保存する.
スプレッドシートの作成・スクリプト編集まではLumitecの記事, "Slackのプライベートチャンネルをバックアップする方法"を参考に行った.

以下が手順である(各項目の詳細については元の記事を確認してください).

 ①「Slack App」を作成する
 ②「Slack App」のスコープを設定する
 ③トークンを取得する
 ④プライベートチャンネルのIDを取得する
 ⑤「Googleスプレッドシート」を準備する
 ⑥スクリプトを編集する
 ⑦スクリプトを実行する

ただし,スコープには以下の項目を追加する.

スコープに追加する項目
名前 説明
channels:history パブリックチャンネルのメッセージ履歴
groups:history プライベートチャンネルのメッセージ履歴
im:history DM(1対1)メッセージ履歴
mpim:history DM(複数)メッセージ履歴
users:read ユーザーのプロフィール情報

また,履歴を残したいチャンネルそれぞれに対して,Slackの画面右上の「:」から, 「チャンネル詳細を開く」→ 「インテグレーション」 を経由して作成したSlackアプリを追加するようにする.

Google Apps script内のコード

GoogleスプレッドシートのApps Scriptを開くところまで終わったら,以下のコードをmain.gs内に貼り付ける.

コードの全体像

main.gs

var OAuthAccessToken = "<Bot User OAuth Token>"; 
var channelIDs = [
  
  "<ChannelID_1>", //Channel name1
  "<ChannelID_2>", //Channel name2
  ...
]; // Add your channel IDs here

// Start date for fetching messages (e.g., "2021-01-01")
var startDate = new Date("2021-01-01");
var endDate = new Date();

function main() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

  // Loop through each channel ID
  channelIDs.forEach(function(channelID) {
    var sheet = getOrCreateSheet(spreadsheet, channelID);
    sheet.getRange('A2:H').clearContent(); // Clear previous data at the start

    // Store existing message IDs in a Set for uniqueness
    var existingMessages = new Set(sheet.getRange('A2:A' + sheet.getLastRow()).getValues().flat());

    // Loop through each month between startDate and endDate
    for (var currentDate = new Date(startDate); currentDate <= endDate; currentDate.setMonth(currentDate.getMonth() + 1)) {
      var oldestTimestamp = Math.floor(currentDate.getTime() / 1000);
      var nextMonth = new Date(currentDate);
      nextMonth.setMonth(currentDate.getMonth() + 1);
      var latestTimestamp = Math.floor(nextMonth.getTime() / 1000);

      Logger.log("Fetching messages from " + new Date(oldestTimestamp * 1000).toLocaleString() + " to " + new Date(latestTimestamp * 1000).toLocaleString() + " for channel: " + channelID);
      fetchMessagesForDateRange(oldestTimestamp, latestTimestamp, sheet, existingMessages, channelID);
    }
  });
}

// Function to get or create a sheet for the given channel ID
function getOrCreateSheet(spreadsheet, channelID) {
  var sheetName = channelID; // You can customize this naming convention
  var sheet = spreadsheet.getSheetByName(sheetName);
  
  if (!sheet) {
    sheet = spreadsheet.insertSheet(sheetName);
    sheet.appendRow(['Client Msg ID', 'Type', 'Text', 'User', 'Timestamp', 'Formatted Date', 'Reply Count', 'Reply Users Count']);
  }
  
  return sheet;
}

function fetchMessagesForDateRange(oldestTimestamp, latestTimestamp, sheet, existingMessages, channelID) {
  var fetchUrl = 'https://slack.com/api/conversations.history?channel=' + channelID + '&limit=1000&oldest=' + oldestTimestamp + '&latest=' + latestTimestamp;
  const options = {
    "headers": { 'Authorization': 'Bearer ' + OAuthAccessToken },
    "muteHttpExceptions": true
  };

  var messages = [];
  var hasMore = true;
  var cursor = '';

  do {
    var urlWithCursor = fetchUrl + (cursor ? '&cursor=' + cursor : '');
    var res;
    var retryCount = 0;
    var success = false;

    // Attempt to fetch with exponential backoff in case of rate limiting
    while (!success && retryCount < 5) {
      try {
        res = UrlFetchApp.fetch(urlWithCursor, options);
        success = true;
      } catch (e) {
        Logger.log("Error fetching messages: " + e);
        if (e.toString().includes('429')) {
          Utilities.sleep(Math.pow(2, retryCount) * 1000);
          retryCount++;
        } else {
          throw e;
        }
      }
    }

    if (!success) {
      throw new Error("Failed to fetch messages after multiple retries due to rate limiting.");
    }

    res = JSON.parse(res.getContentText());

    if (!res.ok) {
      Logger.log('Error fetching messages: ' + res.error);
      return; // Exit the function if the API call fails
    }

    Logger.log("Fetched " + res.messages.length + " messages for channel: " + channelID);

    var newMessages = [];
    res.messages.forEach(function (v) {
      if ('thread_ts' in v) {
        var threadUrl = 'https://slack.com/api/conversations.replies?channel=' + channelID + '&ts=' + v.thread_ts;
        var reply;
        var replyRetryCount = 0;
        var replySuccess = false;
        
        while (!replySuccess && replyRetryCount < 5) {
          try {
            reply = UrlFetchApp.fetch(threadUrl, options);
            replySuccess = true;
          } catch (e) {
            Logger.log("Error fetching replies: " + e);
            if (e.toString().includes('429')) {
              Utilities.sleep(Math.pow(2, replyRetryCount) * 1000);
              replyRetryCount++;
            } else {
              throw e;
            }
          }
        }

        if (!replySuccess) {
          throw new Error("Failed to fetch thread messages after multiple retries due to rate limiting.");
        }

        reply = JSON.parse(reply.getContentText());

        // Check if reply.messages exists before reversing
        if (reply.messages && Array.isArray(reply.messages)) {
          newMessages = newMessages.concat(reply.messages.reverse());
        }
      } else {
        newMessages.push(v);
      }
    });

    newMessages = newMessages.map(function (v) {
      var date = new Date(parseFloat(v.ts) * 1000); // Convert timestamp to Date
      var formattedDate = date.toLocaleString(); // Format date to readable format
      return {
        client_msg_id: v.client_msg_id || '',
        type: v.type || '',
        text: v.text || '',
        user: v.user || '',
        ts: v.ts || '',
        formattedDate: formattedDate, // Include formatted date
        reply_count: v.reply_count || 0,
        reply_users_count: v.reply_users_count || 0
      };
    });

    messages = messages.concat(newMessages);
    hasMore = res.has_more;
    cursor = res.response_metadata && res.response_metadata.next_cursor;

    Utilities.sleep(1000);  // Regular delay between requests
  } while (hasMore);

  // Append new messages for this date range
  if (messages.length > 0) {
    // Map user IDs to display names
    var userIds = [...new Set(messages.map(m => m.user))];
    var userDisplayNames = getUserDisplayNames(userIds);

    var finalMessages = messages.map(function (msg) {
      return [
        msg.client_msg_id,
        msg.type,
        msg.text,
        userDisplayNames[msg.user] || msg.user, // Use display name or fallback to user ID
        msg.ts,
        msg.formattedDate,
        msg.reply_count,
        msg.reply_users_count
      ];
    });

    // Filter out duplicates before appending
    finalMessages = finalMessages.filter(msg => !existingMessages.has(msg[0])); // Check against existing IDs

    // Append messages to the first available row
    if (finalMessages.length > 0) {
      sheet.getRange(sheet.getLastRow() + 1, 1, finalMessages.length, finalMessages[0].length).setValues(finalMessages.reverse());
      Logger.log("Appended " + finalMessages.length + " new messages to the sheet for channel: " + channelID);
    } else {
      Logger.log("No new messages to append for channel: " + channelID);
    }
  } else {
    Logger.log("No messages found for this date range for channel: " + channelID);
  }
}

// Function to get user display names
function getUserDisplayNames(userIds) {
  var displayNames = {};
  
  userIds.forEach(function(userId) {
    var userFetchUrl = 'https://slack.com/api/users.info?user=' + userId;
    const options = {
      "headers": { 'Authorization': 'Bearer ' + OAuthAccessToken },
      "muteHttpExceptions": true
    };

    var success = false;
    var retryCount = 0;

    while (!success && retryCount < 5) {
      try {
        var response = UrlFetchApp.fetch(userFetchUrl, options);
        var userInfo = JSON.parse(response.getContentText());
        
        if (userInfo.ok) {
          displayNames[userId] = userInfo.user.profile.display_name || userInfo.user.name; // Fallback to the user's real name if display_name is not set
        } else {
          Logger.log('Error fetching user info for ' + userId + ': ' + userInfo.error);
        }
        success = true;
      } catch (e) {
        Logger.log("Error fetching user info: " + e);
        if (e.toString().includes('429')) {
          Utilities.sleep(Math.pow(2, retryCount) * 1000);
          retryCount++;
        } else {
          throw e;
        }
      }
    }
  });

  return displayNames;
}


コードの概要

上のコードの大まかな説明を以下に記す.

1. 必要な変数の定義

var OAuthAccessToken = "<Bot User OAuth Token>"; 
var channelIDs = [
  "<ChannelID_1>", // Channel name1
  "<ChannelID_2>", // Channel name2
  // 他のチャンネルIDをここに追加
];

var startDate = new Date("2021-01-01"); // メッセージ取得開始日
var endDate = new Date(); // メッセージ取得終了日(今日)
  • OAuthAccessToken: Slack APIにアクセスするための認証トークン(Botユーザーのトークンを指定する).
  • channelIDs: メッセージを取得するSlackチャンネルのIDリスト.
  • startDateendDate: メッセージを取得する期間の開始日と終了日(終了日を指定しない場合は最新の日付になる).

2. メイン関数の実行

function main() {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

  channelIDs.forEach(function(channelID) {
    var sheet = getOrCreateSheet(spreadsheet, channelID);
    sheet.getRange('A2:H').clearContent();

    var existingMessages = new Set(sheet.getRange('A2:A' + sheet.getLastRow()).getValues().flat());

    for (var currentDate = new Date(startDate); currentDate <= endDate; currentDate.setMonth(currentDate.getMonth() + 1)) {
      var oldestTimestamp = Math.floor(currentDate.getTime() / 1000);
      var nextMonth = new Date(currentDate);
      nextMonth.setMonth(currentDate.getMonth() + 1);
      var latestTimestamp = Math.floor(nextMonth.getTime() / 1000);

      Logger.log("Fetching messages from " + new Date(oldestTimestamp * 1000).toLocaleString() + " to " + new Date(latestTimestamp * 1000).toLocaleString() + " for channel: " + channelID);
      fetchMessagesForDateRange(oldestTimestamp, latestTimestamp, sheet, existingMessages, channelID);
    }
  });
}
  • getOrCreateSheet 関数を呼び出してチャンネルごとのシートを作成・取得.
  • チャンネルIDごとにループして、指定期間内のメッセージを月ごとに取得し、スプレッドシートに出力.

3. シート作成・取得関数

function getOrCreateSheet(spreadsheet, channelID) {
  var sheetName = channelID;
  var sheet = spreadsheet.getSheetByName(sheetName);
  
  if (!sheet) {
    sheet = spreadsheet.insertSheet(sheetName);
    sheet.appendRow(['Client Msg ID', 'Type', 'Text', 'User', 'Timestamp', 'Formatted Date', 'Reply Count', 'Reply Users Count']);
  }
  
  return sheet;
}
  • チャンネルごとのシートが存在しない場合は新規作成する.
  • ヘッダー行を追加する.

4. メッセージ取得関数

function fetchMessagesForDateRange(oldestTimestamp, latestTimestamp, sheet, existingMessages, channelID) {
  var fetchUrl = 'https://slack.com/api/conversations.history?channel=' + channelID + '&limit=1000&oldest=' + oldestTimestamp + '&latest=' + latestTimestamp;
  const options = {
    "headers": { 'Authorization': 'Bearer ' + OAuthAccessToken },
    "muteHttpExceptions": true
  };

  var messages = [];
  var hasMore = true;
  var cursor = '';

  do {
    var urlWithCursor = fetchUrl + (cursor ? '&cursor=' + cursor : '');
    var res;
    var retryCount = 0;
    var success = false;

    while (!success && retryCount < 5) {
      try {
        res = UrlFetchApp.fetch(urlWithCursor, options);
        success = true;
      } catch (e) {
        Logger.log("Error fetching messages: " + e);
        if (e.toString().includes('429')) {
          Utilities.sleep(Math.pow(2, retryCount) * 1000);
          retryCount++;
        } else {
          throw e;
        }
      }
    }

    if (!success) {
      throw new Error("Failed to fetch messages after multiple retries due to rate limiting.");
    }

    res = JSON.parse(res.getContentText());

    if (!res.ok) {
      Logger.log('Error fetching messages: ' + res.error);
      return;
    }

    Logger.log("Fetched " + res.messages.length + " messages for channel: " + channelID);

    var newMessages = [];
    res.messages.forEach(function (v) {
      if ('thread_ts' in v) {
        var threadUrl = 'https://slack.com/api/conversations.replies?channel=' + channelID + '&ts=' + v.thread_ts;
        var reply;
        var replyRetryCount = 0;
        var replySuccess = false;
        
        while (!replySuccess && replyRetryCount < 5) {
          try {
            reply = UrlFetchApp.fetch(threadUrl, options);
            replySuccess = true;
          } catch (e) {
            Logger.log("Error fetching replies: " + e);
            if (e.toString().includes('429')) {
              Utilities.sleep(Math.pow(2, replyRetryCount) * 1000);
              replyRetryCount++;
            } else {
              throw e;
            }
          }
        }

        if (!replySuccess) {
          throw new Error("Failed to fetch thread messages after multiple retries due to rate limiting.");
        }

        reply = JSON.parse(reply.getContentText());

        if (reply.messages && Array.isArray(reply.messages)) {
          newMessages = newMessages.concat(reply.messages.reverse());
        }
      } else {
        newMessages.push(v);
      }
    });

    newMessages = newMessages.map(function (v) {
      var date = new Date(parseFloat(v.ts) * 1000);
      var formattedDate = date.toLocaleString();
      return {
        client_msg_id: v.client_msg_id || '',
        type: v.type || '',
        text: v.text || '',
        user: v.user || '',
        ts: v.ts || '',
        formattedDate: formattedDate,
        reply_count: v.reply_count || 0,
        reply_users_count: v.reply_users_count || 0
      };
    });

    messages = messages.concat(newMessages);
    hasMore = res.has_more;
    cursor = res.response_metadata && res.response_metadata.next_cursor;

    Utilities.sleep(1000);
  } while (hasMore);

  if (messages.length > 0) {
    var userIds = [...new Set(messages.map(m => m.user))];
    var userDisplayNames = getUserDisplayNames(userIds);

    var finalMessages = messages.map(function (msg) {
      return [
        msg.client_msg_id,
        msg.type,
        msg.text,
        userDisplayNames[msg.user] || msg.user,
        msg.ts,
        msg.formattedDate,
        msg.reply_count,
        msg.reply_users_count
      ];
    });

    finalMessages = finalMessages.filter(msg => !existingMessages.has(msg[0]));

    if (finalMessages.length > 0) {
      sheet.getRange(sheet.getLastRow() + 1, 1, finalMessages.length, finalMessages[0].length).setValues(finalMessages.reverse());
      Logger.log("Appended " + finalMessages.length + " new messages to the sheet for channel: " + channelID);
    } else {
      Logger.log("No new messages to append for channel: " + channelID);
    }
  } else {
    Logger.log("No messages found for this date range for channel: " + channelID);
  }
}
  • fetchMessagesForDateRange:Slack APIを使用して指定した期間内のメッセージを取得し、スプレッドシートに追加する.

5. ユーザー名取得関数

function getUserDisplayNames(userIds) {
  var displayNames = {};
  
  userIds.forEach(function(userId) {
    var userFetchUrl = 'https://slack.com/api/users.info?user=' + userId;
    const options = {
      "headers": { 'Authorization': 'Bearer ' + OAuthAccessToken },
      "muteHttpExceptions": true
    };

    var success = false;
    var retryCount = 0;

    while (!success && retryCount < 5) {
      try {
        var response = UrlFetchApp.fetch(userFetchUrl, options);
        var userInfo = JSON.parse(response.getContentText());
        
        if (userInfo.ok) {
          displayNames[userId] = userInfo.user.profile.display_name || userInfo.user.name;
        } else {
          Logger.log('Error fetching user info for ' + userId + ': ' + userInfo.error);
        }
        success = true;
      } catch (e) {
        Logger.log("Error fetching user info: " + e);
        if (e.toString().includes('429')) {
          Utilities.sleep(Math.pow(2, retryCount) * 1000);
          retryCount++;
        } else {
          throw e;
        }


      }
    }
  });

  return displayNames;
}
  • getUserDisplayNames:Slack APIからユーザーの表示名を取得する(スプレッドシートの"User"列に表示名を出力するため).

コメント

今回初めて記事作成に挑戦してみました.至らぬ点もあると思いますので,内容等に関しまして何かご指摘・質問がありましたら連絡いただけますと幸いです.

参考文献・関連ページ

本ページの作成およびコード作成に関しては,ChatGPTの助けを借りました.

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?