• #Chrome拡張機能
  • #X
  • #Google Sheets API
  • #OAuth2
  • #Claude Code
  • #Codex
開発chrome-extension-xメモ

Chrome拡張機能でXブックマークをGoogle Sheetsに自動エクスポートする設計と実装

Xのブックマークが300件を超えたあたりで、過去に保存したツイートを探すのに毎回スクロールを繰り返していた。ブラウザのスクロールバーが米粒ほどに縮んでいくのを眺めながら、「これ全部CSVに抜けたら一発で検索できるのに」と手が動いた。会計ソフトA用に作ったChrome拡張のOAuth2認証パターンがそのまま使えそうだったので、流用する前提で設計を始めた。


設計フェーズ: 既存の認証パターンを流用する

会計ソフトA拡張のOAuth2パターン

会計ソフトA用に作ったChrome拡張では、chrome.identity.getAuthToken()でGoogleアカウントのOAuth2トークンを取得している。この仕組みをそのまま横展開する。

// chrome.identity.getAuthToken() で Google OAuth2 トークンを取得
const token = await chrome.identity.getAuthToken({
  interactive: true,
  scopes: ['https://www.googleapis.com/auth/spreadsheets']
});

manifest.jsonのoauth2セクションにスコープを追加するだけで、Sheets APIへのアクセス権が手に入る。Chrome拡張のOAuth2認証は、WebアプリのようにリダイレクトURIを設定する手間がなく、Chromeが認証フローを丸ごと管理してくれる。

全体アーキテクチャ

popup.html(UI)
  ↓ ユーザーが出力先を選択(CSV / Google Sheets)
content-script.js
  ↓ Xのブックマークページをスクレーピング
background.js
  ↓ データ整形 + 差分判定(tweet_idベース)
  ├→ CSV: Blob生成 → chrome.downloads
  └→ Sheets: chrome.identity → Sheets API

Codexレビュー: 致命的指摘2点への対応

Codex CLIが動かない → codex-review-docスキルに切り替え

最初はcodex execコマンドで計画書を直接レビューしようとした。しかしCLIがトークン認証エラーで弾かれ、ターミナルにエラーが返ってくるだけだった。環境変数を確認しても問題なさそうで、ここで時間を溶かすのは避けたかった。codex-review-docスキル経由に切り替えたところ、すんなりレビューが通った。

指摘1: 認証設計の明記不足

Codexが「OAuth2認証フローの詳細が計画書に書かれていない。manifest.jsonのスコープ定義、トークンのリフレッシュ戦略、失効時のエラーハンドリングが抜けている」と指摘してきた。

確かに「chrome.identity使う」とだけ書いて、トークン失効時にどうするかを一行も書いていなかった。以下を計画書に追記した。

  • manifest.jsonのoauth2.scopesにSheets APIスコープを明記
  • トークン失効時はchrome.identity.removeCachedAuthToken()で古いトークンを破棄し、再取得を試みる
  • 再取得にも失敗した場合はポップアップにエラーメッセージを表示し、手動で再認証を促す

指摘2: 差分判定アルゴリズムの曖昧さ

「差分取得の基準がcreated_atなのかtweet_idなのか曖昧。タイムゾーン問題を考えるとcreated_atは罠が多い」という指摘。これは的を射ていた。

created_atはXのAPIがUTCで返すが、ユーザーのローカル時間と混在するとバグの温床になる。tweet_idはSnowflake IDで単調増加が保証されている。比較も数値の大小だけで済む。差分判定をtweet_idベースに統一した。

// 前回エクスポート時の最大tweet_idを保存
const lastExportedId = await chrome.storage.local.get('lastMaxTweetId');

// 新規ブックマークのみ抽出
const newBookmarks = allBookmarks.filter(
  b => BigInt(b.tweet_id) > BigInt(lastExportedId || '0')
);

Phase 2: Google Sheets API連携

スプレッドシートの新規作成と既存シートへの差分追記

出力先として2つのモードを用意した。

  1. 新規作成モード: Sheets APIのspreadsheets.createで新しいスプレッドシートを作り、ヘッダー行とデータを一括で書き込む
  2. 追記モード: 既存のスプレッドシートIDを指定し、spreadsheets.values.appendで末尾にデータを追加する

追記モードでは、前回エクスポート時のlastMaxTweetIdをchromeストレージに保存しておき、それより大きいIDのブックマークだけをappendする。

popup.htmlの出力先選択UI

ポップアップに出力先選択のラジオボタンを追加した。

  • CSV: ローカルにCSVファイルをダウンロード。オフラインでも使える
  • Google Sheets: 認証フローを経てスプレッドシートに出力。新規作成 or 既存シートIDを入力

Google Sheetsを選んだ場合のみ、スプレッドシートIDの入力欄が表示される。空欄なら新規作成、IDを入力すれば追記モードになる。


スクレーピング部分の設計メモ

Xのブックマークページは無限スクロールで読み込まれる。content-scriptでIntersectionObserverを使い、最下部に到達したら自動スクロールを繰り返す。各ツイートのDOM要素から以下を抽出する。

フィールド取得方法
tweet_idツイートリンクのURLパスから抽出
authorユーザー名のテキスト要素
contentツイート本文
created_attime要素のdatetime属性
urlツイートへのパーマリンク

DOMの構造はXの更新で変わる可能性がある。セレクタが壊れたときに検知できるよう、抽出結果が0件のときは「セレクタが古い可能性があります」とポップアップに警告を出す設計にした。


試行錯誤の記録

うまくいったこと

  • 会計ソフトA拡張のOAuth2パターンがほぼコピペで動いた。chrome.identityの設計がChrome拡張同士でポータブルなのを体感した
  • Codexの指摘で差分判定をtweet_idに統一したことで、タイムゾーン周りの複雑さが消えた

手間取ったこと

  • Codex CLIのトークン認証エラーで15分ほど詰まった。結局codex-review-docスキル経由に逃げた形だが、原因は未調査のまま
  • Xのブックマークページのセレクタ調査に時間がかかった。Reactのclass名がハッシュ化されていて、属性セレクタやdata属性を頼りに辿る必要があった

明日への引き継ぎ

Googleタスクに以下を登録した。

  • content-scriptのスクレーピング部分を実装し、実際のXブックマークページで動作確認する
  • Sheets APIへの書き込みをテスト用スプレッドシートで検証する
  • エラーハンドリング(認証失効、スクレーピング失敗、API制限)のUI表示を作り込む

学びメモ

chrome.identity.getAuthToken()を2つ目のプロジェクトで使ったことで、Chrome拡張のOAuth2認証がプロジェクトを跨いで再利用できるパターンとして手元に定着した。manifest.jsonにスコープを足すだけでAPIアクセスが広がるのは、Webアプリのリダイレクト設定と比べると手数が半分以下で済む。

Codexレビューで「tweet_idに統一しろ」と言われたとき、最初は「created_atでも別にいけるだろう」と思った。しかし実際にタイムゾーン変換のコードを頭の中で組み立て始めた瞬間、UTCとJSTの変換ミスで重複取得するシナリオが3つ浮かんで、手が止まった。Snowflake IDの単調増加を信頼する方が、コードもテストもシンプルになる。レビューに救われた場面だった。