Integromatを使ってアクセス制限つきGASを外部サービスのwebhookから実行する

# 概要

何かと便利なGAS(Goole Apps Script)はwebアプリケーションやAPIとして公開することもできます。
しかし、G Suiteを利用していて組織外に公開できない場合などは外部サービスのwebhookなどとの連携にOAuth認証が必要となりやや複雑です。
今回はIntegromatを用い、実行可能なGoogleアカウントが制限された状態でもGASで作成した実行可能APIを外部のwebhookから実行できる構成を試しました。
なお、webhookの例としてChatworkを用います。

※組織的に本当にAPIを外から叩けるようにするのが良いのかは各自のご判断でお願いします

### 今回の目標
* GASで作成したAPIを組織内のみに公開した状態で、ChatworkのwebhookからのデータをGASで受け取れるようにする
* OAuthのアクセストークンのリフレッシュが自動で行われるようにする

### 構成
最終的な構成は以下のようなイメージです。
![スクリーンショット 2021-03-29 0.58.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/d00da292-6353-1b42-2e61-c04851397463.png)

### Integromatについて
IFTTTやZapierのようなノーコードで複数のAPIを簡単に接続することができるサービスです。
1000リクエスト/月までは無料で使うことができます。(21/3/27現在)

https://www.integromat.com/

# 実装
以下のような流れで実装を推めていきます。
1. [スクリプトの用意](スクリプトの用意)
2. [実行可能API形式でGASを公開するためのGCP設定](実行可能API形式でGASを公開するためのGCP設定)
3. [APIの公開](APIの公開)
4. [IntegromatによるOAuth認証つきリクエストの作成](IntegromatによるOAuth認証つきリクエストの作成)
5. [外部サービスのwebhookとの接続](#外部サービスのwebhookとの接続)

### スクリプトの用意
今回は、とりあえず適当なスプレッドシートにバインドした形で以下のようなスクリプトを用意しました。これをwebhookで呼び出せるようにします。

```js
function testFunc(param) {
  return { 
      sheet_name : SpreadsheetApp.getActiveSheet().getName() // "hogehoge"
    , param      : param
  };
}
```

### 実行可能API形式でGASを公開するためのGCP設定
実行可能API形式で使用するにはいくつか下準備が必要になります。
[公式ドキュメント](https://developers.google.com/apps-script/api/how-tos/execute#requirements)をざっくり要約すると、

* GCPのプロジェクトと紐付けて適切なOAuthのスコープをクライアントに与えてね
* GCPプロジェクトでGoogle Apps Script APIを有効化してね
* 実行可能API形式でデプロイしてね

という感じです。まずはGCP周りの設定を行います。

#### プロジェクトの作成
まずは[GCPにのダッシュボード](https://console.cloud.google.com/home/dashboard)にアクセスし、こんな感じでプロジェクトを作成します。
![スクリーンショット 2021-03-27 22.04.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/989558a1-44d6-4700-8a1a-eb4c526fd25d.png)

その後プロジェクト選択してダッシュボードに戻ると、プロジェクトIDが表示されるのでメモしておきます。
![スクリーンショット 2021-03-27 22.05.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/60e758e4-fb8c-8b39-88b4-8f752b5fb605.png)

#### APIの有効化
プロジェクトを作成したら、メニューから「APIとサービス」に遷移し、「APIとサービスの有効化」を選択します。
![スクリーンショット 2021-03-27 22.08.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/b9dec549-d2c5-5876-5c9e-a8e9561050e1.png)

すると各種APIが検索できる画面に切り替わるので、実行に必要なAPIを探して有効化していきます。
今回はApps Script APIと、スプレッドシートを利用するのでSheets APIを検索して有効化しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/a7f873d8-0c8a-9137-8ebd-077dd08e4640.png" width="300"/><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/8da95f3c-597f-3cfb-1f2c-241eb6da70a9.png" width="300"/>

なお、スクリプトが必要とするスコープはGASのエディタの概要から確認できます。
![スクリーンショット 2021-03-27 22.25.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/d459fc71-de91-3097-3bc2-454046dbe57f.png)

#### OAuth関連の設定
次に、メニューから「APIとサービス」>「OAuth同意画面」へ遷移します。
G Suiteユーザでは最初に公開範囲を確認されますが、「内部」を選択します。
![スクリーンショット 2021-03-27 22.11.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/82580848-11e3-7a7c-00ec-10a07b4dda5a.png)

その後の設定画面については、アプリ名とメールアドレスなどの必須項目だけ入力してあとは無視して進めます。

同意画面の設定が終わったら、同じ「APIとサービス」から「認証情報」を開き、OAuthクライアント IDを発行します。
![スクリーンショット 2021-03-27 22.16.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/111bdc79-67c0-6bb5-8b5c-124d65028a88.png)

種類はwebアプリケーションを選び、リダイレクトURIはIntegromatでの認証のため「`https://www.integromat.com/oauth/cb/oauth2`」を入れます。
![スクリーンショット 2021-03-27 22.18.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/8050d48b-52bf-1746-40c9-f01f39db0c48.png)

作成を押下するとクライアントIDとクライアントシークレットが発行されるので、そちらをメモしておきます。

最後にGASのエディタに戻り、設定から「プロジェクトを変更」を押下して先ほどコピーしたプロジェクトIDを入れます。
これでGCP周りの設定は完了です。
![スクリーンショット 2021-03-27 22.00.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/d7136787-af64-9e68-0528-990943628b40.png)

### APIの公開
GCPの設定が終わったらAPIの公開です。
画面右上のデプロイから「新しいデプロイ」に進み、その後出てくるウインドウにて、種類を「実行可能API」と設定します。(GCPの設定を先にしていないとここでエラーが出ます）

![スクリーンショット 2021-03-27 22.36.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/9841a4e9-00bd-521b-d596-f206a247b7c0.png)

この時、G Suiteアカウントであれば「[組織名]内の全員」という選択があるので、それを選びます。
個人利用でアクセス制限をかけたい場合は「自分のみ」にします。

![スクリーンショット 2021-03-27 22.37.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/8ccdc495-5a08-0d6e-93e9-e2adce83807b.png)

デプロイボタンを押すと「実行可能API」のURLが表示されるのでメモしておきます。
なお、このURLはデプロイをするたびに変更になるので注意が必要です。
![スクリーンショット 2021-03-28 2.44.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/14dad206-f012-1e9c-f56f-be810e025e63.png)

これでスクリプトを公開できました。
ただしアクセス制限があるので、試しにPostmanなどを使ってOAuth認証なしのリクエストを投げてみると認証エラーが返ってきます。
webhookで利用するにはアクセストークンを付加した継続的なリクエストが発行できる状態にする必要があります。

```json
{
    "error": {
        "code": 401,
        "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
        "status": "UNAUTHENTICATED"
    }
}
```

### IntegromatによるOAuth認証つきリクエストの作成
継続的にOAuthのアクセストークンを含んだリクエストを送れるようにするために、[Integromat](https://www.integromat.com/
)を設定していきます。

#### シナリオの作成とOAuth設定
Integromatではシナリオ内に各種サービスの連携を定義するので、そのシナリオの作成です。
なお、この辺りの設定は基本的に[Integromat公式チュートリアル](https://support.integromat.com/hc/en-us/articles/360008096679-Calling-Google-APIs-via-the-HTTP-Make-a-OAuth-2-0-request-module)(手順12以降)に結構詳細な説明があってその通りに進めるだけなので適度に割愛します。

まずIntegromatにログインしたら、新規シナリオを作成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/b912852a-9fd3-1ed1-36a2-7643dbeb3b6b.png" width="600">

サービスを選ぶ画面が表示されますが、まずは単純に先ほどのAPIを叩く部分だけ作れれば良いのでHTTPだけ選択して先に進みます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/73aecb4a-2fab-5a49-2ce7-73ba86c4ec41.png" width="300">

シナリオの編集画面に切り替わるので、右下のアイコンからOAuth2.0通信のリクエストを選択します、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/9425d6b3-a5c4-00d0-2a27-85c9fa634415.png" width="600">

HTTPモジュールのアイコンが画面上に追加されるのでそれをクリックして設定画面を開きます。
すると各種情報を設定するウインドウが開くので、OAuth接続の情報を設定するために一番上のConnectionの「Add」を選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/ce2fdbe7-3bc8-bb21-57c2-dbb6c72c7deb.png" width="400">

するとOAuth認証用の情報を入力するウインドウが出てくるので、入力欄を埋めていきます。(URIなどはチュートリアルのページに文字で記載があるのでそれをコピペしてください。)
補足としてはこんなところです。

* ここでGCPのプロジェクト作成後にメモしておいたClient IdとClient Secretを使います。。
* ウインドウ下部の「Show advanced settings」にチェックを入れないと全設定ができないので注意してください。
* Scope separatorはデフォルトで`COMMA`になっているのでこちらも注意です。（GoogleのScope指定はスペース区切りらしい）
* 私の環境では、チュートリアルにInformationとして記載している`prompt - consent`のペアをAuthorize parmetersに入れないと動きませんでした。

![スクリーンショット 2021-03-27 22.28.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/a9534715-9942-b558-9752-5c994699b781.png)

上記画像のように全て入力したあと「Continue」を押すと、GoogleのOAuth認証画面が開くので、APIを実行させたいアカウントで認証を進めてください。
![スクリーンショット 2021-03-27 22.30.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/cb20100f-3eeb-059a-5245-1fc177607fe2.png)

OAuth接続の画面が問題なく進めば、Integromatの画面でOAuthコネクションのウインドウが自動で閉じられます。

#### APIへのリクエスト設定
OAuthの設定が終わったら、あとはhttpリクエスト本体の設定をしていきます。
URLには先に公開しておいたGASの実行可能APIのURLを入力します。
リクエストボディの中身は[こちらのドキュメント](https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run#request-body)を参照のうえ、入力してください。
今回はパラメータが渡ってくることさえ確認できればいいので、こんな感じ。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/e477ccbc-7e06-e440-198e-36038a212b7c.png" width="400"/>
ここまで入力できたらOKを押して設定を完了させます。

#### 接続確認
ここまで設定できたら、テスト実行をしてみます。

テスト実行をするにはモジュールのアイコンを右クリックし、「Run this module only」を選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/4f66de0f-ad3e-4297-a5c4-7ff2535a8f3f.png" width="300"/>

実行が終わるとモジュールの右上に吹き出しが出るので、それをクリックすると通信の詳細が表示されます。
今回はシート名と送ったパラメータ名をただ返すだけのAPIなので、このようなレスポンスが返ってくれば成功です。
ここでステータスコードが400などで返ってくる場合は認証周りなどがおかしいので設定を見直しましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/6902f825-f0ff-aaa5-10c1-2732b768c02a.png" width="500"/>

なお、Googleのアクセストークンは有効期限が1時間なので、初回の実行から1時間以上あけて再度実行した時にはリフレッシュトークンを用いたアクセストークンの自動更新が走ります。
基本的にはこちらで何もしなくてもIntegromatの裏側で更新してくれるのですが、もし1時間後に実行してステータスコードが401になる場合は、OAuthの設定の見直しやクライアントの作り直しなどをすれば解決するかもしれません。

以上でIntegromatでOAuth認証を含めてGASのAPIを実行する設定は完了です。
画面下部のアイコンからシナリオを保存しておきます。

### 外部サービスのwebhookとの接続
ここまできたら、あとは外部サービスのwebhookをIntegromatで拾って、その内容を先ほどのhttp通信のbodyに反映させられれば完成です。

#### Integromat上でのwebhook受け口準備
先ほどのシナリオ編集画面上で、下の画像のような「？」のアイコンが出ていると思います。
これを選択するとモジュール選択画面になるので、「Webhooks」を検索してその中の「Custom webhook」を選択します。
※Chatwork専用のモジュールも存在しますが、あくまでサンプルなので今回は汎用的なWebhooksを使用しました
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/9994687e-ce3b-6801-e3b3-346fbcf95a19.png" width="300"/>

Webhooksのモジュールが画面に表示されたら、httpの時と同じようにクリックして設定を開きます。
Webhookの設定が空になっているので「Add」をクリックしてコネクション設定を追加します。
ここではwebhookの名前さえ入れておけば他は何も入力しなくても設定を勧められるのですが、今回はadvanced settingsの「JSON pass-through」にチェックを入れます。
これにチェックを入れると渡ってきたJSONを1つの文字列としてそのまま使用することができます。
今回チェックを入れるのは、Integromat上でJSONの中身を見たりするのではなく、そのままGASに流したいので文字列のままの方が扱いやすいためです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/d66e8f71-c848-7963-3eef-47458f5a9105.png" width="300"/>

以上を設定して「Save」を押すと、webhookの欄が埋まるのと同時に、URLが表示されます。
これがIntegromatがwebhookを受付けるためのURLですので、こちらをコピーしておきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/2e1328e0-3ffe-8c84-a077-9afa73be8d5f.png" width="400"/>

また、同時に画面中央あたりに「Stop」というボタンが出ています。
その上にある説明文の通りですが、これはwebhookでどんなデータ構造が送られてくるのかをチェックするために待機している状態です。
この状態で実際のwebhookを受け取ると、「このwebhookはこういうデータ構造なんだ」と解釈してくれますので、Chatwork側の設定を推めます。

#### Chatwork側のwebhook設定
ここは本題ではないので簡単に。
先ほどコピーしておいたIntegromatのURLをChatwork側のwebhookに設定します。
今回は特定ルームへの投稿でキックされるwebhookとします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/967cb7ce-f8a5-5178-ce2c-ee7782dda6e5.png" width="400"/>

設定が終わったら、対象のルームで投稿してみます。
すると、Integromatでの方で実行待ちしていた画面が切り替わるはずです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/9c6baede-7e47-ba3a-51b7-514c645ed715.png" width="400"/>

これでChatworkのwebhookからIntegromatでデータを受け取る準備はできました。

#### webhookのデータをGASに流す設定
Integromatのシナリオ画面にて、webhooksモジュールの右側にカーソルを当てると現れる「+」から新たにJSONモジュールを繋ぎ、Aggregate to JSONを選択します。
後にGASのAPIに投げるときのparametersに入れるJSONを生成するためです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/8d0c1511-767d-72b8-eb58-3d784e6519a2.png" width="300"/>

先ほどの手順でwebhookのデータ構造がうまく定義できていれば、画像のように、テキストエリアにフォーカスを当てると、選択肢が出てきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/c98b28ec-f072-c749-88d0-ada8065163fb.png" width="５00"/>

この`value`と`chatwork_webhook_signature`をそのままJSONでGASに流したいので、データ構造と値を定義していきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/0f6b8691-e3c4-7d9b-e4f3-97dd75f98a08.png" width="５00"/>

そして、最後に今作ったJSONモジュールと、最初に作ったGASのAPIにリクエストを投げるhttpモジュールを繋ぎ、リクエストボディのparametersの部分をJSONモジュールで作成した文字列に変えます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/efb90066-4eb7-2d6c-6a2b-3b5983732080.png" width="５00"/>

Integromatの画面では最終的にこのような形になっているはずです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/da577cf5-db91-f1ad-72c3-32bcbf2eaff8.png" width="400"/>

シナリオを保存したら、最後に画面左上の「<-」ボタンを押して右上のアクティブ状態を「ON」にします。
![スクリーンショット 2021-03-28 0.24.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/7802dda2-aef1-b357-16bd-b17168a0477a.png)

以上で、webhookからGASのAPIにつなぐ設定は完了です。

#### 動作確認
Chatworkの監視対象のルームに戻り、また適当な投稿をしてみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/ba720834-3a7d-5b55-53d5-995afe4a6aa0.png" width="300"/>

IntegromatのHISTORYタブに移動し、最新の実行ログのDetailsを確認していきます。
HTTPを試しに実行した時のようにモジュールに吹き出しがついているはずなので、それをクリックして実際の通信状況について見てみると、webhooksで受け取ったjsonがそのままhttpのモジュールでGASに送信されていることがわかります。
※valueの中身のJSONは文字列として受け取った関係でエスケープが入ってます

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/035327d7-2a8c-72c6-eb45-5eac1e381dc4.png" width="400"/><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86185/c715085c-7775-2b20-e578-7aec6a39892b.png" width="400"/>

これで全工程が完了です。
今回はダミーのスクリプトでしたが、接続するサービスやスクリプトの内容を工夫すれば様々なタスクを自動化できそうです。

# 最後に
今回は外部サービスのwebhookをGASの実行可能APIで受け取るための構成を試してみました。
IntegromatもGCPを用いたOAuth認証もほぼ触ったことなかったのでもしかすると非常に効率の悪いやり方をしてしまっているかもしれません。
もし何かおかしな点があればご指摘いただけますと幸いです。