Slackアプリについて調べたことと実装例

# 記事の背景
Slack統合されたWebアプリケーションを作るために公式ドキュメントを読み漁ったので、備忘のために残す。
また、実装例を簡単に説明する。

#### 参考にしたもの
- [Slack API 公式](https://api.slack.com/)
- [Bolt入門ガイド](https://slack.dev/bolt/ja-jp/tutorial/getting-started)
  - BoltというSlackアプリ構築のフレームワークのドキュメントだが、日本語なのでサクッとSlack APIの挙動をイメージするために利用した。
- [slack-ruby-client](https://github.com/slack-ruby/slack-ruby-client)
  - Rubyのgem

# やったこと
- Slackアプリの作成
  - [Slackアプリのページ](https://api.slack.com/apps)からGUIで作成する。
- WebアプリケーションからSlackアプリを動かすために、API呼び出し処理を作成
  - Rubyの場合は[slack-ruby-client](https://github.com/slack-ruby/slack-ruby-client)を使う。(Webアプリケーションからタイミングを指定してSlack通知を行うだけなら[slack-notifier](https://github.com/stevenosloan/slack-notifier)でも十分。)
  - Python/Node/Javaの場合には、公式がSDKを用意しており、他のいくつかの言語でもslack-ruby-clientのようなライブラリが提供されている。

# 前提
## 登場人物とその関係
- 「Webアプリケーション」と「Slackワークスペース」を結びつけるのが「Slackアプリ」
- 「Webアプリケーション」と「Slackアプリ」を通信するのが「API」

## Slackアプリの機能
Slackアプリの機能は全て以下に集約されている。
<img width="700" alt="スクリーンショット 2020-04-16 11.40.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/a822e7b4-6ad4-83e9-604c-dea1913bfd2c.png">

### Incoming Webhook
機能は制限されるが、手軽に投稿できる機能。
投稿後、投稿したメッセージを削除したり、変更したいという場合には、Botsを使う必要がある。

### Interactive Components
Slackに投稿されたメッセージの、ボタンや選択ボックス等のコンポーネントの操作をトリガとして、アプリケーションにリクエストを送る機能。
##### 例(Rollbar)
<img width="564" alt="スクリーンショット 2020-04-16 13.10.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/7fb370cb-05e6-f100-238f-b7510ff9d78d.png">

また、上記コンポーネント以外にもショートカットによって、アプリケーションにリクエストを送ることができる。
ショートカットの場所はSlackクライアントのバージョンによって頻繁に変わるが、投稿ボックスの側の⚡️マークや、メッセージ単位の縦3点リーダーからショートカットを呼び出すことができる。

<img width="700" alt="スクリーンショット 2020-04-16 13.02.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/1e16ce10-3c63-937c-e67c-83c7d038c358.png">

ただ、残念なことに、このInteractive ComponentsによりリクエストできるURLはSlackアプリにつき1つとなっている。
そのため、アプリケーション側で、どのコンポーネントからのどのようなリクエストなのかを識別子(ActionID、BlockID、CallbackIDがある。後述のWebAPIにおけるメッセージ投稿の際にJSONに含める。)により判断する必要がある。
実装例を後述している。

### Slash Commands
投稿ボックスにてコマンドを呼び出すことで、アプリケーションにリクエストを送る機能。
##### 例(zoom)
<img width="559" alt="スクリーンショット 2020-04-16 13.36.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/0de1c6f3-ee15-5a4f-c5a0-857a3a253c62.png">

Interactive ComponentsがSlackアプリ1つあたりリクエストURL1つだったのに対して、Slash Commandsはコマンド1つあたりリクエストURL1つとなっているので、アプリケーション側での処理が簡易になるが、コマンドに馴染みのない人には操作が難しく感じるかもしれない。

### Event Subscriptions
後述のEvents APIと密接に結びついている。
Slackワークスペース上では様々なイベント(投稿、絵文字リアクション、チャンネル作成、ユーザー追加等)が発生するが、その中のどのイベントが発生した場合に、アプリケーションにリクエストを送るかを指定できる。

こちらもInteractive Componentsと同様に、Slackアプリ1つあたりリクエストURLは1つ。

### Bots
後述のWeb APIと密接に紐づいている。
アプリケーションからのWebAPIの呼び出しに反応して、Slackワークスペース上で様々な操作(投稿、チャンネル操作等)をボットが行なっているように見せることができる。
Slackワークスペース上でユーザーの操作以外で何かが発生している場合は、このBotsかIncomming Webhookが実施している。

### Permissions
これだけ他の機能とは毛色が異なる。
SlackアプリがSlackワークスペースに対して「できること」「許されていること」(スコープと呼ばれる)を選択できる。
Botがチャンネルに参加するスコープ、Botがチャンネルの投稿を読むスコープ等、細かく分割されている。
[スコープ一覧](https://api.slack.com/scopes)

Slackアプリには、Tokenと呼ばれるAPIアクセス管理用のトークンがあり、このTokenそれぞれに対してスコープを複数選択する仕組みとなっている。

- Bot user tokens
  - ボットに紐付くアクセストークン。
  - Slackアプリ1つあたり1つ。
- User tokens
  - ユーザーに紐付くアクセストークン。
  - このアクセストークンを利用して、WebAPIを呼び出せば、そのユーザーになりすましてワークスペース上で操作できる。

※Verification tokensというのもありセキュリティ的には利用が好ましくないとされている。また、過去にはWorkspace tokens, Legacy tokensというものがあった。

## APIの種類
Slackアプリの機能を実現する、アプリケーションとSlackアプリ間を通信する主要なAPIは以下の2種類。
他にも、RTM API、Status API等のニッチなAPIがいくつか存在する。

### Web API
アプリケーションから呼び出して、Slackを動作させるためのAPI。
SlackのGUIでできる処理をほぼ全てこのWeb APIから実行できる。
[Web API 一覧](https://api.slack.com/methods)
#### 例
- チャンネルやDMからのメッセージを送信
- チャンネルとユーザーグループを作成、変更
- ファイル、メッセージ添付ファイル、絵文字リアクションのアップロードと編集
- ワークスペースにユーザーを追加、削除

※ただ、Slackに問い合わせした結果、残念ながらパブリックチャンネルをプライベートチャンネルに変更するAPIは存在しなかった。

### Events API
Web APIとは逆に、Slackの動作をアプリケーションに伝えるAPI。
[Event API 一覧](https://api.slack.com/events-api)

#### 例
- ユーザーがメッセージの投稿、チャンネルの作成や変更、ファイルの追加や変更を行った場合にイベントを受信する
- ユーザーがチャンネルにピン、スター、絵文字リアクションを追加したときに、イベントを受信する

# 実装してみた
### やりたかったこと

- アプリケーションでの実行をトリガに、ボットがSlackに投稿する。
- 投稿したメッセージ上のボタンを押すと、モーダルが開かる。
- モーダルのテキストボックスにユーザーがフィードバックを入力して、内容をアプリケーションで受け取り保存。

※投稿するメッセージのレイアウトやボタンの仕組みは[Block Kit](https://api.slack.com/block-kit)参照

### 環境
- Ruby 2.7.0
- Rails 6.0.2.1
- slack-ruby-client 0.14.6

### 利用した機能、処理

- Slackアプリの作成
  - 投稿にBotsを利用
  - モーダルを開いたり、ユーザー登録内容を受け取るためにInteractive Componentsを利用
  - モーダル操作にスコープは不要なので、スコープは`chat:write`のみ
- WebアプリケーションからSlackアプリを動かすために、API呼び出し処理を作成
  - slack-ruby-clientの以下メソッドを利用
     - chat_postMessage: チャンネルにメッセージを投稿(レイアウトはjsonで指定)
     - views_open: モーダルを開く(レイアウトはjsonで指定)

### 処理の流れ

(1)アプリケーションでchat_postMessageメソッドを実行し、ボタン付きメッセージを投稿(button_components.json)
(2)ユーザーがボタン押下
(3)アプリケーションにボタン押下リクエストが送信 
(4)Controllerでモーダルを開く処理がリクエストされていることを判断
(5)アプリケーションでviews_openメソッドを実行し、モーダルを開く(modal_components.json)
(6)ユーザーがモーダル上のテキストボックスにフィードバックを入力して、登録ボタン押下
(7)アプリケーションに登録ボタン押下リクエストと入力したテキストが送信
(8)Controllerでフィードバック保存のリクエストがされていることを判断
(9)保存処理


```interactions_controller.rb
class InteractionsController < ApplicationController
  def create
    case payload[:type]
    when 'block_actions' # メッセージ上のボタン押下の場合はtypeはblock_actionsとなる
      action = payload[:actions].first
      case action[:action_id]
      when 'feedback_request' # button_components.jsonで指定
        FeedbackModalOpener.call(trigger_id: payload[:trigger_id]) # (4),(5)の処理
      when ...
        (略)
      end
    when 'view_submission' # モーダルの登録ボタン押下の場合はtypeはview_submissionとなる
      case payload[:view][:callback_id]
      when 'feedback'　# modal_components.jsonで指定
        ResultUpdater.call(params: params[:payload])　# (8),(9)の処理
      when ...
        (略)
      end
    end

    head :ok
  end

  private

  def payload
    @payload ||= JSON.parse(params[:payload]).deep_symbolize_keys
  end
end
```

```button_components.json
  [
    {
      "type": "section",
      "block_id": "feedback_request",
      "text": {
        "type": "mrkdwn",
        "text": "test_name"
      },
      "accessory": {
        "type": "button",
        "text": {
          "type": "plain_text",
          "text": "記載する"
        },
        "action_id": "feedback_request"
      }
    }
  ]
```

```modal_components.json
  {
    "callback_id": "feedback",
    "type": "modal",
    "title": {
      "type": "plain_text",
      "text": "test"
    },
    "submit": {
      "type": "plain_text",
      "text": "送信"
    },
    "close": {
      "type": "plain_text",
      "text": "閉じる"
    },
    "blocks": [
      {
        "type": "input",
        "block_id": "reason",
        "element": {
          "type": "plain_text_input",
          "action_id": "reason",
          "multiline": true,
          "placeholder": {
            "type": "plain_text",
            "text": "フィードバックを記載してください。"
          }
        },
        "label": {
          "type": "plain_text",
          "text": "フィードバック",
        }
      }
    ]
  }
```

# まとめ
Slackをインターフェースとするあらゆるものが作れてSlackアプリ便利