Google Drive Host PageとGoogle Apps Scriptで簡単な掲示板作った話

Google Driveで静的なWebサイトが作れるようになりましたね。
http://www.itmedia.co.jp/news/articles/1302/06/news139.html

今回はせっかく出来たGoogle Driveの静的なページ(以下Drive Host Pageと書きます。正式名称じゃないっす。)とGoogle Apps ScriptのContentServiceを使って簡単な掲示板を作ってみたよって話を書きます。

# ゴール

以下が作ったサイトです。
https://googledrive.com/host/0BwzWIlBMCiR9SlZkbkFqZ21YeFk/

# Drive Host Pageの作り方
まずDrive Host Pageの作り方

1. Google Drive内にどこでもいいのでフォルダを作ります。
今回は`home/掲示板/webapp`というフォルダを作りました。

2. 1.で作った公開したいファイルを置くフォルダの共有設定を`Public on the Web`にします。
誰でも閲覧可の状態 今回はwebappをこの設定にしています。

3. 1.のフォルダにサイトとして公開したいhtmlファイルを置きます。
あまりいないかもですがもしこの時、GASなどのツールを使ってアップロードした場合、
Content-Typeをtext/htmlで指定してないとうまくいかない場合があります。

4. 置いたhtmlファイルをdrive上で開きます。そうすると`Preview`というボタンが表示されます。それを押下します。

5. 多分`https://googledrive.com/host/{公開フォルダのID}/{ファイル名}`のURLに遷移して、webページが表示されます。

こんな感じで、公開したフォルダの配下にwebサイトとして公開したいファイルを置いていきます。  
なお、js用のフォルダや、img用のフォルダを切った場合は`https://googledrive.com/host/{公開フォルダのID}/{作ったフォルダ名}/{ファイル名}`の形でアクセスできます。

# GASのContentServiceを使ってwebサービス(Web API)を作る

Google Apps ScriptのContentServiceはtext、json、xml(atomやrssを含む)等のhtml以外のコンテンツをwebサービスとして返却するためのサービスです。

例えば単純なJSONを返したい場合は以下のように書きます。

```javascript
function doGet(e) {
  var jsonObject = {hoge : 'fuga'};
  return ContentService.createTextOutput(JSON.stringify(jsonObject)).setMimeType(ContentService.MimeType.JSON);
}
```

ただしこのサービスが公開されるURLは`https://script.google.com/macros/s/{id}/exec`となるため、
上ほど作成したDrive Host Pageとは異なるドメインになってしまいます。
なのでJavascriptからは直接アクセスができません。
※GASのContentServiceで返却されるコンテンツには`Access-Control-Allow-Origin`ヘッダもついてないのでCross Domain XHRも無理っす

そこでGAS側でJSONP形式でも返却するようにしておきます。

```javascript
function doGet(e) {
  var jsonObject = {hoge : 'fuga'};
  if(e.parameter.callback) {//callbackはクライアントアプリケーションから指定されるcallback関数名
    return ContentService.createTextOutput(e.parameter.callback + '(' + JSON.stringify(jsonObject) + ')').setMimeType(ContentService.MimeType.JSON);
  } else {
    return ContentService.createTextOutput(JSON.stringify(jsonObject)).setMimeType(ContentService.MimeType.JSON);
  }
}
```

またGASではGETとPOSTのみサポートされており、かつURIに寄るRESTっぽいAPIも作れません。
なので操作(`データを取得する`とか`データを追加する`)は全てパラメータとして渡します。

```javascript
function doGet(e) {

  if(!e.parameter.action) { //actionはapiの操作
    return createContent(e.parameter.callback , {error :'action is required '});
  }

  switch(e.parameter.action) { //switchで書いてますが、規模によってはstrategyパターンとかにしたほうがいいかもですね
    case 'get':
      return createContent(e.parameter.callback , doSomething1(e));
    case 'put':
      return createContent(e.parameter.callback , doSomething2(e));
    default : 
      return createContent(e.parameter.callback , {error : "unsupported operation"});
   } 
}

function createContent(callback , returnObject) {
  if(callback ) {//callbackはクライアントアプリケーションから指定されるcallback関数名
    return ContentService.createTextOutput(callback + '(' + JSON.stringify(returnObject) + ')').setMimeType(ContentService.MimeType.JSON);
  } else {
    return ContentService.createTextOutput(JSON.stringify(returnObject)).setMimeType(ContentService.MimeType.JSON);
  }


}
```

後はクライアント(Drive Host Pageのjsとか)からJSONPでアクセスすればデータを取得出来ます。

# 認証とか

GASはGoogle Appsドメインを除き、認証(ユーザを判別する方法)は1種類しか無いです。    
GASをWeb Appとして公開するときに指定する「Execute the app as:」を「User accessing the web」にし、
ユーザがAPIに初回アクセスするときに表示されるダイアログを利用してインストール(インストールというよりOAuth認可に近いと思いますが)して貰う方法です。

もし「Execute the app as:」を「me」にしている場合、GASの`Session.getActiveUser()`というサービスでユーザは返却されますが、その後取得した`User`オブジェクトの`getEmail`メソッドなどは空を返却します。

※あとでキャプチャ貼る

残念ながらこの方法はContentServiceを利用した場合も同じで、Web APIとして、jsonが還ってくることを期待してjavascriptからアクセスしても、
還ってくるのは上のダイアログ(つまりhtml)になります。

そこで、今回作ったアプリではDrive Host Pageを開いた際にjavascriptで一度jsonpとしてアクセスし、
返却されたテキストをjsonと解釈させ、エラーが発生した場合は認証(GASのインストール)が済んでないと考え、
認証してもらうためのリンクを表示しています。

```javascript
//クライアントサイドのJS angularjsを利用しています。
$http({
  method: 'JSONP',
  url: 'https://script.google.com/macros/s/AKfycbylYV5MrVg3d-Yi3IssTzo4_xIGsfGs0fgLY1fMcF5WUc02UKM/exec?callback=JSON_CALLBACK&action=messages'
}).success(function(data, status) {

  //successの場合はjson文字列の評価が成功 == 認証済み
  $scope.messages = $scope.messages.concat(data);
}).error(function(data,status,headers,config){

  //errorの場合はjson文字列の評価が失敗 == 多分認証できてない => 認証してもらうためにリンクを表示
  $scope.showAuthentification = true;
});
```

また認証は 元のアクセスしたGASのURL → Googleの認証URL → 元のアクセスしたGASのURL と遷移するので、
Web APIを表示しているGAS側で元のDrive Host Pageに戻るためのリンクを表示させます。

```javascript
function doGet(e) {
  if(e.parameter.auth) {  //認証時はauthというパラメータを付けておく
    return HtmlService.createHtmlOutputFromFile("index.html"); //認証後に元のDrive Host Pageに戻ってもらうためのリンクを表示する画面を返却する。
  }
  
  //web apiの処理が続く
  ...
}
```

なお、Google AppsのGASの場合は「Execute the app as:」を「me」にしていても、GASの`Session.getActiveUser().getEmail()`はアクセスした
ユーザのアドレスが返却されるため、上記のような操作は不要になります。

# データの保存

ここまで来ればデータの保存はどこにやっても良いです。
今回のアプリでは返却するJSONをラクに作るために`ScriptDb`を利用していますが、
サービスの種類によってSpreadsheetにしたり、ScriptPropertiesにしたり、Fusion Tablesにしたりと適切なものにして下さい。
ただし、「Execute the app as:」を「User accessing the web」にしているため、
SpreadsheetやFusion Tablesの場合はユーザ自身が、そのファイルへのアクセス権限を保つ必要があります。

DBを公開したくない場合はScriptDbや、ScriptPropertiesなどScript自体にデータが紐づくサービスを利用したほうが良いです。

P.S.
Google Drive Host Pageが2/6に追加されたって書いてあるんですが仕組み的には昨年ぐらいに追加されているんですよね
なんでGoogleも改めて発表したのか若干気になったりします。

http://googleappsdeveloper.blogspot.jp/2012/11/announcing-google-drive-site-publishing.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed:+GoogleAppsDeveloperBlog+(Google+Apps+Developer+Blog)