REST APIを用いてGASからGoogle Chatに「返信」を行う＠インラインスレッド対応

# はじめに
本稿では、Google ChatのWebhookを用いて、Google Apps Script(GAS)のBOTからインラインスレッドに自動返信する際の注意点をまとめます。

# 背景
Google Chatでは、2022/12/11にインラインスレッド機能がリリースされ、続く2023/03/13に旧来のトピック機能が廃止されました。
トピック機能が廃止されるまでは、スペース作成時に「会話をトピック別に整理する」のチェックをオンとすることでトピック表示できましたが現在は、新しく作成されるスペースはすべてインラインスレッド形式になります。

トピックの表示例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/d1211408-647f-505b-c139-e319d122e318.png)

インラインスレッドの表示例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/1d6b0379-a623-4c03-f907-701ea0b00f4f.png)

::: note info
2023/05現在、過去作成済みのトピック表示スペースはそのまま使えます。
:::

Google workspace Updates note
https://workspaceupdates-ja.googleblog.com/2023/03/2023-3-13-google-chat.html

# GASを用いたBOTからの投稿
Google Chatに対してBOT投稿をするための方法は、www上に多数公開されています。
また、トピック表示に対して返信を行う方法も多数公開されており、比較的容易に実現が可能です。

本稿では、投稿時のレスポンスも含めて簡単に記載します。

参考：Google Chat APIガイド
https://developers.google.com/chat/api/reference/rest?hl=ja

GASで、投稿をするサンプルは以下になります。
事前にチャットスペース側で「アプリと統合」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/e36dc43b-c53d-f3c1-7b93-2278b4b053fa.png)

から、「Webhookを管理」に移動し
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/b1fa044d-aad8-ef80-81f2-63fe37c1f157.png)

webhookを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/2c2edb62-bf97-14c0-be3b-dd30bc254aa9.png)

追加後webhookのURLをコピーし、GASコードのurlへ適用します。


::: note info
Google Chatの利用シーンとして、プロジェクト毎、係の仕事毎などでスペースが設立されることが多いかと思います。その際に、各スペースでデイリーの報告などをメッセージで乱立させるとログが流れすぎる問題があります。
本稿のGASは、デイリー報告用のスレッドに対するしおり機能をイメージして日付を投稿するサンプルとしています。
:::

GASコードサンプル
``` js
function bot_message() {
  var url = "https://chat.googleapis.com/v1/spaces/XXXXXXXXXXX/messages?key=XXXXXXXXXXXXXXX";
  
  var dt = new Date();
  var today = dt.getFullYear() + "年" + (dt.getMonth()+1) + "月" + dt.getDate() + "日";
  
  var json_msg = {
    "text" : "```" + today + "```\n",
  }
  
  var json = JSON.stringify(json_msg);
  var options = {
    "method" : "POST",
    "contentType" : "application/json; charset=utf-8",
    "payload" : json
  }
  
  var response = UrlFetchApp.fetch(url, options);
  Logger.log(response);
}
```

## トピックに投稿する
トピック表示のスペースに対して投稿を行った場合、新規投稿が行われます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/78dde048-7ec1-c41f-b527-d454186351c1.png)

塗りつぶしていますが、投稿のレスポンスを確認すると、生成されたスレッドの識別子（name）が把握できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/7d421dc1-78e0-d075-499e-581a2c106c2b.png)

このスレッド識別子を指定して投稿することで、当該トピックに対して返信の形で投稿することができます。

``` js
function bot_message() {
  var url = "https://chat.googleapis.com/v1/spaces/XXXXXXXXXXX/messages?key=XXXXXXXXXXXXXXX";
+  var thread = "spaces/XXXXXXXXXXX/threads/XXXXXXXXXXXXXX";
  
  var dt = new Date();
  var today = dt.getFullYear() + "年" + (dt.getMonth()+1) + "月" + dt.getDate() + "日";
  
  var json_msg = {
    "text" : "```" + today + "```\n",
+    "thread": {
+      "name": thread,
+    },
  }
  
  var json = JSON.stringify(json_msg);
  var options = {
    "method" : "POST",
    "contentType" : "application/json; charset=utf-8",
    "payload" : json
  }
  
  var response = UrlFetchApp.fetch(url, options);
  Logger.log(response);
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/1920cd89-7528-b790-f16e-54fcabb6ca7b.png)


## インラインスレッドに投稿する
では、インラインスレッドのスペースに対しても同じことを実施してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/1b137deb-3233-c21c-d877-dc15decc65ec.png)

実行ログはこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/22af23b6-b84a-baf4-8fba-10f7d364beb2.png)

::: note warn
※説明のため、末尾3文字分のIDだけマスク外にしています。
:::


同様に、スレッド識別子を指定して再度実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/3bbdb9d4-3914-3180-2c4e-204771f4fb46.png)

投稿には成功しましたが、返信ではなく新規スレッドになってしまいました。
ログを確認すると異なるスレッド識別子が返却されており新規投稿となっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/8e87c862-b385-8803-23b8-4e343d7f03cd.png)

Google ChatのREST APIの説明を読むと、スレッドに返信するには次のオプションを設定する必要があるようです。

## MessageReplyOption
https://developers.google.com/chat/api/reference/rest/v1/spaces.messages/create?hl=ja#MessageReplyOption

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/b143b4b3-4bdc-d3f1-775c-9abe957e88fe.png)

トピック表示スペースに対してはこのオプションの指定はせずとも同一スレッドへの返信となりましたが、インラインスレッドのトピックに対してはこのオプションを正しく指定する必要がありそうです。

メッセージと共に送信する方法を試しましたが何れもエラーとなり、最終的にGETの引数として指定することで動作してくれました。

``` js
function bot_message() {
  var url = "https://chat.googleapis.com/v1/spaces/XXXXXXXXXXX/messages?key=XXXXXXXXXXXXXXX";
+  var res_swith = "&messageReplyOption=REPLY_MESSAGE_OR_FAIL";
  var thread = "spaces/XXXXXXXXXXX/threads/XXXXXXXXXXX_b8";
  
  var dt = new Date();
  var today = dt.getFullYear() + "年" + (dt.getMonth()+1) + "月" + dt.getDate() + "日";
  
  var json_msg = {
    "text" : "```" + today + "```\n",
  }
  
  var json = JSON.stringify(json_msg);
  var options = {
    "method" : "POST",
    "contentType" : "application/json; charset=utf-8",
    "payload" : json
  }
  
-  var response = UrlFetchApp.fetch(url, options);
+  var response = UrlFetchApp.fetch(url + res_swith, options);
  Logger.log(response);
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/37129a8b-c055-0368-7ec5-a3ab647fb822.png)

# まとめ
発言をHookしてBOTにレスポンスさせる場合など、返信を活用したいシーンは多く、インラインスレッドとなってから二カ月ほど経ちましたが、検索をしてもなかなかサンプルコードに出会わなかったため、Qiita投稿として記録しておきます。

# 最終GASコードサンプル
``` js
function bot_message() {
  var url = "https://chat.googleapis.com/v1/spaces/XXXXXXXXXXX/messages?key=XXXXXXXXXXXXXXX";
  var res_swith = "&messageReplyOption=REPLY_MESSAGE_OR_FAIL";
  var thread = "spaces/XXXXXXXXXXX/threads/XXXXXXXXXXXXXX";
  
  var dt = new Date();
  var today = dt.getFullYear() + "年" + (dt.getMonth()+1) + "月" + dt.getDate() + "日";
  
  var json_msg = {
    "text" : "```" + today + "```\n",
  }
  
  var json = JSON.stringify(json_msg);
  var options = {
    "method" : "POST",
    "contentType" : "application/json; charset=utf-8",
    "payload" : json
  }
  
  var response = UrlFetchApp.fetch(url + res_swith, options);
  Logger.log(response);
}
```