人生初めてChrome拡張機能を書いたので、開発フローをメモする。
TL;DR
ぴあアプリに独占される内容をブラウザに表示する Chrome 拡張機能。Web Store にて公開中なんで、ぴあのコンテンツを楽しんている方々はぜひお試しください。コードも GitHub で公開中。
経緯
ぴあにて世界の押井守監督が雑談コラムを連載している。僕は長年の大ファンで、フォローしたいと思うが、PCのブラウザーで文章内容を一部しか見えない。完全版はなんとアプリ独占。アプリをプロモートすることは理解できるが、さすがに押井監督のコンテンツだけをフォローしたい僕にはアプリを使わせることが無理。
どうしようと思ったら、とりあえず Whistle で、アプリのトラフィックを分析した。まあまあアプリのコンテンツを簡単に cURL できるんで、自分のためにChrome拡張を書こうと決めた。
デモ
閲覧しているページにはぴあアプリ独占のコンテンツがあるかを検出し、完全版の内容を相応のHTMLに入れ替わる。ユーザー側のアクションは不要。
API
コンテンツに関わるリクエストには全て x-dpia-accesstoken
という HTTP ヘッダーがついている。で、問題はどうやってそのトークンをゲットすること。
1. トークン取得
Whistleで探知されたリクエストから見ると、下記のように DeviceID を/authentication/user
というエンドポイントへポストするとaccessToken
を取得できる。ぴあのアプリが使った DeviceID は iOS 内蔵の API による生成したもの模様。Chromeでそんな API がないというか、そもそも Device ではない。幸い、ID のフォーマットとかをサーバー側で検証するシステムがなさそうで、文字列であればなんでも受け入れる(下記のように「ぴあ助です」でもOK)。Web API の Crypto.randomUUID
を使うことにした。
そのIDをサーバーエンドへ送信し、認証トークンをゲット。
curl -X POST "https://api.p.pia.jp/v1/authentication/user" \
-H "host: api.p.pia.jp" -H "accept: */*" \
-H "content-type: application/json" \
-H "x-dpia-accesstoken: " \
-H "user-agent: Dpia/4.5.0 (jp.co.pia.DigitalPia; build:125; iOS 15.6.1) Alamofire/5.4.0" -H "accept-language: zh-Hans-CN;q=1.0, ja-CN;q=0.9, en-CN;q=0.8" \
-d "{\"deviceType\":1,\"deviceId\":\"ぴあ助です\"}"
{"accessToken":{"token":"demo-f8cb-demo-9c6f-demo","expiresIn":604800,"expiresAt":1664970045},"refreshToken":{"token":"demo-cdd2-4ae4-80f1-demo"}}%
2. コンテンツ取得
トークンがあれば、いろいろなAPIを利用できるようになる。
curl -L -X GET \
"https://api.p.pia.jp/v1/webview/locked/cnt/cnt-11-02_2_f6ea2534-4d00-4598-90d4-a67cd1c394a2.html?td_user_id=f8e80ff5-0e4b-42cd-864f-e7c3d7da3086&contentId=f6ea2534-4d00-4598-90d4-a67cd1c394a2&contentTypeId=2&commentViolation" \
-H "x-dpia-accesstoken: ${token}" \
-H "Origin: https://www.p.pia.jp"
Chrome拡張
1、バックエンドとフロントエンド
フロントエンドは通常のウェブ開発と同じで、画面に表示していることで、JSスクリプト注入で変更することができる。
バックエンドは、Chrome自身に関連するセービスの登録とか、フロントでは処理出来ない作業ができる。
2、CORS
問題
前述のように、最後の Content API をコールし App 版の HTML を取得できるのだが、Domain Name は www.lp.pia.jp
になったので、フロントエンドでそれを使うと、CORS のエラーが出る。なんと、api.lp.pia.jp
だと問題ないんだげど、www
だと無理そう。
バンクエンドではたぶん問題ないと思って、Message Passingという機能を試してみた。文字通り、フロントのメッセージをバックに送信する機能。コンテントIDとコンテントタイプをバックエンドへ送信し、そこで HTML をゲットし、またフロントへ返信する。これで CORS エラーがなくて済む。
3、action
アイコンに関する処理は全てAction
です。最初にはアイコンをクリックして初めて処理開始ということになっているんだが、そのクリックも不要じゃないかなと思って、content_script
という機能を見つけて、自動的にぴあのブロックを検知し、自動的にアプリにあるコンテンツを表示するようにした。アクションのコードを全て削除。
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: replaceWithAppContent,
});
});
4、content_script
ある URL パターンがマッチすれば、指定のスクリプトを実行するという機能です。ここでのスクリプトは目標ウェブパージあるいはタブに注入する Javascript コード。だから、フロントエンドの方。基本的に標準 Web API は全部使う可能。それに Chrome 自身の API を使っても問題なし。
僕はこの前、アクションのために書いたコードを全てここへ移行。
5、Storeへ公開
他の人に使わせてもらうなら、やっぱり Google のウェブストアへ公開するんだ。
新規開発者になるために、5ドルを払わなきゃならないことが要注意。いろいろな制限があり、審査にもかなり時間がかかりそうなので、覚悟がないと。