Yammer に投稿したLGTMな画像を、GitHub に POST する Chrome 拡張を作ってみた
皆さん、こんにちは。MUGENUP の osada です。
今回のテーマは、
の3点です。
読者ターゲットは、
画像は S3 に置きたいけど、管理はしたい
人や、
<input type="file"> を使わずに画像をPOSTしたい
人です。
注: 一部画像にモザイクを掛けてあります。copyrightを守ります
Yammer から GitHub に
開発部では、社内コミニケーションツールとして、Yammer
を使っています。
Yammer は社内専用の Facebook のようなSNSです。
そこに LGTM な画像置き場を作っているのですが、Yammer の URL をそのまま GitHub に投稿しても表示できません。
Yammer は会員制でクローズドなので GitHub はアクセスすることができないからです。
そこで今までは、
- yammer から ローカルPC にダウンロード
- ローカルPC から GitHub にアップロード
という2手順を踏んで、LGTM に使っていました。
しかしこの手順は意外と面倒くさい!
結局、LGTM.in
と拡張機能 LGTMでめでたさを伝えるChrome拡張をつくった - Thinking-megane を使ってしまい、
このままでは Yammer は、ただの画像置き場になってしまう!
ということで、今回、Chrome 拡張を作ることにしました。
Chrome拡張の動作
動作は大きく3つに分かれます
Yammer API
で、投稿を取得(JSON
)し、サムネイル画像をレンダリングする- 選択したサムネイル画像を、
BLOB形式
でダウンロードする - 画像を
GitHub
を経由してS3にアップロードする
鍵となるのは、下記の2点です。
- 画像を BLOB として扱うこと
- GitHub は画像を3段階でアップロードすること
では、1つずつ解説していきます。
Chrome 拡張の構造
と、その前に、Chrome 拡張の構造を解説します。
この Chrome 拡張は、下記の3つから成り立っています。
- chrome拡張を定義する、
manifest.json
- chrome拡張側の、アイコンをクリックしてポップアップする、
popup.html
とyammer.js
。 - 表示中のページで読み込まれる
content.js
chrome 拡張と、表示中のタブ間は、完全に独立しているため、 yammer.js と content.js は、メッセージで通信することになります。
Yammer API
で、投稿を取得(JSON
)し、サムネイル画像をレンダリングする
chrome 拡張側、アイコンをクリックすると、popup.html を開き、yammer.js を読み込んで投稿を取得します。
function loadAndAppendGroupImages($body, groupID){ var url = "https://www.yammer.com/api/v1/messages/in_group/"+ groupID + ".json"; $.getJSON(url) .done(function(data, status, xhr){ var images = []; data.messages.forEach(function(message){ if(message.attachments){ message.attachments.forEach(function(attachment){ if(image = createImage(attachment)){ images.push(image); } }); } }); var image_tags = []; images.forEach(function(image){ image_tags.push(toHTML(image)); }); $body.replaceWith(image_tags); }) .fail(function(xhr, status){ $body.replaceWith("<div style='width: 200px'>読み取れません。ログインしていないか、ネットワークが違います</div>"); }); }
この json は、messages の array が返ってきます。
この中で画像の情報は attachments
に含まれています。
ここから、サムネイルURLとダウンロードURLを取り出して、popup.html にレンダリングします。
なお、createImage
と toHTML
は、取り出しと、HTML整形です。
function createImage(attachment){ return { thumbnail_url: attachment.thumbnail_url, download_url: attachment.download_url }; } function toHTML(image){ return "<a href='" + image.download_url + "'>" + "<img src='" + image.thumbnail_url + "'>" + "</a>" + "<br>"; }
{ "threaded_extended": {}, "messages": [ { "id": , ...... "attachments": [ { "id": xxxxx, "url": "https://www.yammer.com/api/v1/uploaded_files/xxxxx", "web_url": "https://www.yammer.com/mugenup.com/uploaded_files/xxxxx", "type": "image", "name": "xxxx.gif", "original_name": "xxxx.gif", "full_name": "xxxx", "description": "", "content_type": "image/gif", "small_icon_url": "https://c64.assets-yammer.com/images/file_icons/types/picture_orange_39x50_icon.png", "large_icon_url": "https://c64.assets-yammer.com/images/file_icons/types/picture_orange_79x102_icon.png", "download_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/download", "thumbnail_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/thumbnail", "preview_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/preview/xxxx.gif", "large_preview_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/large_preview/xxxx.gif", "size": 1014474, ...... "height": 278, "width": 500, "scaled_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/scaled/{{width}}x{{height}}", "image": { "url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/preview/xxxx.gif", "size": 1014474, "thumbnail_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/thumbnail" }, } ], },......
選択したサムネイル画像を、BLOB形式
でダウンロードする
popup.html
に表示された画像一覧から、画像をクリックしたとき、
download_url
を使って、画像をダウンロードします。
このダウンロード動作は拡張側ではなく、ページ側で行うため、download_url
を tab
側に メッセージとして送信します。
// 画像がクリックされたとき、ダウンロードURLを、コンテンツスクリプトに委譲 $(document).on("click", "a", function(e){ $this = $(this); $this.replaceWith("<img src='./imgs/loading.gif'>"); var download_url = $this.attr("href"); chrome.tabs.getSelected(null, function(tab) { chrome.tabs.sendRequest( tab.id, { download_url: download_url }, function(response){ window.close(); }); }); });
コンテンツスクリプト側では、addListener
を使い、
メッセージをキャッチします。
chrome.extension.onRequest.addListener( function(request, sender, sendResponse) { var download_url = request.download_url; xhr = new XMLHttpRequest(); xhr.open('get', download_url, true); xhr.responseType = 'blob'; xhr.onload = function(){ if(this.status == 200){ var blob = this.response; var ContentDisposition = xhr.getResponseHeader("Content-Disposition"); blob.filename = ContentDisposition.match(/filename="([^"]+)"/)[1]; postGithub(blob); } }; xhr.send(); } );
ここで大事なのは、画像としてダウンロードすることです。
よって、xhr.responseType = 'blob';
を指定しています。
blob
というのは、バイナリ・ラージ・オブジェクトのことであり、コレを使うことで、response
をバイナリとして扱うことができます。
(ここだけ、jQuery
ではなく、XMLHttpRequest
を使っているのは、
jQuery で responseType
を指定する方法が不明だったためです)
画像をGitHub
を経由してS3にアップロードする
GitHub の画像のアップロードは、大変優れた方法です。 GitHub側では、レコードを管理し、大きなバイナリデータはS3に直接アップロードさせます。
(ご存じの方は3ウェイ・ハンドシェイク を 思い浮かべていただければ、わかりやすいと思います。)
GitHub へのアップロードは下記の動作で実行されます。
1つずつ解説します
1. GitHub に post する
name
, size
, content_type
を GitHub に post します。
function postGithub(blob){ var formData = new FormData(); formData.append("name", blob.filename); formData.append("size", blob.size); formData.append("content_type", blob.type); var url = "https://github.com/upload/policies/assets"; $.ajax({ url: url, type: "post", headers: {"X-CSRF-Token": csrf}, data: formData, processData: false, contentType: false }).done(function(data){ postS3(data, blob); }); }
ここでは、ファイル自体は post していないことに注目してください
2. GitHub は、S3 へのアップロードに必要な情報を返す
すると、GitHub は、S3へのアップロードに必要な情報を返してきます。
(一部の箇所は、xxxxx
で隠しています)
{ "upload_url": "https://s3.amazonaws.com/github-cloud", "header": {}, "asset": { "id": xxxxx, "name": "xxxxx.jpg", "size": 59192, "content_type": "image/jpeg", "href": "https://cloud.githubusercontent.com/assets/xxxxx/3711249/fd7cc29e-14ca-11e4-9914-0ca5b063b0c5.jpg", "original_name": "xxxxx.jpg" }, "form": { "key": "assets/xxxxx/3711249/fd7cc29e-14ca-11e4-9914-0ca5b063b0c5.jpg", "AWSAccessKeyId": "xxxxx", "acl": "public-read", "policy": "xxxxx", "signature": "xxxxx", "Content-Type": "image/jpeg", "Cache-Control": "max-age=31557600", "x-amz-meta-Surrogate-Control": "max-age=31557600", "x-amz-meta-Surrogate-Key": "user-xxxxx" }, "asset_upload_url": "/upload/assets/xxxxx" }
3. S3 にアップロードする
返された json の、form
に含まれるデータと、
実際の画像データのBLOB
データを formData
化して、
S3にアップロードします。
function postS3(data, blob){ var formData = new FormData(); for ( var key in data.form ) { formData.append(key, data.form[key]); } formData.append("file", blob); var asset_upload_url = data.asset_upload_url; $.ajax({ url: data.upload_url, type: "post", headers: {"X-CSRF-Token": csrf}, data: formData, processData: false, contentType: false }).done(function(data){ putGitHub(data, asset_upload_url); }); }
4. GitHub に put して、データを更新する(?)
実は、この put がなくても、url にアクセスすれば、画像は見られます。 よって、この put が何のデータを更新しているのか不明なのですが、 何かのデータを更新しているに違いありません(!?)。
(個人的には、S3へのアップロードが完了したフラグを更新しているのではないかと思っています)
function putGithub(data, asset_upload_url){ $.ajax({ url: asset_upload_url, type: "put", headers: {"X-CSRF-Token": csrf}, processData: false, contentType: false }).done(function(data, status, xhr){ writeLGTM(data); }); }
そして、最後に、アップロードされた画像データのURLを textarea に貼り付けて終了です。
function writeLGTM(data){ var lgtm = "![LGTM](" + data.href + ")"; var oldMessage = $("textarea[name='comment[body]']").val(); if(oldMessage != ""){ lgtm = oldMessage + "\n" + lgtm; } $("textarea[name='comment[body]']").val(lgtm); sendResponse({}); }
画像をアップロード、というと、一回のPOSTで全部処理してしまいそうですが、 RESTful を遵守することで、3アクションにはなっていますが、シンプルな構成になっています。 さすが GitHub といったところでしょうか。
まとめ
さて今回はYammer に投稿したLGTMな画像を、GitHub に POST する Chrome 拡張を作ってみた
というテーマでした。
動作は概要図を再掲します。
今回わかったことは、下記の3点です。
- Chrome 拡張 は表示中のページに介入できる
- ajax で画像を扱うときは、
xhr.responseType = 'blob';
を使うと便利 - GitHub の画像アップロードは、3アクションの
RESTful
で美しい
まず、Chrome 拡張 は表示中のページに介入できる
というのは、本当に怖いことです。
yammer
や、github
へのログイン認証についての、説明が無いことに気づかれたかもしれません。
Chrome拡張はブラウザのセッションを使うことができるので、ログインさえしていれば、拡張側では処理が必要ないのです。
自分がログインしているサービスに対して、バックグラウンドで、Chrome拡張が通信することができてしまいます。
例えば、Facebook
にログインしている Chrome に、勝手に投稿させる
という javascript は簡単に書くことができそうです。野良拡張には本当に気をつけましょう。
気をつけ方としては、manifest.json
の "permissions":
を確認することです。
ここに記載があるサイトに関して、Chrome拡張は権限を持つので、オカシイな?と思ったら、使うのを止めましょう(そもそも使わないですよね)。
(一番安心なのは、ソースコードを全部読んでしまうことです。unzip で解凍することができます)
unzip hoge.crx
次に、responseType ですが、普段ajax
はjQuery
を使うことが多いため、今回初めて知りました。xhr.responseType
を使うと、返り値を制御するとができますが、jQuery ではどうやると良いのでしょうか?
最後に、GitHub の画像の扱いはスマートでした。DBとストレージが異なる、というのは、昨今当たり前なので、大変参考になりました。皆さんもこのように実装されてはいかがでしょうか?(もうやってる?)
ということで、Chrome拡張を作ってみた、というお話でした。
拡張は、html+css+js なので、WEBエンジニアならとても簡単に作ることができます。是非、作ってみることをおすすめします。きっと新しい発見があると思います。