Googleハングアウトのメッセージを検索する（Gmailにアクセスできない環境用）

諸事情でGmailにアクセスできない。
ドメイン制御がかかってる。
でもハングアウトは使ってるから検索したい。

もうしょうがないからAPIたたく！

# 流れ
1. GmailAPIで検索 ⇒ ハイライトされた文字列とやりとり（thread）のIDを取得
2. threadIdからその日のやりとりをさらに取得 ⇒ 日付と人の名前をパースして表示

1でテーブルに一覧化、行をクリックしたらそのやりとり詳細をmodalで表示するようにした。

# 参考
* [Google I/O 2014の裏でひっそり公開されたGmail APIを触ってみる](http://qiita.com/soundTricker/items/e09351ab2c82760ac5b1)
    * GmailAPIの基盤はまるっとそのまま頂きました。大変参考になりましたありがとうございます。
* [Gmail API](https://developers.google.com/gmail/api/)

# ソース
もうまるごと貼る。clientIdだけは各自で。

## search_hangout.js
```js
// https://console.developers.google.com/project/search-hangout-sample/apiui/credential/oauthclient/85744380901-k64k1r9hccgrlccmeob0pbqtuugkf6ll.apps.googleusercontent.com?quickstart=1
var clientId = "xxxxx";

var scopes = ["https://www.googleapis.com/auth/gmail.readonly"].join(",");
//https://mail.google.com/スコープはすべての権限を持ったスコープだけど、Threadやメッセージの削除を行いたい場合のみに利用するべき
//それ以外にいかが有る。
//https://www.googleapis.com/auth/gmail.modify
//https://www.googleapis.com/auth/gmail.readonly
//https://www.googleapis.com/auth/gmail.compose

function handleClientLoad() {
  window.setTimeout(checkAuth,1);
}

function checkAuth() {
  gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: true}, handleAuthResult);
}

function handleAuthResult(authResult) {
  var $authorizeArea = $("#authorizeArea"),
  $buttonsArea = $("#buttonsArea");
  if (authResult && !authResult.error) {
    //エラーがなければ認可ボタン消して操作ボタン群オープンする
    $authorizeArea.hide();

    gapi.client.load("gmail","v1", function(){

    $buttonsArea
      .find("#searchButton")
        .on("click", search)
      .end()
      .show();
    });

  } else {
    $buttonsArea.hide();
    $authorizeArea
      .find("#authorize-button")
        .on("click", handleAuthClick)
      .end()
      .show();
  }
}

function handleAuthClick(event) {
  // Step 3: get authorization to use private data
  gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: false}, handleAuthResult);
  return false;
}

function search() {
  var searchText = $("#searchText").val();
  if (!searchText) return;

  // 初期化
  $("#result_table tbody *").remove();

  searchThreads(searchText, function(results) { makeResultTable(results) });
}

var searchThreads = function(searchText, func) {
  gapi.client.gmail.users.threads.list({
    userId:"me",
    q: searchText
  }).execute(function(response, component) {
    func(response.result.threads);
  });
}

var getThread = function(threadId, func) {
  if(threadId == "undefined") return;
  gapi.client.gmail.users.threads.get({
    id: threadId,
    userId:"me"
  }).execute(function(response , component) {
    func(response.result);
  });
}

var makeResultTable = function(results) {
  $.each(results, function(i, result) {
    appendSearchedSnippet(result.id, result.snippet);
  });

  prepareDetailTable();
}

// modalを閉じたときの初期化イベント
$('#detail_modal').on('hidden.bs.modal', function (e) {
  // 初期化
  $("#detail_table tbody *").remove();
})

var prepareDetailTable = function() {
  // 検索結果にmodalイベント
  // クリック時にやりとりを取得するイベント
  jQuery(function($) {
    $('#result_table tr').addClass('clickable')
      .click(function(e) {
        var threadId = $(this).find("td")[0].innerText;

        getThread(threadId, function(thread) { makeDetailTable(thread) });

        $('#detail_modal').modal('show');
    });
  });
}

var makeDetailTable = function(thread) {
  $.each(thread.messages, function(i, message) {
    appendDetail(
      formatDate(new Date(Number(message.internalDate))),
      message.payload.headers[0].value,
      message.snippet
    );
  });
}

var appendSearchedSnippet = function(threadId, snippet) {
  appendRow("#result_table tbody",
    "<td>" + threadId + "</td>" +
    "<td>" + snippet + "</td>"
  );
}

var appendDetail = function(date, from, snippet) {
  appendRow("#detail_table tbody",
    "<td>" + date + "</td>" +
    "<td>" + from + "</td>" +
    "<td>" + snippet + "</td>"
  );
}

var appendRow = function(table_selector, columns) {
  $(table_selector).append(
    "<tr>" +
      columns +
    "</tr>"
  );
}

var formatDate = function (date, format) {
  if (!format) format = 'YYYY-MM-DD hh:mm:ss';
  format = format.replace(/YYYY/g, date.getFullYear());
  format = format.replace(/MM/g, ('0' + (date.getMonth() + 1)).slice(-2));
  format = format.replace(/DD/g, ('0' + date.getDate()).slice(-2));
  format = format.replace(/hh/g, ('0' + date.getHours()).slice(-2));
  format = format.replace(/mm/g, ('0' + date.getMinutes()).slice(-2));
  format = format.replace(/ss/g, ('0' + date.getSeconds()).slice(-2));
  return format;
};

```

## html
```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>はんぐあうとけんさく</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
    <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
    <!-- build:css(.tmp) styles/main.css -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <!-- endbuild -->
  </head>
  <body>
    <!--[if lt IE 10]>
      <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
    <![endif]-->


    <div class="container">
      <div class="row" id="authorizeArea" style="display: none;">
        <div class="col-lg-12">
          <button id="authorize-button" class="btn btn-default">このアプリの許可</button>
        </div>
      </div>
      <div class="row" id="buttonsArea" style="display: none;">
        <div class="col-lg-3">
          <div class="form-inline">
            <div class="form-group">
              <input id="searchText" type="text" class="form-control">
              <button id="searchButton" type="button" class="btn btn-default">検索</button>
            </div>
          </div>
        </div>
      </div>
      <div class="table-responsive">
        <div id="result_area">
          <table id="result_table" class="table table-striped table-hover">
            <thead>
              <tr>
                <th>threadId</th>
                <th>snippet</th>
              </tr>
            </thead>
            <tbody>
            </tbody>
          </table>
        </div>
      </div>
      <div class="footer text-sm text-muted">
        <p><small>検索結果をクリックするとやりとりが取得できます</small></p>
        <p><small>うまくひっかからないと直近の100件くらいを取得するようです。あと何故か名前だと引っかからない場合があります。</small></p>
      </div>
    </div>

<div class="modal fade" id="detail_modal" tabindex="-1">
    <div class="modal-dialog modal-lg">

      <div class="modal-content">
        <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal">
                <span aria-hidden="true">&times;</span>
            </button>
            <h4 class="modal-title" id="modal-label">やりとり（Ctrl+Fで検索してください）</h4>
        </div>
        <div class="modal-body">
          <div class="table-responsive">
            <table id="detail_table" class="table table-striped">
              <thead>
                <tr>
                  <th>date</th><th>from</th><th>snippet</th>
                </tr>
              </thead>
              <tbody>
              </tbody>
            </table>
          </div>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default" data-dismiss="modal">閉じる</button>
        </div>
      </div>
    </div>
</div>

    <!-- build:js scripts/vendor.js -->
    <!-- bower:js -->
    <script src="assets/jquery-2.1.1.min.js"></script>
    <script src="assets/bootstrap.min.js"></script>
    <!-- endbower -->
    <!-- endbuild -->

    <!-- build:js({app,.tmp}) scripts/main.js -->
    <script src="assets/search_hangout.js"></script>
    <!-- endbuild -->
    <script src="https://apis.google.com/js/client.js?onload=handleClientLoad"></script> <!-- ① -->
  </body>
</html>
```