見出し画像

【組織のAI活用#222】Slackのメッセージをスプレッドシートへ自動転記するGAS

こんにちは!寺田です。

現在は、デジタル関連会社のAI推進者などのAI活用者のコミュニティである「AI Digital Community (ADC)」の代表理事や、顧客事業の「×AI化」をサポートするFURIKAKE Partners(株)と組織のAI活用を支援するプロダクトを提供するAI Portalize(株)の代表取締役を務めており、様々な側面から組織のAI活用をサポートしています!

「Slackの報告チャンネルの内容をまとめてAIボットをつくりたい」「問い合わせチャンネルのやり取りをもとに、FAQをAIでつくりたい」といったように、Slackのメッセージをスプレッドシートに蓄積できると、集計・検索・FAQ化など活用の幅が一気に広がります。この仕組みはGoogle Apps Script(GAS)だけで構築できます。本記事では、Slack App(Bot)の作成からGASコード全文、スレッド返信の取得、トリガー設定までを通しで解説します。

AI を活用することで、複雑な GAS のコード生成も簡単にできるようになっていますし、手順の詳細まで案内してくれるようになっています。

また、使う AI によっては設定作業の代行まで全部行ってくれますので、非常にやりやすくなっています。

さらに、AI を活用して何かをやっていく中で、散らばった色々な情報を一箇所にまとめていくことで、別の価値に転換するということもできますので、そういった観点でも見てみてください。


作るものと設計方針

今回作るのは、「指定した1つのチャンネルの新着メッセージを、1時間おきにスプレッドシートへ追記する」仕組みです。設計のポイントは3つあります。

  • 定期取得(Pull型):GASが1時間おきにSlackのAPIを呼び、前回取得分以降の差分だけを転記します。リアルタイム通知ではありませんが、ログ蓄積用途には十分で、構成がシンプルで壊れにくい方式です。

  • 対象チャンネルを限定する:全チャンネルを対象にすると、書き出すボリュームが膨大になり、実行時間や管理の面で破綻しやすくなります。まずは目的のはっきりした1チャンネルに絞るのがおすすめです。

  • スレッド返信も取得できる:コード冒頭のスイッチ(INCLUDE_THREADS)で、スレッド内の返信まで取得するかを切り替えられます。

Slack App(Bot)の作成とTokenの取得

GASからSlackのメッセージを読むには、Slack Appを作成して「Bot Token」を発行する必要があります。

1. api.slack.com/apps を開き、「Create New App」→「From scratch」を選択。App名(例:ログ転記Bot)と対象ワークスペースを設定します。

2. 左メニューの「OAuth & Permissions」を開き、「Bot Token Scopes」に次のスコープを追加します:channels:history(メッセージ読み取り)、channels:read(チャンネル情報)。※プライベートチャンネルを対象にする場合は groups:history と groups:read を追加します。

3. 同じ画面上部の「Install to Workspace」でアプリをワークスペースにインストールし、表示される「Bot User OAuth Token」(xoxb- で始まる文字列)をコピーして控えます。

4. Slackで対象チャンネルを開き、メッセージ欄に「/invite @ログ転記Bot」と入力してBotをチャンネルに招待します。この招待を忘れると not_in_channel エラーになります(つまずきの定番です)。

5. 対象チャンネルのIDを控えます。チャンネル名をクリックして開く詳細画面の最下部に「チャンネルID: C0XXXXXXXXX」と表示されています。

スプレッドシートの準備

転記先のスプレッドシートを新規作成し、シート名を「Slackログ」にして、1行目に次のヘッダーを入力しておきます。

  • A列: timestamp/B列: 日時/C列: ユーザーID/D列: テキスト/E列: スレッドTS/F列: 返信数

E列の「スレッドTS」は、そのメッセージがどのスレッドに属するかを示すIDです。親メッセージと返信が同じ値を持つため、後からスレッド単位でまとめ直すときに使えます。

コード全文

準備したスプレッドシートの「拡張機能」→「Apps Script」を開き、以下を貼り付けて、冒頭の設定を書き換えてください。


// ======== 設定(自分の環境に合わせて書き換える) ========
const SLACK_TOKEN = 'ここにBot User OAuth Token(xoxb-〜)を貼り付け';
const CHANNEL_ID = 'ここにチャンネルID(C0〜)を貼り付け';
const SHEET_NAME = 'Slackログ';  // 書き込み先シート名
const INCLUDE_THREADS = true;    // スレッド返信も取得するか(不要なら false)

// メイン関数:前回以降の新着メッセージをシートへ転記(1時間おきトリガーで実行)
function syncSlackToSheet() {
  const props = PropertiesService.getScriptProperties();
  // 初回は過去24時間分から開始(全履歴の一括取得によるタイムアウトを防ぐ)
  const lastTs = props.getProperty('lastTimestamp') || String(Date.now() / 1000 - 60 * 60 * 24);

  const messages = fetchMessages_(lastTs);
  if (messages.length === 0) {
    console.log('新着メッセージはありません');
    return;
  }

  // 時系列(昇順)に並べ替えてから追記
  messages.sort(function(a, b) { return parseFloat(a.ts) - parseFloat(b.ts); });

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  const rows = messages.map(buildRow_);
  sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);

  // 最新のタイムスタンプを保存(次回はここ以降だけを取得する)
  props.setProperty('lastTimestamp', messages[messages.length - 1].ts);
  console.log(rows.length + ' 件を転記しました');
}

// チャンネルの新着メッセージを取得(ページネーション・スレッド対応)
function fetchMessages_(oldest) {
  let all = [];
  let cursor = '';
  do {
    const params = { channel: CHANNEL_ID, oldest: oldest, limit: 200 };
    if (cursor) params.cursor = cursor;
    const res = callSlackApi_('conversations.history', params);
    all = all.concat(res.messages || []);
    cursor = res.response_metadata ? (res.response_metadata.next_cursor || '') : '';
    if (cursor) Utilities.sleep(500); // API負荷への配慮
  } while (cursor);

  // スレッド返信の取得(返信のある親メッセージだけAPIをもう1回呼ぶ)
  if (INCLUDE_THREADS) {
    const parents = all.filter(function(m) { return m.reply_count && m.reply_count > 0; });
    parents.forEach(function(parent) {
      const res = callSlackApi_('conversations.replies', {
        channel: CHANNEL_ID, ts: parent.thread_ts || parent.ts, limit: 200
      });
      const replies = (res.messages || []).filter(function(m) {
        return m.ts !== (parent.thread_ts || parent.ts)  // 親メッセージ自身を除外
            && m.subtype !== 'thread_broadcast'           // 重複の定番原因を除外
            && parseFloat(m.ts) > parseFloat(oldest);     // 取得済み分を除外
      });
      all = all.concat(replies);
      Utilities.sleep(300);
    });
  }
  // チャンネル側にも混ざるthread_broadcastを除外
  return all.filter(function(m) { return m.subtype !== 'thread_broadcast'; });
}

// メッセージ1件を行データに変換
function buildRow_(msg) {
  return [
    msg.ts,                        // A: Slackのタイムスタンプ
    tsToJst_(msg.ts),              // B: 日時(日本時間)
    msg.user || msg.bot_id || '',  // C: ユーザーID
    msg.text || '',                // D: テキスト
    msg.thread_ts || '',           // E: スレッドTS
    msg.reply_count || 0           // F: 返信数
  ];
}

// Slackのタイムスタンプを日本時間の文字列に変換
function tsToJst_(ts) {
  const d = new Date(parseFloat(ts) * 1000);
  return Utilities.formatDate(d, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
}

// Slack Web APIの共通呼び出し
function callSlackApi_(method, params) {
  const query = Object.keys(params).map(function(k) {
    return k + '=' + encodeURIComponent(params[k]);
  }).join('&');
  const res = UrlFetchApp.fetch('https://slack.com/api/' + method + '?' + query, {
    headers: { Authorization: 'Bearer ' + SLACK_TOKEN },
    muteHttpExceptions: true
  });
  const json = JSON.parse(res.getContentText());
  if (!json.ok) throw new Error('Slack APIエラー: ' + json.error + ' (' + method + ')');
  return json;
}

// 初回に一度だけ手動実行:1時間おきのトリガーを登録
function setupTrigger() {
  ScriptApp.getProjectTriggers().forEach(function(t) {
    if (t.getHandlerFunction() === 'syncSlackToSheet') ScriptApp.deleteTrigger(t);
  });
  ScriptApp.newTrigger('syncSlackToSheet').timeBased().everyHours(1).create();
}

コードのポイント

  • 差分取得の仕組み:前回転記した最後のメッセージのタイムスタンプをGASのプロパティに保存し、次回は「それ以降」だけをAPIに要求します。何度実行しても重複しない作りです。

  • 初回のボリューム対策:初回実行時は過去24時間分からスタートする設計にしています。数年分の全履歴をいきなり取得するとGASの実行時間制限(約6分)に達するためです。過去ログも欲しい場合は、初回だけ期間を区切って複数回実行してください。

  • スレッド返信:INCLUDE_THREADS が true のとき、返信付きの親メッセージに対してだけ追加のAPI呼び出しを行います。なお、この方式は「新着の親メッセージのスレッド」を取得する設計のため、ずっと前の親メッセージに後日付いた返信は対象外になります。厳密に全返信を追いたい場合は取得間隔を短くするより、月次で全体を取り直す運用を併用するのが現実的です。

トリガー設定と初回実行

1. コードを保存し、関数選択で「syncSlackToSheet」を選んで実行します。初回は権限の承認画面が出るので許可します(「確認されていません」警告は「詳細」→「移動」で進めます)。



2. シートにメッセージが転記されることを確認したら、関数選択で「setupTrigger」を選んで実行します。これで1時間おきの自動実行が設定されます。

3. 左メニューの時計アイコン「トリガー」で、syncSlackToSheetのトリガーが登録されていることを確認します。


動作確認チェックリスト

  • 対象チャンネルに新しいメッセージを投稿→次の実行後にシートへ追記されるか

  • 同じメッセージが重複して転記されていないか

  • スレッドに返信を付けた場合、返信も転記されるか(INCLUDE_THREADS = true時)

  • GASエディタの「実行数」画面でエラーが出ていないか

うまく動かないときは

  • not_in_channel エラー → Botを対象チャンネルに招待していません。「/invite @Bot名」を実行してください。

  • invalid_auth エラー → Tokenの貼り付けミスか、Appの再インストールが必要です。「Bot User OAuth Token」を確認してください。

  • 何も取得されない → チャンネルIDの間違い(Cで始まるIDか)、またはlastTimestampが進みすぎている可能性があります。プロパティを一度削除して再実行してください。

  • 実行がタイムアウトする → 取得対象が多すぎます。初回の対象期間を短くする、INCLUDE_THREADSをfalseにする、などで量を減らしてください。

社内で導入する際のセキュリティ注意点

  • 導入前に社内の承認を得る:Slack Appの作成・インストールは、ワークスペースの設定によって管理者承認が必要な場合があります。また、業務チャットのログを別の場所(スプレッドシート)に複製する行為そのものが、社内のセキュリティポリシーや情報管理規程の対象になり得ます。情報システム部門や上長に確認してから進めましょう。

  • 転記先の共有範囲に注意:チャンネルでは限られたメンバーしか見られなかった内容が、スプレッドシートの共有設定次第で広く見えてしまいます。転記先の共有範囲は、元チャンネルの参加メンバーと同等以下にするのが原則です。

  • Bot Tokenは秘密情報として扱う:Tokenがあれば、権限の範囲でワークスペースの情報へアクセスできてしまいます。コードに直書きしたスクリプトを安易に共有しない、公開ドキュメントに貼らない、を徹底してください。

やってしまいがちな危険な設定例も挙げておきます。

  • 機密度の高いチャンネル(人事・経営情報など)を軽い気持ちで転記対象にする

  • 転記先スプレッドシートを「リンクを知っている全員」に共有する

  • 退職者が作ったBot・スクリプトが放置され、誰も管理していないのに動き続ける(作成者と管理者をドキュメントに残しておく)

応用

  • 複数チャンネルの転記:チャンネルIDを配列にしてループすれば拡張できます。ただし対象を増やすほど実行時間とログ量が増えるため、「必要なチャンネルだけ」の原則は保ってください。

  • 蓄積ログの活用:たまったログはAIとの相性が抜群です。特定キーワードで絞り込んでFAQの元データにする、NotebookLMに読み込ませて「チャンネルの過去のやり取りに答えるボット」を作る、といった展開ができます(大量データを扱う際は、Excelでの事前絞り込み=段階処理も思い出してください)。

Slackのやり取りを、チームの資産に変える

Slack→スプレッドシートの転記は、Slack App(Bot Token)の作成、コード貼り付けと設定4行の書き換え、setupTriggerの実行で完成します。1時間おきの差分取得・チャンネル限定・スレッドはスイッチで選択、という今回の設計は、ボリュームを抑えながら実務で回し続けられる構成です。蓄積したログは検索・集計からAI活用まで広がる資産になります。まずは1チャンネルから始めてみてください。

ぜひ、まずは手動での1回実行から、試してみていただければなと思います!



読んでいただきありがとうございました!
過去のAI関連記事はこちらです!

ちなみに、組織におけるAIの推進についてまとめた本も出版させていただいております。もしよければご覧ください。


<自己紹介>
直近までは、電通グループに属する約1400人・20社以上の事業会社群で構成されるCARTA HOLDINGSにて、全社横断のAI推進室で組織全体のAI活用推進を担いつつ、法人向けの「生成AI&デジマ人材」研修サービスであるD-Marketing Academyの代表取締役も兼務し、大手企業からスタートアップまで、数百社にわたるAI人材育成を支援してきました。

現在は、デジタル関連会社のAI推進や担当コミュニティである「AI Digital Community (ADC)」の代表理事や、顧客事業の「×AI化」をサポートするFURIKAKE Partners(株)と組織のAI活用を支援するプロダクトを提供するAI Portalize(株)の代表取締役を務めており、様々な側面から組織のAI活用をサポートしています!

<略歴>
2005年 5月 大学在学時にEC事業を開始
2007年 5月 (株)サイバーエージェントに入社し新規事業の立ち上げに携わる
2011年 10月 (株)VOYAGE GROUPにてKDDIとの協業事業を行う(株)Flesselを設立し代表取締役に就任
2015年 11月 ECコンサルティング事業を行う(株)JSコンサルティングの代表取締役へ就任
2018年 4月 東証一部プライム企業Hamee(株)にJSコンサルティングをM&Aし、代表取締役を継続
2019年 5月 Hamee(株)の執行役員に就任し、Hameeグループの新規事業領域を管轄
2021年 2月 D2C支援を行うTHE CHOSEN ONE(株)の顧問に就任
2021年 3月 アパレルD2C事業を行う(株)NAAFYの取締役に就任
2021年 4月 D-Marketing Academy(株)を設立し代表取締役に就任
2023年 1月 (株)CARTA HOLDINGSにD-Marketing AcademyをM&Aし、代表取締役を継続
2025年 3月 CARTA HOLDINGSグループ全体のAI活用促進を行うAI推進室を兼務開始
2026年 1月 生成AIに関しての顧問を行うFURIKAKE Partners(株)を設立し、代表取締役に就任
2026年 1月 組織の生成AIプラットフォームサービスAI Portalize(株)を設立し、代表取締役に就任
2026年 1月 デジタル関連のAI活用企業コミュニティ「AI Digital Community(ADC)」を設立し代表理事に就任

いいなと思ったら応援しよう!