現在、情報を随時更新中です。

Chatworkのやり取りをGoogleスプレッドシートに自動で残す方法

Chatworkのやり取りを自動保存
to.igarashi@nekonote-design.info

「Chatworkのチャット、あとから見返したいな」
「引き継ぎや監査のために、履歴を残しておきたい」

そんなときに便利なのが、
GoogleスプレッドシートとGoogle Driveを使った自動保存です。

少し難しそうに見えるかもしれませんが、
一度設定してしまえば、あとは自動で動き続けます。

この記事では、
専門知識がなくても迷わないように
順番に、やさしく説明していきます。

IGA
IGA

スクリーンショットを撮って、画像で場所が迷わないようにしてあります。

この設定でできること

設定が完了すると、こんなことが自動でできるようになります。

  • Chatworkの
    • グループチャット
    • 個別チャット(DM)
      をまとめて取得
  • ルームごとに
    • Googleスプレッドシートへ保存
  • 添付ファイルも
    • 必要なルームだけGoogle Driveに保存
  • 毎日0時に自動実行
  • 「今日は更新がなかった」日も履歴として残る

一度設定すれば、基本的に放置でOKです。

IGA
IGA

無料で使っていて、chatworkの履歴が見えなくなる人の一時的な保存方法です。注意として、基本的には他の方とスプレッドシート共有しないこと。すべて出力されるのでセキュリティ的にはあまりよくありません。

決して悪用せず、自己責任でご使用してください。

事前に準備するもの

あらかじめ、次の2つがあれば大丈夫です。

  • Googleアカウント
  • Chatworkのアカウント

ステップ①:保存先のフォルダを作ります

まずは、
添付ファイルを保存するためのフォルダを作ります。

Google Driveを開く

Googleを開いたときに、9つの点をクリックするとGoogledriveがあります。

GoogleDriveの場所
「+新規」→「フォルダ」
フォルダ名を付ける(例:ChatworkExport)
フォルダを開いて、ブラウザのURLを確認
folders/ の後ろの文字列をコピー

ステップ②:管理用のスプレッドシートを作ります

次に、
チャット履歴を管理するスプレッドシートを作ります。

ChatworkExportの中に管理用のスプレッドシートを作るといいかもしれません。(お任せします)
Google Driveで「+新規」→「Googleスプレッドシート」
名前を付ける
(例:Chatwork_バックアップ管理)

このスプレッドシートが、
これからの管理画面になります。

ステップ③:Apps Script を開きます

作ったスプレッドシートを開く

Chatwork_バックアップ管理のスプレッドシート

上のメニューから、拡張機能→Apps Script
新しい画面が開きます

ステップ④:スクリプトを貼り付けます

表示されているコード.gsを開く
中身をすべて消す
用意した スクリプト全文をそのまま貼り付け
下記のスクリプトを貼り付けてください。
/**
 * Chatwork → Google Sheets(グループチャット別シート:DM含む) + Drive(添付ファイル)
 *
 * ✅要件
 * - シート一覧:更新(する/しない)+添付取得(する/しない, デフォルトしない)
 * - 更新しないはチャット/添付ともに完全スキップ(ログは0件で残す)
 * - 添付は file_id マーカー方式で重複保存防止
 * - バッチ実行+タイムアウト前に安全停止
 * - シート並び順固定:
 *    1番左:シート一覧
 *    2番目:更新履歴(シート一覧の右隣)
 *
 * ▼ Script Properties(事前に設定)
 * - CHATWORK_TOKEN
 * - CW_EXPORT_FOLDER_ID
 */

const CW_BASE = "https://api.chatwork.com/v2";
const BATCH_SIZE = 10;
const MAX_RUNTIME_MS = 5 * 60 * 1000;
const FILE_MARKER_PREFIX = ".saved_";

function runDailyChatworkExport() {
  const startMs = Date.now();

  const props = PropertiesService.getScriptProperties();
  const token = props.getProperty("CHATWORK_TOKEN");
  const rootId = props.getProperty("CW_EXPORT_FOLDER_ID");
  if (!token) throw new Error("CHATWORK_TOKEN が未設定です");
  if (!rootId) throw new Error("CW_EXPORT_FOLDER_ID が未設定です");

  const ss = SpreadsheetApp.getActiveSpreadsheet();

  // ★一覧設定読み込み(更新/添付)
  const indexSettings = getIndexSettings_(ss);

  // ★まずシート一覧を更新(この中でシート順も固定)
  updateSheetIndex_(ss, indexSettings);

  // 日付が変わったらバッチ先頭へ
  const today = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy-MM-dd");
  if (props.getProperty("CW_BATCH_DATE") !== today) {
    props.setProperty("CW_BATCH_DATE", today);
    props.setProperty("CW_BATCH_OFFSET", "0");
  }

  // Drive(添付ファイル用)
  const root = DriveApp.getFolderById(rootId);
  const filesRoot = getOrCreateFolder_(root, "Files");

  // 更新履歴
  const logSheet = getOrCreateSheet_(ss, "更新履歴");
  ensureLogHeader_(logSheet);
  const runAt = new Date();

  // グループチャット一覧(DM含む)
  const rooms = cwGet_("/rooms", token) || [];
  const total = rooms.length;

  // バッチ範囲
  const offset = Number(props.getProperty("CW_BATCH_OFFSET") || 0);
  const end = Math.min(offset + BATCH_SIZE, total);

  let nextOffset = offset;

  for (let i = offset; i < end; i++) {
    if (Date.now() - startMs > MAX_RUNTIME_MS) {
      props.setProperty("CW_BATCH_OFFSET", String(i));
      return;
    }

    const r = rooms[i];
    const roomId = r.room_id;
    const roomName = r.name || String(roomId);
    const sheetName = makeSafeSheetName_(roomName, roomId);

    // 設定(デフォルト:更新する/添付しない)
    const setting = indexSettings[sheetName] || { update: "する", attach: "しない" };

    // 更新しない:完全スキップ(ログは0件)
    if (setting.update === "しない") {
      logSheet.appendRow([runAt, roomId, roomName, 0, 0]);
      nextOffset = i + 1;
      continue;
    }

    /* ---------- チャット ---------- */
    const roomSheet = getOrCreateSheet_(ss, sheetName);
    ensureRoomHeader_(roomSheet);

    const initKey = `CW_INIT_DONE_${roomId}`;
    const isInitDone = props.getProperty(initKey) === "1";
    const force = isInitDone ? 0 : 1;

    const msgs = cwGet_(`/rooms/${roomId}/messages?force=${force}`, token) || [];
    if (!isInitDone) props.setProperty(initKey, "1");

    const lastMsgKey = `CW_LAST_MSGID_${roomId}`;
    const lastMsgId = Number(props.getProperty(lastMsgKey) || 0);
    let maxMsgId = lastMsgId;

    const rows = [];
    for (const m of msgs) {
      const mid = Number(m.message_id || 0);
      if (mid <= lastMsgId) continue;
      maxMsgId = Math.max(maxMsgId, mid);
      const acc = m.account || {};
      rows.push([
        m.message_id || "",
        m.send_time ? new Date(m.send_time * 1000) : "",
        m.update_time ? new Date(m.update_time * 1000) : "",
        acc.account_id || "",
        acc.name || "",
        acc.chatwork_id || "",
        m.body || ""
      ]);
    }

    if (rows.length) {
      roomSheet.getRange(roomSheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
    }
    if (maxMsgId > lastMsgId) props.setProperty(lastMsgKey, String(maxMsgId));

    /* ---------- 添付(設定が「する」の場合のみ) ---------- */
    let savedFileCount = 0;

    if (setting.attach === "する") {
      const roomFolder = getOrCreateFolder_(filesRoot, sanitizeDriveName_(roomName) + "_" + roomId);
      const files = cwGet_(`/rooms/${roomId}/files`, token) || [];

      for (const f of files) {
        if (Date.now() - startMs > MAX_RUNTIME_MS) break;

        const marker = FILE_MARKER_PREFIX + String(f.file_id);
        if (roomFolder.getFilesByName(marker).hasNext()) continue;

        const finfo = cwGet_(`/rooms/${roomId}/files/${f.file_id}?create_download_url=1`, token);
        if (!finfo.download_url) continue;

        const res = UrlFetchApp.fetch(finfo.download_url, {
          headers: { "x-chatworktoken": token },
          muteHttpExceptions: true
        });

        const code = res.getResponseCode();
        if (code >= 200 && code < 300) {
          roomFolder.createFile(res.getBlob().setName(finfo.filename || `file_${f.file_id}`));
          roomFolder.createFile(marker, "", MimeType.PLAIN_TEXT);
          savedFileCount++;
        }
      }
    }

    // 更新履歴
    logSheet.appendRow([runAt, roomId, roomName, rows.length, savedFileCount]);
    nextOffset = i + 1;

    if (i % 5 === 4) Utilities.sleep(200);
  }

  props.setProperty("CW_BATCH_OFFSET", String(nextOffset >= total ? 0 : nextOffset));
}

/* ======================= シート一覧(リンク+更新設定+並び順固定) ======================= */

function getIndexSettings_(ss) {
  const sheet = ss.getSheetByName("シート一覧");
  const map = {};
  if (!sheet || sheet.getLastRow() < 2) return map;

  // ヘッダー:リンク / シート名 / 更新 / 添付取得 / 更新日時
  const values = sheet.getRange(2, 1, sheet.getLastRow() - 1, Math.min(5, sheet.getLastColumn())).getValues();
  values.forEach(r => {
    const name = r[1];
    if (!name) return;
    map[String(name)] = {
      update: (String(r[2]) === "しない") ? "しない" : "する",
      attach: (String(r[3]) === "する") ? "する" : "しない"  // デフォルトしない
    };
  });
  return map;
}

function updateSheetIndex_(ss, settings) {
  // --- 必要シートを確保
  let indexSheet = ss.getSheetByName("シート一覧");
  if (!indexSheet) indexSheet = ss.insertSheet("シート一覧");

  let logSheet = ss.getSheetByName("更新履歴");
  if (!logSheet) logSheet = ss.insertSheet("更新履歴");

  // --- ★並び順固定:1番左=シート一覧、2番目=更新履歴
  ss.setActiveSheet(indexSheet);
  ss.moveActiveSheet(1);
  ss.setActiveSheet(logSheet);
  ss.moveActiveSheet(2);

  // --- 一覧生成(シート一覧/更新履歴は除外)
  const exclude = new Set(["シート一覧", "更新履歴"]);

  indexSheet.clear();
  indexSheet.appendRow(["リンク", "シート名", "更新", "添付取得", "更新日時"]);
  indexSheet.setFrozenRows(1);

  const names = ss.getSheets()
    .map(s => s.getName())
    .filter(n => !exclude.has(n))
    .sort((a, b) => a.localeCompare(b, "ja"));

  const now = new Date();
  const rows = names.map(name => {
    const gid = ss.getSheetByName(name).getSheetId();
    const url = ss.getUrl() + "#gid=" + gid;
    const conf = settings[name] || { update: "する", attach: "しない" }; // ★添付はデフォルトしない
    return [`=HYPERLINK("${url}","開く")`, name, conf.update, conf.attach, now];
  });

  if (rows.length) {
    indexSheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);

    const rule = SpreadsheetApp.newDataValidation()
      .requireValueInList(["する", "しない"], true)
      .setAllowInvalid(false)
      .build();

    // 更新(C列)+添付取得(D列) にプルダウン
    indexSheet.getRange(2, 3, rows.length, 2).setDataValidation(rule);
  }

  indexSheet.setColumnWidth(1, 80);
  indexSheet.setColumnWidth(2, 360);
  indexSheet.setColumnWidth(3, 90);
  indexSheet.setColumnWidth(4, 110);
  indexSheet.setColumnWidth(5, 170);
}

/* ======================= 共通ユーティリティ ======================= */

function cwGet_(path, token) {
  const res = UrlFetchApp.fetch(CW_BASE + path, {
    method: "get",
    headers: { "x-chatworktoken": token },
    muteHttpExceptions: true
  });
  const code = res.getResponseCode();
  if (code === 204) return [];
  if (code < 200 || code >= 300) {
    throw new Error(`Chatwork API error ${code}: ${res.getContentText()}`);
  }
  return JSON.parse(res.getContentText());
}

function getOrCreateSheet_(ss, name) {
  return ss.getSheetByName(name) || ss.insertSheet(name);
}

function getOrCreateFolder_(parent, name) {
  const it = parent.getFoldersByName(name);
  return it.hasNext() ? it.next() : parent.createFolder(name);
}

function ensureRoomHeader_(sheet) {
  if (sheet.getLastRow() > 0) return;
  sheet.appendRow(["message_id","send_time","update_time","account_id","account_name","chatwork_id","body"]);
  sheet.setFrozenRows(1);
}

function ensureLogHeader_(sheet) {
  if (sheet.getLastRow() > 0) return;
  sheet.appendRow(["更新日時", "room_id", "ルーム名", "件数", "添付ファイル数"]);
  sheet.setFrozenRows(1);
}

function makeSafeSheetName_(roomName, roomId) {
  const suffix = "_" + String(roomId);
  const base = String(roomName)
    .replace(/[\[\]\*\?\/\\:]/g, "_")
    .replace(/[\u0000-\u001F]/g, "")
    .trim();
  const maxBaseLen = Math.max(1, 31 - suffix.length);
  return (base || "room").slice(0, maxBaseLen) + suffix;
}

function sanitizeDriveName_(s) {
  return String(s).replace(/[\\/:*?"<>|]/g, "_").trim().slice(0, 80);
}
貼り付けたら、このようになります。
保存(Ctrl + S)

「意味が分からない…」と感じても大丈夫です。
触るのはここまでです。

ステップ⑤:ChatworkのAPIトークンを取得します

chatworkにログイン

記録を取得するアカウントでログインをしてください。

右上の自分のアイコンをクリック
「サービス連携」の設定画面を開く
APIトークンをコピー

このトークンは
自分専用の鍵のようなものなので、
外に公開しないようにしてください。

ステップ⑥:必要な設定を登録します(ここが大事)

Apps Scriptの画面で、

左側の歯車アイコン(設定)をクリック
「スクリプト プロパティ」を開く
次の2つを登録します

① Chatworkのトークン

  • キー:CHATWORK_TOKEN
  • 値:コピーしたAPIトークン

② 保存先フォルダID

  • キー:CW_EXPORT_FOLDER_ID
  • 値:ステップ①でコピーした文字列

登録したら保存します。

ステップ⑦:最初のテスト実行

  1. 関数の選択で
    runDailyChatworkExport を選ぶ
  2. 実行ボタンをクリック
  3. 表示される確認画面で
    権限を許可する
GASのコード画面に戻ります

<>のマークを押すと戻れます。

GASEditor
GASのプロジェクト画面です
実行ボタンをクリック
GAS実行
GASを実行するときの実行ボタンです。
【初回のみ】権限の確認をクリック(次回以降出ません)
GAS権限確認、承認が必要
GAS権限確認、承認が必要です。
【初回のみ】Googleで自分のアカウントを選択して下さい。

【初回のみ】詳細をクリック
GAS権限確認の詳細
GAS権限確認の詳細をクリックして進みます。
【初回のみ】安全でないページに移動をクリック
GAS権限確認、安全でないページ
GAS権限確認、安全でないページをクリックして進みます。
【初回のみ】すべて選択にチェック
GAS権限確認、すべて選択
GAS権限確認、すべて選択をして、添付ファイルのダウンロードやチャット履歴をスプレッドシートに転記することを許可します
一番下の続行をクリック
スクリプトが実行されます。
GAS実行中

少し警告っぽい画面が出ることがありますが、
Google Apps Scriptではよくある表示です。
そのまま進んで大丈夫です。

IGA
IGA

ルーム(グループチャット)・DMが多ければ多いほど、出力に時間がかかります。この次に説明するトリガー設定を行なって、放置しておきましょう。
お急ぎに場合は、手動で実行を行い、実行ログで実行完了になったら→また実行を何回も繰り返し行うと、どんどん反映されます。

初回起動する時は、最低限の情報のみを出力しています。

  • 直近の100件のみ、出力します。
    • 次回以降は、その続きから出力されます。
  • 初回実行時、添付ファイルは出力されません。
    • データ量が多くてパンクする可能性があるため、実行を2回目以降に前に実行されたルーム・DMがシート一覧に表示されます。
    • 添付ファイルも出力したい場合は、シート一覧に表示されたルームの添付取得を「する」に変更してから、実行して下さい。
  • クラッシュ予防に、5〜10ルームづつ出力されます。
    • 何回も実行されることで、まだ出力されていないルームも表示されるようになります。

ステップ⑧:スプレッドシートを確認します

スプレッドシートに戻ると、

  • 一番左に シート一覧
  • その右に 更新履歴
  • さらに右に 各ルームのシート

が自動で作られていれば成功です。

ステップ⑨:シート一覧で調整します

「シート一覧」では、
ルームごとに動きを調整できます。

ただし、初回起動のみだとまだ表示がされていません。

  • 更新
    → チャットを取得するかどうか
  • 添付取得
    → 添付ファイルを保存するかどうか
シート一覧_添付取得
シート一覧_添付取得するときは、するに変更して下さい

おすすめは、

  • 普段:
    • 更新:する
    • 添付取得:しない
  • 必要なルームだけ:
    • 添付取得:する

添付は重いので、
必要なときだけONが安心です。

ステップ⑩:毎日0時に自動で動かします

Apps Script画面で
時計アイコン(トリガー)をクリック
GASトリガー
右下の新しいトリガーを追加
GASの右下のトリガー追加
実行関数:runDailyChatworkExport・時間主導型 → 毎日 → 0時
GASタイマー設定
GASタイマー設定
実行関数:runDailyChatworkExport・時間主導型 → 毎日 → 0時
下の保存をクリック

これで、
毎日自動でバックアップされます。

よくある不安

同じ添付ファイルが何度も保存されませんか?

保存されません。
一度保存したファイルには「保存済みの印」が付き、
次回以降は自動でスキップされます。

途中で止まっても大丈夫?

大丈夫です。
次回は続きから再開します。

シート一覧と更新履歴のシートがどっかいきました!

シートの一番左の位置のすべてのシート(3本線)をクリック
Googleスプレッドシートのすべてのシート
一番上にいますので、クリックするとシートに飛べます。
シート一覧を開いたあと
IGA
IGA

少し手順は多いですが、
設定は最初の1回だけです。

  • 日々のチャット管理が楽になる
  • 引き継ぎや監査にも安心
  • 「もしものとき」に履歴が残っている

そんな仕組みを、
無料で作ることができます。

■ 最後に—デザインの相談はお気軽に。

  • LP制作・Meta広告運用代行
  • ホームページ制作
  • バナー制作
  • 採用サイト構築
  • セミナー・スクールサイト構築
  • Elementor / STUDIO 実装
  • LINE公式xエルメ構築

など、あなたの事業に合わせて最適な提案ができます。

「まずは小さく試してみたい」という方も大歓迎です。

見積もりだけでも歓迎します!
ABOUT ME
猫乃手デザイン所|IGA
猫乃手デザイン所|IGA
猫の手も、デザインの手も。 中小企業・個人事業主の 「丸っとデザインします。
猫乃手デザイン所は、 「専門用語はできるだけナシ」「現場感のある提案」を大切に、ホームページ・LP・バナー・資料デザインまで あなたの事業にそっと寄り添うWEBパートナーです。
記事URLをコピーしました