Slack上に外部APIを利用したBotを作成したい

# はじめに
現業務にてチーム内の非エンジニアメンバーからGoogleMaps APIからの情報を簡単に取得できるようなツールがほしいとのニーズがあったため作成方法の備忘メモを残しておく。

## What is this?
- Google Maps Platformが提供しているPlaces APIを用いた緯度経度or場所名を元にプレイスIDと住所、建物名称を取得するSlack Botの設定方法メモ
- 利用サービス: 
	- [Places API by Google Maps Platform](https://developers.google.com/maps/documentation/places/web-service/overview?hl=ja)
	- [Slack api](https://api.slack.com/)
	- [Google Apps Script](https://script.google.com/home)
- 利用イメージ:
    - Slack上で`/get_place_id 場所名`のように建物の名称や緯度経度を指定するとGoogle Mapsの情報が返却される
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/5cf00789-ba8b-a592-e2f4-abe380eaeea5.png)
- 必要なもの:
	- Places APIキー発行用GoogleCloudアカウント
	- Slackアカウントおよび、アプリ作成・追加権限
	- GAS作成用のGoogleアカウント
- 料金: 無料(2025.01時点)
	- Places API:
		- 100,000 リクエスト/月までなら無料
		- memo: 月200ドルの無料枠があるので余裕で収まる
		- ref: https://developers.google.com/maps/documentation/places/web-service/usage-and-billing?hl=ja#text-search
	- GAS:
		- 20,000 リクエスト/日までなら無料
		- memo: 現サービスの運用上、一日多くて30件くらいなので余裕で収まる
		- ref: https://developers.google.com/apps-script/guides/services/quotas?hl=ja
	- Slack Bot:
		- 基本無料
		- ref: https://app.slack.com/plans/TCQ1A3AMT?geocode=ja-jp


# 設定手順
## 1. Places APIの有効化とトークン発行
- Google Cloudにアクセスし、「APIとサービス」に移動する
- 「+APIとサービスを有効にする」を押す
- `Places API`を検索し選択 >  有効化
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/6cdc8ef2-cc26-6ac6-c20b-e43bfec1889d.png)
- APIとサービスにて「鍵と認証情報」に移動する
- 「+認証情報を作成」> 「APIキー」を選択
- 作成されたキーを保存する
	- 後続のGASにて設定する
- memo: キーの制限やAPIの制限については一旦デフォルトのままとした
	- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/540b8302-5f71-112d-7f81-a51ccc0dd9ab.png)

## 2. Slack App作成
- [Slack api](https://api.slack.com/)にアクセスし、Your appsに移動する
- 「Create New App」を押す
	- 「From scratch」を選択する
	- App Nameにアプリの名前を入れる
	- Pick a workspace...にてデプロイしたいSlackのワークスペースを選択する
	- 「Create App」を押す
	- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/8f7a532c-4957-c327-c819-19f4382b430a.png)
- 必要に応じてBasic Information情報を入力する(アイコン画像や概要など)
- Verification Tokenを控えておく
	- 後続のGASにて設定する
- (左メニュー) Features内のOAuth & Permissionsに移動する
- Scopesの項目にて下記Bot Token Scopesの設定を行う
	- `commands`を追加
	- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/56c1d124-ceba-8048-5644-34ef338c415b.png)


## 3. Google Apps Scriptのデプロイ
- [Apps Script](https://script.google.com/home)にアクセスし「+新しいスクリプト」を押す
	- 必要に応じてプロジェクト名を設定する
- (左メニュー) プロジェクトの設定に移動する
	- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/e33fb078-b062-240f-265c-ab2a06ca6b69.png)
- スクリプトプロパティに下記を追加する
	- プロパティ名: `GOOGLE_API_KEY`
		- 値：手順1にて取得したPlaces APIのキー情報
	- プロパティ名: `SLACK_VERIFICATION_TOKEN`
		- 値：手順2にて取得したVerification Token情報
	- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/fe57f98a-0114-4de9-d476-850b15e11922.png)
- (左メニュー) エディタに移動する
- コード.gsに下記をコピペ
    ```
    function doPost(e) {
      const scriptProperties = PropertiesService.getScriptProperties();
      const SLACK_VERIFICATION_TOKEN = scriptProperties.getProperty("SLACK_VERIFICATION_TOKEN");
      const GOOGLE_API_KEY = scriptProperties.getProperty("GOOGLE_API_KEY");
    
      // URLエンコード形式のデータを解析
      if (!e || !e.parameter) {
        return ContentService.createTextOutput("Invalid request: No parameters found.")
          .setMimeType(ContentService.MimeType.TEXT);
      }
    
      const params = e.parameter;
    
      // Slack Verification Tokenチェック
      if (params.token !== SLACK_VERIFICATION_TOKEN) {
        return ContentService.createTextOutput("Unauthorized").setMimeType(ContentService.MimeType.TEXT);
      }
    
      const input = params.text.trim(); // ユーザー入力を取得
      let placeData;
      let errorMessage = null;
    
      try {
        if (input.includes(",")) {
          // 緯度・経度の場合
          const [lat, lng] = input.split(",").map(Number);
          if (isNaN(lat) || isNaN(lng)) throw new Error("Invalid coordinates format.");
          placeData = getPlaceDataByCoordinates(lat, lng, GOOGLE_API_KEY);
        } else {
          // 場所名の場合
          placeData = getPlaceDataByName(input, GOOGLE_API_KEY);
        }
      } catch (error) {
        errorMessage = error.message || "An error occurred.";
      }
    
      const response = {
        response_type: "in_channel", // 公開応答
        text: errorMessage
          ? `Error: ${errorMessage}`
          : placeData
          ? `Name: ${placeData.name || "建物名称が利用できません"}\nPlace ID: ${placeData.placeId}\nAddress: ${placeData.address}`
          : "No place found.",
      };
    
      return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
    }
    
    // 緯度・経度からPlace ID、住所、建物の名称を取得
    function getPlaceDataByCoordinates(lat, lng, apiKey) {
      const url = `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${lat},${lng}&radius=1&key=${apiKey}&language=ja`;
      const response = UrlFetchApp.fetch(url);
      const data = JSON.parse(response.getContentText());
    
      if (data.results && data.results.length > 0) {
        const result = data.results[0];
        return {
          name: result.name || "建物名称が利用できません", // 建物の名称
          placeId: result.place_id,
          address: result.vicinity || "住所情報が利用できません", // 日本語住所
        };
      }
      return null; // 見つからない場合
    }
    
    // 場所名からPlace ID、住所、建物の名称を取得
    function getPlaceDataByName(name, apiKey) {
      const url = `https://maps.googleapis.com/maps/api/place/textsearch/json?query=${encodeURIComponent(name)}&key=${apiKey}&language=ja`;
      const response = UrlFetchApp.fetch(url);
      const data = JSON.parse(response.getContentText());
    
      if (data.results && data.results.length > 0) {
        const result = data.results[0];
        return {
          name: result.name || "建物名称が利用できません", // 建物の名称
          placeId: result.place_id,
          address: result.formatted_address || "住所情報が利用できません", // 日本語住所
        };
      }
      return null; // 見つからない場合
    }
    
    ```
- 右上の「デプロイ」ボタンを押す > 「新しいデプロイ」を選択
- 選択の種類の右側歯車マークから「ウェブアプリ」を選択する
- 「アクセスを承認」を押す
- Googleアカウントにログイン後、下記の画面が表示された場合は「Advanced」をクリックし、「Go to プロジェクト名」を選択、「Allow」ボタンを押す
	- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/d7c3ac03-2e2b-df5f-e65f-c10771416827.png)
- デプロイが完了する
	- ウェブアプリURLを控えておく
		- 後続のSlackApp設定にて利用する

## 4. Slack Appにおけるコマンド設定
- [Slack api](https://api.slack.com/)にアクセスし、作成したアプリに移動する
- (左メニュー) Slash Commandsに移動する
- 「Create New Command」ボタンを押す
	- Command: `/get_place_id` (任意のコマンド名でok)
	- Request URL: 手順3にて作成したGASのウェブアプリURLをペースト
	- Short Description: 任意で記載(下記のような感じでSlack UI上で表示される)
	- Usage Hint: 任意で記載(下記のような感じでSlack UI上で表示される)
	- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/4e592a35-8643-2b2d-522a-263842338405.png)
	- 右下の「Save」を押す
- (左メニュー) 「Install App」に移動する
- 「Install to ワークスペース名」を押す

## 動作確認
- Slackの任意のチャンネル or 自分へのDM等にて下記コマンドを実行してみる
	- `/get_place_id 大手町プレイスウエスト`
	- プレイスIDが返却されたら成功
	- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269076/95375123-b615-6e39-173b-1b6f0e44228b.png)
- うまく返ってこない場合はGAS上の設定やSlack apiの設定を見直す


# おわりに
GASとSlack apiを用いることでサーバレスで簡単にbotが作成できるので今後も活用したい。

# 参考
- [Places APIドキュメント](https://developers.google.com/maps/documentation/places/web-service/overview?hl=ja)
- [Slackで動くChatGPTのチャットボットをGoogle Apps Script（GAS）でサクッと作ってみる](https://zenn.dev/lclco/articles/712d482d07e18c)