Google Apps Script ハンズオン資料 #GoogleAppsScript

## この資料の説明

2018/10/04 に [グロービス](https://qiita.com/organizations/globis) で行った [第 1 回 Google Apps Script (GAS) ハンズオン @麹町](https://globis.connpass.com/event/100268/) のハンズオン用資料です。以下の流れに沿ってハンズオンを進めていきます。

手順は細かく書いていますので、社内外で是非流用してください！

ハンズオンは以下の記事の感じで進めました！
[チューター少人数でも回せる！Slack を駆使したハンズオン勉強会のやり方](https://qiita.com/tanabee/items/04a648cede43956c0e9d)

## ハンズオンをスムーズに行うために

- Chrome 推奨
- Chrome 拡張の [JSONView](https://chrome.google.com/webstore/detail/jsonview/chklaanhfefbnpoihckbnefhakgolnmc?hl=ja) 入れておくと Good!
- [意図しないエラー対策](#drive-操作時のアカウントと-apps-script-の実行アカウントが異なってエラー)
- [GAS をローカル開発で開発したい方はこちらを参照](https://qiita.com/tanabee/items/2c51681396fe12b6a0e4#1-%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E7%92%B0%E5%A2%83%E3%81%A7%E9%96%8B%E7%99%BA%E3%81%99%E3%82%8B)
- ハンズオン用の Slack に JOIN

## アジェンダ

- [Google Apps Script とは](#google-apps-script-とは)
- [公式チュートリアル Your first script](#公式チュートリアル)
- [Gmail to Spreadsheet](#gmail-to-spreadsheet)
- [Form to Slack](#form-to-slack)
- [Spreadsheet to JSON](#spreadsheet-to-json)
- [Spreadsheet to Calendar](#spreadsheet-to-calendar)

## Google Apps Script とは

以下 [Google Apps Script とは | Google Cloud Next 2018 資料](https://docs.google.com/presentation/d/16Oo0_rVPl-gieZNAj2HNtWqQMJCPhW27CqLk-c5peyY/edit#slide=id.g3dc77a0a06_0_64) より抜粋


- Google Apps ( Spreadsheet, Docs, Gmail, Slide など ) の API を簡単にクラウドで実行できる環境
- G Suite に強い FaaS
- GAS と略される

### Google Apps Script の言語環境

- プログラミング言語は JavaScript ( Version 1.6 がベース )
- Web フロントエンド JavaScript と異なる
  - DOM, Window API はない
- Node.js とも異なる
  - Node.js の Core API が使えない
  - npm モジュールは標準で非対応

## 公式チュートリアル

[Your first script](https://developers.google.com/apps-script/overview#your_first_script)

## Gmail to Spreadsheet

- Gmail からメールを検索して、該当するメッセージ一覧をスプレッドシートに書き込むサンプル
- 解説ページ: [Gmail to SpreadSheet](https://qiita.com/tanabee/items/17eb721801eff8d97db9)

### 作業の流れ

1. スプレッドシートを新規に作成
- https://script.google.com にアクセスして `新規スクリプト` を選択
- プロジェクト名を入力、保存
- 以下のコードを GAS エディタにペーストして保存
- 作成したスプレッドシートの ID を spreadSheetId に代入（ docs.google.com/spreadsheets/d/`ここの文字列`/edit ）
- 同じくタブ名を spreadSheetTabName に代入
- 検索ワードを searchText に代入（参考: [Gmail で使用できる検索演算子](https://support.google.com/mail/answer/7190?hl=ja)）
- 関数一覧から main 関数を選んで実行
- [スクリプトに権限を与える操作を行う](#スクリプトに権限を与える操作を行う流れ)
- スプレッドシートに件名、送信元メアド、送信先メアドが追加されれば OK

### 余裕がある方向け

- [Class GmailMessage](https://developers.google.com/apps-script/reference/gmail/gmail-message) を参考に日付や本文などの他のデータも取れるようにする
- 501 スレッド以上の取得
- 2 回目以降の実行で上書きでなく、重複チェックして行追加されるようにする
- 定期実行してみる（ 5 分おきなど ）
- getConfig 関数でなく、 [Properties Service](https://developers.google.com/apps-script/reference/properties/) を用いて設定値管理する

```JavaScript
// 設定情報
function getConfig() {
  return {
    spreadSheetId: '',// スプレッドシートの ID
    spreadSheetTabName: 'シート1',// スプレッドシートのタブ名
    searchText: ''// Gmail の検索ワード
  };
}

// 実行関数
function main() {
  var config   = getConfig();
  // 書き込み先のスプレッドシートを取得
  var sheet    = SpreadsheetApp.openById(config.spreadSheetId)
                               .getSheetByName(config.spreadSheetTabName);
  // メッセージ一覧の配列を初期化
  // 件名, 送信元, 送信先のヘッダーを 1 行目に入れる
  var messages = [['Subject', 'From', 'To']];

  GmailApp
    // スレッド検索
    .search(config.searchText, 1, 500)
    .forEach(function (thread) {
      // スレッド内のメッセージ一覧を取得
      thread.getMessages().forEach(function (message) {
        // 件名
        var subject = message.getSubject();
        // 送信元
        var to      = message.getTo();
        // 送信先
        var from    = message.getFrom();
        // メッセージ一覧に追加
        messages.push([subject, from, to]);
      });
    });
  // ２次元配列のサイズにあった範囲を指定して、シートに書き込む
  sheet.getRange( 'A1:C' + messages.length ).setValues(messages);
}
```

## Form to Slack

- Google フォームの回答時に、回答内容を Slack に通知するサンプル
- [Form to Slack](https://github.com/tanabee/form-to-slack)

### 作業の流れ

1. Google フォームを作成
- 適当に質問を 2-3 個作る 
- `︙` > `スクリプトエディタ` を選択
- プロジェクト名を入力、保存
- 以下のコードを GAS エディタにペーストして保存
- Webhook URL を slackWebhookUrl に代入（別途 Slack で共有）
- `編集` > `現在のプロジェクトのトリガー` からトリガー追加（以下画像参照）
- [スクリプトに権限を与える操作を行う](#スクリプトに権限を与える操作を行う流れ)
- 右上メニューのプレビューを選択して、 Google フォームに回答してみる
- 回答内容が Slack に通知されれば OK

![スクリーンショット 2018-10-01 18.12.26.png](https://qiita-image-store.s3.amazonaws.com/0/19698/b1b31b5d-ccac-3e22-de5d-682c10dd7e5f.png)
トリガーの設定

### 余裕がある方向け
- メッセージの内容カスタマイズ
- 普段使っている Slack で [incoming webhook を作成](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks) してslackWebhookUrl を更新して投稿してみる

```JavaScript
var slackWebhookUrl = '';// Slack の Webhook URL を入力

function onSubmit(e) {
  // Google フォームの回答状況を文章化する
  // [ 例 ]
  // 社名: グロービス
  // Qiita: tanabee
  var answer = e.response.getItemResponses().map(function (itemResponse) {
    return itemResponse.getItem().getTitle() + ': ' + itemResponse.getResponse();
  }).join('\n');

  // インラインコードの書式を回答情報に設定して、通知文言を適宜追記
  var message = [
    '新しい投稿がありました。',
    '```', answer, '```'
  ].join('\n');

  // Slack 通知
  postSlack(slackWebhookUrl, message);
}

function postSlack(webhookUrl, message) {
  // UrlFetchApp.fetch メソッドで外部リソース（Slack）にアクセス
  // 参考: Use your Incoming Webhook URL to post a message
  // https://api.slack.com/incoming-webhooks#posting_with_webhooks
  UrlFetchApp.fetch(slackWebhookUrl, {
    method: 'POST',
    headers: { "Content-Type": 'application/json' },
    payload: JSON.stringify({ text: message })
  });
}
```

## Spreadsheet to JSON

- スプレッドシートの内容を JSON 変換して API 化するサンプル
- [Spreadsheet to JSON](https://github.com/tanabee/spreadsheet-to-json)

### 作業の流れ

1. API 化したいスプレッドシートを探す。ない場合は、[都道府県別人口シート](https://docs.google.com/spreadsheets/d/1gWI98XMLhtI3BRtgk5sMCAgC-mWLb-KA-degLehgjSw/edit?usp=sharing) を利用（[ソース](https://www.e-stat.go.jp/stat-search/files?page=1&layout=datalist&toukei=00200521&tstat=000001011777&cycle=0&tclass1=000001094741)）。
- https://script.google.com にアクセスして `新規スクリプト` を選択
- プロジェクト名を入力、保存
- 以下のコードを GAS エディタにペーストして保存
- 1 で選んだスプレッドシートの ID とタブ名を 1-2 行目の変数に代入
- `公開` > `ウェブアプリケーションとして導入` を選択（以下画像参照）
- [スクリプトに権限を与える操作を行う](#スクリプトに権限を与える操作を行う流れ)
- 出力された URL をブラウザ等から叩いてみる

![スクリーンショット 2018-10-01 23.03.10.png](https://qiita-image-store.s3.amazonaws.com/0/19698/c55305d2-b504-3aee-943d-022e67f96e39.png)
上記のように設定して導入。

アウトプット： [都道府県別人口 API](https://script.googleusercontent.com/macros/echo?user_content_key=MwXFYRiQaHzmUMKk5z2I0ad2HlVCsSVTpNa5dv5AeDBtplVsOQ4g4P7H89SlChj2W6AVrS4VzAvSi9s3bKcwH_7Uu9Vj340Zm5_BxDlH2jW0nuo2oDemN9CCS2h10ox_1xSncGQajx_ryfhECjZEnGvd-kvXdHpJcBdKkMx-gsBtdhXM1pATo9MJpu6WpEsV8fgq4VGppULHcj3pjRirYdveWPraiRQB&lib=MdBIpkv3p7yJyohgDgQz692bANieabb6R)

### 余裕がある方向け

- JSON の形式を変えてみる（ネストさせる、count を入れるなど）
- query parameter を受け取って、特定の条件に合うものだけを返す
- ページング実装
- [Properties Service](https://developers.google.com/apps-script/reference/properties/) を用いて設定値管理する

```JavaScript
var spreadSheetId = '1gWI98XMLhtI3BRtgk5sMCAgC-mWLb-KA-degLehgjSw';// スプレッドシート ID
var spreadSheetTabName = 'シート1';// スプレッドシートのタブ名

// doGet は GET リクエストに反応するメソッド
// 参考: https://developers.google.com/apps-script/guides/triggers/
function doGet(e) {
  // 該当のシートから全行取得
  var rows = SpreadsheetApp
    .openById(spreadSheetId)
    .getSheetByName(spreadSheetTabName)
    .getDataRange()
    .getValues();
  // 1 行目を key に使う
  var keys = rows.shift();
  // 1 行目以降を Object 形式で配列に格納
  var data = rows.map(function(row) {
    var obj = {};
    row.forEach(function(element, i) {
      obj[keys[i]] = element;
    });
    return obj;
  });
  // JSON 形式で return
  return ContentService.createTextOutput(JSON.stringify(data))
                       .setMimeType(ContentService.MimeType.JSON);
}
```

## Spreadsheet to Calendar

- スプレッドシートに記載されたイベント情報を Google Calendar に一括登録するサンプル
- [Installable Triggers](https://developers.google.com/apps-script/guides/triggers/installable) を利用
- [Spreadsheet to Calendar](https://qiita.com/tanabee/items/7ebcc63e5b561d18a582)

### 作業の流れ

1. スプレッドシートの　[テンプレ](https://docs.google.com/spreadsheets/u/1/d/1yoiwKPBs-FwGYq-zZkQza2B7VYpvvqcM0tCLDSyFiR8/edit#gid=0) を開いて、 `ファイル` > `コピーを作成`
- `ツール` > `スクリプトエディタ` を選択
- プロジェクト名を入力、保存
- 以下のコードを GAS エディタにペーストして保存
- onOpen メソッドを選択して実行
- [スクリプトに権限を与える操作を行う](#スクリプトに権限を与える操作を行う流れ)
- スプレッドシートに戻ると、カレンダー連携メニューが表示される
- `カレンダー連携` > `一括登録` を選択して、Google カレンダーを確認

### 余裕がある方向け

- 終日イベントにも対応する（参考: [createAllDayEvent(title, startDate, endDate)](https://developers.google.com/apps-script/reference/calendar/calendar#createAllDayEvent(String,Date,Date))）
- ゲストが予定を変更できるようにする（参考: [setGuestsCanModify(guestsCanModify)](https://developers.google.com/apps-script/reference/calendar/calendar-event#setGuestsCanModify(Boolean))）
- 新しいメニューを追加する

```JavaScript
// Spreadsheet 起動時に呼び出す関数
function onOpen() {
  SpreadsheetApp
    .getActiveSpreadsheet()
    // カレンダー連携メニューをスプレッドシートに追加
    // 一括登録、キャンセルメニューとメソッドの紐付け
    // https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#addMenu(String,Object)
    .addMenu('カレンダー連携', [
      {name: '一括登録', functionName: 'registerAll'},
      {name: '一括キャンセル', functionName: 'cancelAll'},
    ]);
}

// カレンダーにイベントを登録
function registerAll() {
  // 開いているシートを取得
  var sheet = SpreadsheetApp.getActiveSheet();
  var eventIds = sheet
    // データが入っている範囲を取得
    .getDataRange()
    // ２次元配列形式で取得
    .getValues()
    // 一行目と、イベント ID が登録済みの場合は除去
    .filter(function (e, i) {
      return i !== 0 && e[6] === '';
    // イベントを一括登録して、イベント ID の配列を作成
    }).map(function (e) {
      // Google カレンダーに登録
      var calendarEvent = CalendarApp.getDefaultCalendar().createEvent(
        e[0], e[1], e[2],
        {
          description: e[3],
          location: e[4],
          guests: e[5]
        });
      // 作成済みイベントの ID を返す
      return [calendarEvent.getId()];
    });

  // 該当するイベントがなければスプレッドシートに記載しない
  if (eventIds.length === 0) return;

  // イベント一覧をスプレッドシートに記載
  sheet.getRange('G2:G' + (eventIds.length+1) ).setValues(eventIds);
}

// 登録されたイベントをキャンセルして、シートから削除
function cancelAll() {
  // イベント一覧の G 列データ範囲
  var range = 'G2:G1000';
  // 開いているシートを取得
  var sheet = SpreadsheetApp.getActiveSheet();

  sheet
    // range で指定した範囲を取得
    .getRange(range)
    .getValues()
    // イベント ID 欄が空のものを除く
    .filter(function (eventId) {
      return eventId[0] !== '';
    // イベントの削除
    }).forEach(function (id) {
      CalendarApp.getDefaultCalendar().getEventById(id).deleteEvent();
    });
  // G 列のデータをクリア
  sheet.getRange(range).clear();
}
```

## ハマりどころ

### Drive 操作時のアカウントと Apps Script の実行アカウントが異なってエラー

複数アカウントでログインしている時にたまに起こるので、作業（ハンズオン）前に一度 Google アカウントのログアウト後にログインしてから作業するとズレが起こらない。もしくは Chrome のシークレットブラウザで実行する。

### スクリプトに権限を与える操作を行う流れ

@gmail.com で GAS 実行時にスクリプトに権限を与えようとすると、「このアプリは確認されていません」ワーニングが出ます。以下のフローで権限を与えることができます。

![スクリーンショット 2018-10-03 22.28.58.png](https://qiita-image-store.s3.amazonaws.com/0/19698/a639f64e-152c-5671-eb7e-180d91be198c.png)

![スクリーンショット 2018-10-03 22.33.16.png](https://qiita-image-store.s3.amazonaws.com/0/19698/82d79fc8-278b-eda6-6b47-1f8170a03aeb.png)

![スクリーンショット 2018-10-03 22.29.17.png](https://qiita-image-store.s3.amazonaws.com/0/19698/aeead847-49b9-c012-7720-ceaf7b3b1c65.png)

![スクリーンショット 2018-10-03 22.29.32.png](https://qiita-image-store.s3.amazonaws.com/0/19698/354bc454-5f71-77d5-3ebb-b889cd2b9076.png)

![スクリーンショット 2018-10-03 22.29.45.png](https://qiita-image-store.s3.amazonaws.com/0/19698/2a34a313-c645-17a0-c204-54a101a92e74.png)

## その他の GAS ネタ

- [毎月 500 時間の社内ルーティンワークを GAS を使って自動化した話](https://qiita.com/tanabee/items/ce9c3cf9403c4f3f14a9)
- [GAS ビギナーが GAS を使いこなすために知るべきこと 10 選](https://qiita.com/tanabee/items/2c51681396fe12b6a0e4)
- [3 分で作る無料の翻訳 API with Google Apps Script](https://qiita.com/tanabee/items/c79c5c28ba0537112922)