Yammer に投稿したLGTMな画像を、GitHub に POST する Chrome 拡張を作ってみた

皆さん、こんにちは。MUGENUP の osada です。

今回のテーマは、

  1. Chrome 拡張を作ってみた
  2. JS の BLOB として、画像をダウンロードする
  3. GitHub 経由で、S3 に POST する

の3点です。

読者ターゲットは、 画像は S3 に置きたいけど、管理はしたい 人や、 <input type="file"> を使わずに画像をPOSTしたい 人です。

注: 一部画像にモザイクを掛けてあります。copyrightを守ります

Yammer から GitHub

開発部では、社内コミニケーションツールとして、Yammer を使っています。 Yammer は社内専用の Facebook のようなSNSです。 そこに LGTM な画像置き場を作っているのですが、Yammer の URL をそのまま GitHub に投稿しても表示できません。 Yammer は会員制でクローズドなので GitHub はアクセスすることができないからです。

そこで今までは、

  1. yammer から ローカルPC にダウンロード
  2. ローカルPC から GitHub にアップロード

という2手順を踏んで、LGTM に使っていました。

f:id:mgnup:20140727210649p:plain

しかしこの手順は意外と面倒くさい!

結局、LGTM.in拡張機能 LGTMでめでたさを伝えるChrome拡張をつくった - Thinking-megane を使ってしまい、 このままでは Yammer は、ただの画像置き場になってしまう! ということで、今回、Chrome 拡張を作ることにしました。

Chrome拡張の動作

動作は大きく3つに分かれます

  1. Yammer API で、投稿を取得(JSON)し、サムネイル画像をレンダリングする
  2. 選択したサムネイル画像を、BLOB形式でダウンロードする
  3. 画像をGitHubを経由してS3にアップロードする

f:id:mgnup:20140727210702p:plain

鍵となるのは、下記の2点です。

  1. 画像を BLOB として扱うこと
  2. GitHub は画像を3段階でアップロードすること

では、1つずつ解説していきます。

Chrome 拡張の構造

と、その前に、Chrome 拡張の構造を解説します。

この Chrome 拡張は、下記の3つから成り立っています。

  • chrome拡張を定義する、manifest.json
  • chrome拡張側の、アイコンをクリックしてポップアップする、popup.htmlyammer.js
  • 表示中のページで読み込まれる content.js

f:id:mgnup:20140727210908p:plain

chrome 拡張と、表示中のタブ間は、完全に独立しているため、 yammer.js と content.js は、メッセージで通信することになります。

Yammer API で、投稿を取得(JSON)し、サムネイル画像をレンダリングする

chrome 拡張側、アイコンをクリックすると、popup.html を開き、yammer.js を読み込んで投稿を取得します。

f:id:mgnup:20140727212549p:plain

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 にレンダリングします。

なお、createImagetoHTML は、取り出しと、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>";
}
  • yammer api で取得できる json
{
  "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_urltab 側に メッセージとして送信します。

// 画像がクリックされたとき、ダウンロード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 を使っているのは、 jQueryresponseType を指定する方法が不明だったためです)

画像をGitHubを経由してS3にアップロードする

GitHub の画像のアップロードは、大変優れた方法です。 GitHub側では、レコードを管理し、大きなバイナリデータはS3に直接アップロードさせます。

(ご存じの方は3ウェイ・ハンドシェイク を 思い浮かべていただければ、わかりやすいと思います。)

GitHub へのアップロードは下記の動作で実行されます。

  1. GitHub に post する
  2. GitHub は、S3 へのアップロードに必要な情報を返す
  3. その情報を使い、S3 にアップロードする
  4. GitHub に put して、データを更新する(?)

1つずつ解説します

1. GitHub に post する

name, size, content_typeGitHub に 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 拡張を作ってみた というテーマでした。

動作は概要図を再掲します。

f:id:mgnup:20140727210702p:plain

今回わかったことは、下記の3点です。

  1. Chrome 拡張 は表示中のページに介入できる
  2. ajax で画像を扱うときは、xhr.responseType = 'blob'; を使うと便利
  3. GitHub の画像アップロードは、3アクションの RESTful で美しい

まず、Chrome 拡張 は表示中のページに介入できる というのは、本当に怖いことです。 yammerや、github へのログイン認証についての、説明が無いことに気づかれたかもしれません。 Chrome拡張はブラウザのセッションを使うことができるので、ログインさえしていれば、拡張側では処理が必要ないのです。 自分がログインしているサービスに対して、バックグラウンドで、Chrome拡張が通信することができてしまいます。 例えば、Facebookにログインしている Chrome に、勝手に投稿させる という javascript は簡単に書くことができそうです。野良拡張には本当に気をつけましょう

気をつけ方としては、manifest.json"permissions": を確認することです。 ここに記載があるサイトに関して、Chrome拡張は権限を持つので、オカシイな?と思ったら、使うのを止めましょう(そもそも使わないですよね)。 (一番安心なのは、ソースコードを全部読んでしまうことです。unzip で解凍することができます)

unzip hoge.crx

次に、responseType ですが、普段ajaxjQuery を使うことが多いため、今回初めて知りました。xhr.responseType を使うと、返り値を制御するとができますが、jQuery ではどうやると良いのでしょうか?

最後に、GitHub の画像の扱いはスマートでした。DBとストレージが異なる、というのは、昨今当たり前なので、大変参考になりました。皆さんもこのように実装されてはいかがでしょうか?(もうやってる?)

ということで、Chrome拡張を作ってみた、というお話でした。

拡張は、html+css+js なので、WEBエンジニアならとても簡単に作ることができます。是非、作ってみることをおすすめします。きっと新しい発見があると思います。