スポンサーリンク

Gemini、Claudeの協力でSlackの投稿をGoogleドキュメントに自動保存するようにした!

きっかけ。

コムギコ:資本主義をハックしろ!!
コムギコ:資本主義をハックしろ!!
毎日ニュース100本を読むビジネス系VTuberのビデオポッドキャスト。 リサーチャー・編集者のコムギが、日々の出来事に鋭く「なんでやねん!」とツッコミを入れる番組です。

最近、このポッドキャストを聴いておりまして。
そこで”ジャーナリング”がとりあげられていて。
基本、きまった時間で思考を紙に書き出す、
というような習慣のようなのですが。

でもって、
最近はじめた”ひとりSlack”でコレ、できないかしら、
と思いたったわけです。
しかしながら、無料会員だとSlackのメッセージ保存期間は決まっていて。
そこで、その制限から逃がれるための方法として、
Slackへの投稿をGoogleドキュメントに自動保存することにしたのです。

Googleドキュメントに保存すると、
ただ保存するだけではなく、Geminiでの分析、整理も可能になります。
情報漏洩などは意識しなければならないですが、
基本、私個人に焦点をしぼれば、もれてこまることはないわけで。
Googleにもれる分にはね。
さすがに支払い情報などは論外にして、
明日の予定、ラジオのあしたのテーマとか、
そういうのはGoogleに知られたとして問題はないので。
まぁ、あそぶ面では問題なかろう、と判断したのでした。

ここから下は基本、Gemini、Claudeの作となります。

⚠️ 作業を始める前の重要な注意

この記事で扱う 「GASのウェブアプリURL」「Slackのトークン」 は、
パスワードと同じくらい重要な機密情報です。
ブログやSNSのスクリーンショットに写り込んだりしないよう、
管理には十分注意してください。

Slack投稿をGoogleドキュメントに自動保存する方法

Slack APIとGoogle Apps Script(GAS)を使って、Slackの投稿をGoogleドキュメントに自動保存するシステムの構築手順です。

完成イメージ

Slackに投稿すると、指定したGoogleドキュメントに以下の形式で自動追記されます:

----------------------------------
[#チャンネル名] - 2026/01/10 12:34:56
投稿内容がここに表示されます

2つの構成パターン

本マニュアルでは2つの構成を紹介します。

パターン説明用途
パターンA全投稿を1つのドキュメントに保存シンプルに始めたい場合
パターンBチャンネルごとに別ドキュメントへ振り分けジャーナリング等で分類したい場合

Step 1〜4は共通、Step 5でパターンを選択してください。

必要なもの

  • Googleアカウント
  • Slackワークスペースの管理者権限(またはアプリ追加権限)

Step 1: Googleドキュメントの準備

パターンA(単一ドキュメント)の場合

  1. Google ドキュメントで新しいドキュメントを1つ作成
  2. URLからドキュメントIDをコピー
  • URL例:https://docs.google.com/document/d/【このIDをコピー】/edit

パターンB(チャンネル振り分け)の場合

用途ごとに複数のドキュメントを作成し、それぞれのIDをメモしておきます。

例:ジャーナリング用途

ドキュメント名対象チャンネル
Journal – 行動ログj-done, j-tech
Journal – 思考ログj-mind, j-idea
Journal – 受信ログj-inbox

Step 2: Slack Appの作成

2-1. アプリ作成

  1. Slack APIにアクセス
  2. 「Create New App」→「From scratch」を選択
  3. アプリ名とワークスペースを設定して作成

2-2. 権限設定(OAuth & Permissions)

左メニューの「OAuth & Permissions」→「Scopes」→「Bot Token Scopes」に以下を追加:

スコープ用途
channels:historyパブリックチャンネルのメッセージ取得
channels:readチャンネル情報の取得
groups:historyプライベートチャンネルのメッセージ取得(必要な場合)
groups:readプライベートチャンネル情報の取得(必要な場合)

2-3. アプリのインストール

  1. 「OAuth & Permissions」ページ上部の「Install to Workspace」をクリック
  2. 権限を確認して「許可する」
  3. 表示される「Bot User OAuth Token」(xoxb-で始まる)をコピー

2-4. Verification Tokenの取得

  1. 左メニューの「Basic Information」→「App Credentials」
  2. 「Verification Token」をコピー

Step 3: Google Apps Scriptの作成

  1. Google Apps Scriptにアクセス
  2. 「新しいプロジェクト」を作成
  3. Step 5のコードを貼り付け

Step 4: SlackとGASの連携

4-1. 権限の承認

  1. GASエディタでtestRun関数を選択
  2. 「実行」ボタンをクリック
  3. Googleアカウントの認証画面が表示されるので許可

4-2. デプロイ

  1. 「デプロイ」→「新しいデプロイ」
  2. 種類:「ウェブアプリ」を選択
  3. 設定:
  • 説明:任意(例:Slack連携 v1)
  • 次のユーザーとして実行:「自分」
  • アクセスできるユーザー:「全員」(重要)
  1. 「デプロイ」をクリック
  2. 表示される「ウェブアプリのURL」をコピー

4-3. Event Subscriptionsの設定

  1. Slack API管理画面で左メニューの「Event Subscriptions」
  2. 「Enable Events」をON
  3. 「Request URL」にGASのウェブアプリURLを貼り付け
  4. 「Verified」と表示されることを確認

4-4. 購読イベントの追加

「Subscribe to bot events」で以下を追加:

イベント用途
message.channelsパブリックチャンネルのメッセージ
message.groupsプライベートチャンネルのメッセージ(必要な場合)

「Save Changes」をクリック

4-5. Botをチャンネルに招待

対象のSlackチャンネルで以下を実行:

/invite @あなたのBotの名前

Step 5: コードの設定

パターンA: 単一ドキュメントに保存

全ての投稿を1つのGoogleドキュメントに保存するシンプルな構成です。

// ==================== 設定 ====================
const CONFIG = {
  // GoogleドキュメントのID(URLから取得)
  DOCUMENT_ID: 'ここにドキュメントIDを入力',

  // Slack Bot User OAuth Token(xoxb-で始まる)
  SLACK_BOT_TOKEN: 'ここにBot Tokenを入力',

  // Slack Verification Token
  VERIFICATION_TOKEN: 'ここにVerification Tokenを入力',

  // 対象チャンネルID(空配列で全チャンネル対象)
  // 例: ['C01ABC2DEF', 'C02XYZ3GHI']
  TARGET_CHANNEL_IDS: [],
};

// ==================== メイン処理 ====================

function doPost(e) {
  console.log('Received request');

  let payload;
  try {
    payload = JSON.parse(e.postData.contents);
  } catch (error) {
    console.error('JSON parse error:', error);
    return ContentService.createTextOutput("Invalid JSON")
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // 1. URL Verification
  if (payload.type === 'url_verification') {
    console.log('URL Verification request received');
    return ContentService.createTextOutput(payload.challenge)
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // 2. トークン検証
  if (payload.token !== CONFIG.VERIFICATION_TOKEN) {
    console.error('Token verification failed!');
    return ContentService.createTextOutput("Invalid token")
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // 3. 重複防止
  const cache = CacheService.getScriptCache();
  const eventId = payload.event_id;
  if (cache.get(eventId)) {
    console.log('Duplicate event ignored:', eventId);
    return ContentService.createTextOutput("OK")
      .setMimeType(ContentService.MimeType.TEXT);
  }
  cache.put(eventId, "true", 600);

  // 4. メッセージイベントの処理
  try {
    if (payload.event && payload.event.type === 'message' && !payload.event.subtype) {
      processEvent(payload.event);
    }
  } catch (error) {
    console.error('processEvent Error:', error);
  }

  return ContentService.createTextOutput('OK')
    .setMimeType(ContentService.MimeType.TEXT);
}

// --- イベントを処理してドキュメントに送る関数 ---
function processEvent(event) {
  const channelId = event.channel;

  // 指定チャンネル以外を無視するチェック
  if (CONFIG.TARGET_CHANNEL_IDS.length > 0 && !CONFIG.TARGET_CHANNEL_IDS.includes(channelId)) {
    console.log('Channel not in target list:', channelId);
    return;
  }

  // チャンネル名を取得
  const channelName = getChannelName(channelId);
  const text = event.text;
  const timestamp = new Date(event.ts * 1000).toLocaleString('ja-JP'); 

  // ドキュメントへ保存
  appendToDocument(channelName, text, timestamp);
  console.log('Message saved:', channelName, text);
}

// --- Slack APIからチャンネル名を取得する関数 ---
function getChannelName(channelId) {
  const url = `https://slack.com/api/conversations.info?channel=${channelId}`;
  const options = {
    method: 'get',
    headers: {
      'Authorization': `Bearer ${CONFIG.SLACK_BOT_TOKEN}`
    }
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const resJson = JSON.parse(response.getContentText());
    if (resJson.ok) {
      return resJson.channel.name;
    } else {
      console.error('conversations.info API error:', resJson.error);
      return 'unknown-channel';
    }
  } catch (e) {
    console.error('getChannelName error:', e);
    return 'error-fetching-name';
  }
}

// --- Googleドキュメントに追記する関数 ---
function appendToDocument(channelName, text, timestamp) {
  const doc = DocumentApp.openById(CONFIG.DOCUMENT_ID);
  const body = doc.getBody();

  // 区切り線(グレー)
  body.appendParagraph('----------------------------------').setForegroundColor('#cccccc');
  // ヘッダー(黒・太字)
  body.appendParagraph(`[#${channelName}] - ${timestamp}`).setBold(true).setForegroundColor('#000000');
  // 本文(黒・通常)
  body.appendParagraph(text).setBold(false).setForegroundColor('#000000');
}

// --- 権限承認用テスト関数 ---
function testRun() {
  console.log("認証のためのテスト実行");
  const doc = DocumentApp.openById(CONFIG.DOCUMENT_ID); 
  console.log('Document name:', doc.getName());
}

// --- ブラウザからのアクセス確認用 ---
function doGet(e) {
  return ContentService.createTextOutput("OK - GASは正常に動作しています。このURLをSlackに設定してください。")
    .setMimeType(ContentService.MimeType.TEXT);
}
設定項目(パターンA)
項目説明
DOCUMENT_ID保存先GoogleドキュメントのID
SLACK_BOT_TOKENBot Token(xoxb-で始まる)
VERIFICATION_TOKENVerification Token
TARGET_CHANNEL_IDS対象チャンネルIDの配列(空配列で全チャンネル)

パターンB: チャンネルごとに振り分け

チャンネルごとに異なるGoogleドキュメントへ保存する構成です。ジャーナリングや用途別の分類に適しています。

// ==================== 設定 ====================
const CONFIG = {
  // Slack Bot User OAuth Token(xoxb-で始まる)
  SLACK_BOT_TOKEN: 'ここにBot Tokenを入力',

  // Slack Verification Token
  VERIFICATION_TOKEN: 'ここにVerification Tokenを入力',

  // チャンネルIDとドキュメントIDのマッピング
  CHANNEL_DOCUMENT_MAP: {
    // Journal - 行動ログ
    'C_j-done_のチャンネルID': 'Journal_行動ログのドキュメントID',
    'C_j-tech_のチャンネルID': 'Journal_行動ログのドキュメントID',

    // Journal - 思考ログ
    'C_j-mind_のチャンネルID': 'Journal_思考ログのドキュメントID',
    'C_j-idea_のチャンネルID': 'Journal_思考ログのドキュメントID',

    // Journal - 受信ログ
    'C_j-inbox_のチャンネルID': 'Journal_受信ログのドキュメントID',
  },

  // マッピングにないチャンネルのデフォルト保存先(空文字で保存しない)
  DEFAULT_DOCUMENT_ID: '',
};

// ==================== メイン処理 ====================

function doPost(e) {
  console.log('Received request');

  let payload;
  try {
    payload = JSON.parse(e.postData.contents);
  } catch (error) {
    console.error('JSON parse error:', error);
    return ContentService.createTextOutput("Invalid JSON")
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // 1. URL Verification
  if (payload.type === 'url_verification') {
    console.log('URL Verification request received');
    return ContentService.createTextOutput(payload.challenge)
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // 2. トークン検証
  if (payload.token !== CONFIG.VERIFICATION_TOKEN) {
    console.error('Token verification failed!');
    return ContentService.createTextOutput("Invalid token")
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // 3. 重複防止
  const cache = CacheService.getScriptCache();
  const eventId = payload.event_id;
  if (cache.get(eventId)) {
    console.log('Duplicate event ignored:', eventId);
    return ContentService.createTextOutput("OK")
      .setMimeType(ContentService.MimeType.TEXT);
  }
  cache.put(eventId, "true", 600);

  // 4. メッセージイベントの処理
  try {
    if (payload.event && payload.event.type === 'message' && !payload.event.subtype) {
      processEvent(payload.event);
    }
  } catch (error) {
    console.error('processEvent Error:', error);
  }

  return ContentService.createTextOutput('OK')
    .setMimeType(ContentService.MimeType.TEXT);
}

// --- イベントを処理してドキュメントに送る関数 ---
function processEvent(event) {
  const channelId = event.channel;

  // チャンネルに対応するドキュメントIDを取得
  const documentId = CONFIG.CHANNEL_DOCUMENT_MAP[channelId] || CONFIG.DEFAULT_DOCUMENT_ID;

  // 保存先がなければ無視
  if (!documentId) {
    console.log('No document configured for channel:', channelId);
    return;
  }

  const channelName = getChannelName(channelId);
  const text = event.text;
  const timestamp = new Date(event.ts * 1000).toLocaleString('ja-JP'); 

  // ドキュメントIDを渡して保存
  appendToDocument(documentId, channelName, text, timestamp);
  console.log('Message saved to', documentId, ':', channelName, text);
}

// --- Slack APIからチャンネル名を取得する関数 ---
function getChannelName(channelId) {
  const url = `https://slack.com/api/conversations.info?channel=${channelId}`;
  const options = {
    method: 'get',
    headers: {
      'Authorization': `Bearer ${CONFIG.SLACK_BOT_TOKEN}`
    }
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const resJson = JSON.parse(response.getContentText());
    if (resJson.ok) {
      return resJson.channel.name;
    } else {
      console.error('conversations.info API error:', resJson.error);
      return 'unknown-channel';
    }
  } catch (e) {
    console.error('getChannelName error:', e);
    return 'error-fetching-name';
  }
}

// --- Googleドキュメントに追記する関数 ---
function appendToDocument(documentId, channelName, text, timestamp) {
  const doc = DocumentApp.openById(documentId);
  const body = doc.getBody();

  // 区切り線(グレー)
  body.appendParagraph('----------------------------------').setForegroundColor('#cccccc');
  // ヘッダー(黒・太字)
  body.appendParagraph(`[#${channelName}] - ${timestamp}`).setBold(true).setForegroundColor('#000000');
  // 本文(黒・通常)
  body.appendParagraph(text).setBold(false).setForegroundColor('#000000');
}

// --- 権限承認用テスト関数 ---
function testRun() {
  console.log("認証のためのテスト実行");
  // 全ドキュメントへのアクセス権限を確認
  const documentIds = [...new Set(Object.values(CONFIG.CHANNEL_DOCUMENT_MAP))];
  documentIds.forEach(docId => {
    if (docId) {
      const doc = DocumentApp.openById(docId);
      console.log('Document:', doc.getName());
    }
  });
}

// --- ブラウザからのアクセス確認用 ---
function doGet(e) {
  return ContentService.createTextOutput("OK - GASは正常に動作しています。このURLをSlackに設定してください。")
    .setMimeType(ContentService.MimeType.TEXT);
}
設定項目(パターンB)
項目説明
SLACK_BOT_TOKENBot Token(xoxb-で始まる)
VERIFICATION_TOKENVerification Token
CHANNEL_DOCUMENT_MAPチャンネルIDとドキュメントIDの対応
DEFAULT_DOCUMENT_IDマッピングにないチャンネルの保存先(空文字で無視)
チャンネルIDの確認方法
  1. Slackでチャンネルを開く
  2. チャンネル名をクリック
  3. 一番下に「Channel ID」が表示される(例:C01ABC2DEF)

動作確認

  1. Slackの対象チャンネルでメッセージを投稿
  2. Googleドキュメントを開いて確認
  3. 投稿が追記されていれば成功

トラブルシューティング

URL Verificationが通らない

  • GASを新しいバージョンで再デプロイしたか確認
  • アクセスできるユーザーが「全員」になっているか確認
  • デプロイ後の新しいURLをSlackに設定しているか確認

メッセージが反映されない

GASの実行ログを確認:

  1. GASエディタで左メニューの「実行数」(時計アイコン)をクリック
  2. エラーメッセージを確認

よくある原因:

症状原因対処
ログに何も出ないBotがチャンネルに参加していない/invite @Bot名で招待
Token verification failedVerification Tokenが間違っているBasic Informationから再確認
Channel not in target listチャンネルIDが設定と異なるチャンネルIDを確認して設定
No document configuredCHANNEL_DOCUMENT_MAPに未登録マッピングを追加

コード修正後の再デプロイ

コードを修正した場合は必ず再デプロイが必要です:

  1. 「デプロイ」→「デプロイを管理」
  2. 右上の鉛筆アイコン(編集)
  3. バージョン:「新バージョン」を選択
  4. 「デプロイ」

URLは変わりませんが、新バージョンでデプロイしないと変更が反映されません。


セキュリティに関する注意

本マニュアルではSlackのVerification Tokenを使用していますが、Slackは現在Signing Secretによる署名検証を推奨しています。Verification Tokenは非推奨(deprecated)です。

GASの制約により署名検証の実装が困難なため、本マニュアルではVerification Tokenを使用しています。

より高いセキュリティが必要な場合は、以下を検討してください:

  • AWS LambdaやCloud Functionsなど、署名検証が確実に動作する環境への移行
  • GASのURLを推測困難なものとして扱い、公開しない

応用:Geminiとの連携

Googleドキュメントに蓄積した投稿は、GeminiなどのAIによる分析と相性が良いです。

活用例

振り返り・自己分析

このドキュメントは私のSlack投稿の記録です。
直近1週間の投稿から、以下を分析してください:
1. 主なトピック(3つ程度)
2. 未完了と思われるタスク
3. 繰り返し言及しているキーワード

週次レポートの下書き生成

以下の投稿履歴から、週次レポートのドラフトを作成してください。
- 完了した作業
- 進行中の課題
- 来週の予定

アイデアの発掘

思考ログの中から、発展させられそうなアイデアを3つ挙げてください。
それぞれについて、次のアクションを提案してください。

注意事項と免責事項

本記事で紹介しているコードを使用する際は、以下の点にご注意ください。

1. セキュリティ情報の取り扱い

コード内の SLACK_BOT_TOKENVERIFICATION_TOKEN、および発行された GASのウェブアプリURL は、パスワードと同様の機密情報です。
SNSやGitHubなどで公開したり、第三者に教えたりしないよう厳重に管理してください。

2. Slackの仕様について

本スクリプトでは実装の簡易さを優先し、Slackの Verification Token を使用しています。現在は Signing Secret による検証が推奨されていますが、GASでの実装難易度が高いため、個人利用の範囲として本手法を採用しています。将来的にSlackの仕様変更により使用できなくなる可能性がある点をご了承ください。

3. GASの制限(Quota)について

Google Apps Scriptには1日あたりの実行回数や通信回数の制限(Quota)があります。
極端に投稿数が多いチャンネル(1日数千件など)で稼働させると、制限に達して動作が停止する場合があります。

4. 免責事項

本記事の情報を利用して発生したいかなる損害やトラブル(データの消失、情報の漏洩、アカウントの制限など)についても、筆者は一切の責任を負いません。必ずご自身の責任において、テスト環境等で動作確認を行った上でご利用ください。

さいごに。

上までは、Gemini・Claudeの共作みたいなものです。
ですが、新年早々、おもしろいことを始められたかな、
という感じもします。
AIの力は借りましたが、
あたらしいもの、ことを自分に取りこめるなら、
それはいいこと、と思いますし。

紙に書いたほうが、というのもわかりますけれど、
とりあえずは自分の生活にフィットするかたちを求めて。
その助けをAIがしてくれた感じですね。

AIって、自分がなにかをしたい、
というのがなければ何もできないので。
自分をうつす鏡なのかもな、なんておもいます。

よし、2026年も楽しむぞい!!

コメント

タイトルとURLをコピーしました