GASでSlackのスラッシュコマンドを作ってみた

## はじめに
Slackにはスラッシュコマンドという機能があります。`/hoge`で特定のアクションを実行することができます。こちらのスラッシュコマンドは自作することが可能です。今回は、Google Apps Script（GAS）を使って簡単なスラッシュコマンドを自作したので、その作成の手順を簡単にまとめたいと思います！

## GASでSlackのスラッシュコマンドを作ってみた
### claspとTypeScriptの導入
今回はGASのCLIツールである[clasp](https://github.com/google/clasp)を使って、Slackのスラッシュコマンドを作成しました。claspのバージョンが1.5.0以上のものからGASでもTypeScriptが使えるようになったので、今回は試しに使ってみてようと思います。

claspやTypeScriptの導入の仕方は、以下のものを参考にしました。

https://qiita.com/zumi0/items/a4dd6e00cad7ee341d77

https://github.com/google/clasp/blob/master/docs/typescript.md

ディレクトリ構成は以下のような感じです。

```
gas_slackcommand
├── .clasp.json
├── node_modules/
├── package-lock.json
├── package.json
├── src
│   ├── appsscript.json
│   └── main.ts
└── tsconfig.json
```


### スラッシュコマンド
今回自作するスラッシュコマンドは、以下のようにTodoを登録・表示・削除できるようなものを作成しようと思います。

- `/todo register hoge`でスプレッドシートに`hoge`を登録する
- `/todo list`でスプレッドシートに登録されているTodoを表示する
- `/todo done hoge`でスプレッドシートから`hoge`を削除する

### GASでの準備
まずはGASのコードをかきます。

```typescript:main.ts
// トークンなどは、GASのプロパティ機能を使って登録しておく
const VERIFICATION_TOKEN = PropertiesService.getScriptProperties().getProperty('VERIFICATION_TOKEN');
const CHANNEL_NAME = PropertiesService.getScriptProperties().getProperty('CHANNEL_NAME');

const SHEET_ID = PropertiesService.getScriptProperties().getProperty('SHEET_ID');
const SHEET_NAME = PropertiesService.getScriptProperties().getProperty('SHEET_NAME');

// POSTメソッドを使うとき、GASでは必ずdoPostという関数を用意しておく
const doPost = (e) => {
    if (e.parameter.token != VERIFICATION_TOKEN) {
        throw new Error('Invalid token');
    }

    const command = e.parameter.text;

    // コマンドのオプションによって使い分けできるようにしておく
    const blocks = /^register .+/.test(command) ? registerTodo(command.replace("register ", "")) :
                   /^list$/.test(command) ? showTodos() :
                   /^delete .+/.test(command) ? deleteTodo(command.replace("done ", "")) :
                   {
                    "blocks": [
                        {
                            "type": "section",
                            "text": {
                                "type": "mrkdwn",
                                "text": "残念...コマンドが違うよ..."
                            }
                        }
                    ]
                   };

    return ContentService.createTextOutput(JSON.stringify(blocks)).setMimeType(ContentService.MimeType.JSON);
}

// Todoを登録しておく関数
const registerTodo = (todo) => {
    const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
    sheet.appendRow([todo]);
    console.log('Registered!')

    const blocks = {
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": `*ToDoを登録したよー！* \n Todo:${todo}`
                }
            }
        ]
    }

    return blocks; 
}

// Todoを削除する関数
const deleteTodo = (todo) => {
    const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
    const lastRow = sheet.getLastRow();
    const values = sheet.getRange(1,1,lastRow,1).getValues();
    const todos = values.map(value => value[0]);

    const deleteTodoRow = todos.indexOf(todo) + 1;
    sheet.deleteRow(deleteTodoRow);
    console.log('Deleted!');

    const blocks = {
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": `*ToDoを削除したよー！* \n Todo:${todo}`
                }
            }
        ]
    }

    return blocks; 
}

// Todoの一覧を返す関数
const showTodos = () => {
    const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
    const lastRow = sheet.getLastRow();
    const values = sheet.getRange(1,1,lastRow,1).getValues();
    const todos = values.map(value => {
        const todoBlock = {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": `・ ${value[0]}`
            }
        }

        return todoBlock;
    });

    const blocks = {
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": "*ToDoの一覧だよー！*"
                } 
            },
            {
                "type": "divider"
            },
            ...todos
        ]
    }

    return blocks;
}
```

今回Slackに送るメッセージの内容は、[Block Kit Builder](https://app.slack.com/block-kit-builder/T046LSQ9GQ4#%7B%22blocks%22:%5B%5D%7D)を使って作成しました。Slack Appでは、結構リッチなメッセージが表示されたりするものもありますが、そちらはこちらで作成することが可能です！

https://app.slack.com/block-kit-builder/T046LSQ9GQ4#%7B%22blocks%22:%5B%5D%7D

個人的に結構面白かったので、興味ある方はぜひ使ってみてください！

コードが書き終わったら、GASのIDE上でデプロイします。デプロイする手順は以下です。

1. GASのIDEを表示して、画面右上のデプロイ > 新しいデプロイを選択
1. モーダルが表示されるので、種類の選択 > ウェブアプリを選択し、設定ではアクセスできるユーザーを`全員`に変更してデプロイ
    - デプロイする際に、諸々の許可が求められるので許可する
1. デプロイが完了するとURLが表示されるのでコピー
    - 後で行うSlackの設定で利用

### Slackでの準備
以下の手順を踏んでスラッシュコマンドの設定を行いました！

1. アプリを作成する
    - スラッシュコマンドを作成するために、まずはSlack Appを作成する
    - [Slack API: Applications | Slack](https://api.slack.com/apps)にアクセスして`Create New App`をクリック
    - アプリの設定方法を選択するモーダルが出てくるので`From scratch`をクリック
    - 作成するアプリの名前と、それを導入するSlack workspaceを選択して、アプリを作成する
1. アプリの設定を行う
    - 具体的にスラッシュコマンドを設定する
    - Basic Information > Add features and functionality > Slash Commands > Create New Commandの順に遷移
    - スラッシュコマンドに必要な情報を入力する
        - スラッシュコマンド
        - GASをウェブアプリとしてデプロイした時に表示されたURLをRequest URLに入力
        - 簡単なスラッシュコマンドの説明
        - スラッシュコマンドにオプション等をつける場合の簡単な説明
    - コードで使用しているVerification Tokenは、Basic Information > App Credentials > Verification Tokenの順で辿ると取得できる
        - 取得後は、GASのプロパティ機能を使い登録する
    - Install App > Install to Workspaceの順にクリックし、問題なければ許可する

### 実際に使ってみる
実際に使ってみると、Todoの登録、削除、一覧の表示ができました！
![スクリーンショット 2022-10-20 14.14.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581736/474c180b-f1f9-6771-8450-eb4b338d133a.png)


## さいごに
今回はSlackで簡単なスラッシュコマンドを作成してみました！コードに関しては要改善といった感じですが、参考になれば幸いです。