Slack ソケットモードの最も簡単な始め方

## ソケットモード（Socket Mode）とは？

Slack は、2020 年に開催された[年次イベント Frontiers](https://slack.com/intl/ja-jp/frontiers) にて「ソケットモード」という Slack アプリの新しい通信方式を[発表しました](https://slack.com/intl/ja-jp/blog/transformation/people-partners-systems-slack)。

これまでの Slack アプリ開発では、イベント API やモーダル送信、ボタンクリック、スラッシュコマンドなどのインタラクティブな操作のハンドリングには、公開された Web エンドポイントを用意して Slack からのリクエストを受信する必要がありました。

ソケットモードは、これを WebSocket での接続に切り替えることができる機能です。昔から Slack アプリやボットに慣れ親しんでいる方であれば **Hubot や RTM (Real Time Messaging) API のように接続できる方式**という言い方がわかりやすいかもしれません（RTM との違いについては、[こちらのコメント](https://qiita.com/seratch/items/1a460c08c3e245b56441#comment-44354816240098892ca8)を参考にしてください）。

## ソケットモード GA リリース :tada:

そのソケットモードがベータ期間を経て 2021年1月12日（米国時間）に正式リリースとなりました！ :tada: 英語となりますが、Slack Platform Blog に[紹介記事](https://medium.com/slack-developer-blog/socket-to-me-3d122f96d955)が出ていますので、ぜひ読んでみてください。

この記事では、このソケットモードを試す方法を日本語で紹介したいと思います。

## ソケットモードを試す

### Slack アプリを設定する

まず https://api.slack.com/apps?new_app=1 にアクセスして新しい Slack アプリをつくります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/dfaa94f9-e429-fed2-fb47-39cb969faa64.png" width=500 />

#### ソケットモードを有効にする

アプリを作成すると、以下のようにソケットモード（Socket Mode）設定への導線がありますので **Settings** > **Socket Mode** へ遷移します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/c2d9987f-dddc-10e2-2622-8637f2272319.png" width=600 />
**Enable Socket Mode** をトグルして有効にします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/aa6b740b-adc9-4cc8-3f5a-969322e681cd.png" width=600 />
以下のようなモーダルが開きますので、トークンにソケットモード関連であることがわかるような名前をつけて **Generate** ボタンを押します。元々設定されている [`connections:write`](https://api.slack.com/scopes/connections:write) というスコープは、WebSocket 接続 URL の取得に必要となりますので、削除しないようにしてください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/5d5cc64d-cee1-d266-a473-8e29c51ed607.png" width=600 />
**Generate** を押すと、モーダルが切り替わって以下の画面のように `xapp-` で始まるトークンが表示されます。このトークンを後ほど **SLACK_APP_TOKEN** として使いますので、控えておいてください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/5bb06f3f-da67-71de-d9cb-9b2685ada1e8.png" width=600 />

これで、ソケットモード自体は有効になりました。それでは、インタラクティブな機能を有効にしていきましょう。

#### グローバルショートカットを設定する

まずはショートカットを追加してみましょう。**Features** > **Interactivity & Shortcuts** に遷移してください。この画面にくるとすでに **Interactivity** は **On** になっています。そして **Request URL** を設定する必要がない旨、表示されています。以下の **Create New Shortcut** ボタンからショートカットを設定しましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/a041b81a-7b48-50d6-dc9b-e7d91bfab83b.png" width=600 />
今回の例ではグローバルショートカットを使いますので、そのまま **Next** をクリックしてください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/e1e8fadf-daeb-45c8-7f10-af40e80b4668.png" width=600 />
**Name** や **Short Description** は、Slack ワークスペース内でエンドユーザーの目に触れますので、わかりやすい日本語のものを設定するとよいでしょう。**Callback ID** はアプリ側でハンドリングする時に使いますので、アルファベットのみで **socket-mode-shortcut** として **Create** ボタンを押してください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/54debb8e-8c51-4945-d36b-49fbe365cd23.png" width=600 />
元の画面に戻るとショートカットが追加されているはずです。これで完了です。画面下部の **Save Changes** ボタンを押してください
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/98e5aa39-fa6f-5dd5-5bd6-1f2058633550.png" width=600 />

#### イベント API を設定する

次にイベント API を設定します。こちらはデフォルトでは有効になっていませんので **On** にトグルします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/42a4d556-398a-d7c8-f498-8465584ddb98.png" width=600 />
サブスクライブするイベントはいろいろあるので、お好きなものを試してもらえればと思いますが、以下のコード例をそのまま動かすなら [`message.channels`](https://api.slack.com/events/message) イベントを指定してください。こちらも **Save Changes** ボタンを押すのも忘れずに。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/745d277d-bc41-e57b-4ce8-4d7522edf1ca.png" width=600 />

#### その他の設定

インストールする前にその他の設定をしておきます。

まず、チャンネルにメッセージを投稿するのはよくあるユースケースだと思いますので **Features** > **OAuth & Permissions** に移動して [`chat:write`](https://api.slack.com/scopes/chat:write) スコープを追加しておきましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/dadfb9eb-2012-e284-454e-b54433933e0b.png" width=600 />

ボットユーザーの設定を行います。 **Features** > **App Home** に移動して **App Display Name** のところの **Edit** ボタンから変更します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/e907cc65-508a-d986-e46e-1b7256e73db2.png" width=600 />
一つ目の項目がユーザーの目に触れます。二つ目はアルファベットでユニークな名前として指定しておいてください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/d8ecbcbf-e468-7295-6286-bc86f5530b7a.png" width=600 />

#### ワークスペースにインストールする

以上で設定完了です。ワークスペースにインストールしましょう。 **Settings** > **Install App** へ移動して **Install to Workspace** ボタンをクリックしてください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/25725494-94e1-d921-5704-bb93eed55b2e.png" width=600 />
このようにいつもの確認画面が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/982a3525-744c-de4d-1b57-2f078a4b14fa.png" width=500 />
インストールが完了すると `xoxb-` ではじまる **SLACK_BOT_TOKEN** が発行されていますので、この値を後ほどアプリで使用します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/8345c93e-2cb5-0a70-3ae0-95f474f3001a.png" width=600 />

これで Slack アプリの設定、ワークスペースでの有効化が完了しました。あとは Bolt アプリを実装して起動するだけです！

ちなみにアプリが動作すると以下のような感じになります（左はショートカットから起動したモーダル、右はイベント API を使ったキーワード反応）。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/37778dff-2a90-e091-520a-39620a939412.png" width="400" /><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/8ad3dfdf-29bb-0b6c-faa2-b8a49e347c32.png" width="400" />

### 各言語でのサンプル例

#### JavaScript で始める

12 以上のバージョンの Node.js があればすぐに試せます。[こちらの日本語ドキュメント](https://slack.dev/bolt-js/ja-jp/concepts#socket-mode)も合わせて参考にしてください。

```bash
npm init -y
npm i @slack/bolt@3
```

`index.js` というファイル名で以下のソースコードを保存します。

```javascript
const { App } = require('@slack/bolt');

const app = new App({
  logLevel: 'debug',  // これはログレベルの調整なので削除しても OK です
  socketMode: true,
  token: process.env.SLACK_BOT_TOKEN,
  appToken: process.env.SLACK_APP_TOKEN
});

// グローバルショートカット
app.shortcut("socket-mode-shortcut", async ({ ack, body, client }) => {
  await ack();
  await client.views.open({
    "trigger_id": body.trigger_id,
    "view": {
      "type": "modal",
      "callback_id": "modal-id",
      "title": {
        "type": "plain_text",
        "text": "タスク登録"
      },
      "submit": {
        "type": "plain_text",
        "text": "送信"
      },
      "close": {
        "type": "plain_text",
        "text": "キャンセル"
      },
      "blocks": [
        {
          "type": "input",
          "block_id": "input-task",
          "element": {
            "type": "plain_text_input",
            "action_id": "input",
            "multiline": true,
            "placeholder": {
              "type": "plain_text",
              "text": "タスクの詳細・期限などを書いてください"
            }
          },
          "label": {
            "type": "plain_text",
            "text": "タスク"
          }
        }
      ]
    }
  });
});

app.view("modal-id", async ({ ack, view, logger }) => {
  logger.info(`Submitted data: ${view.state.values}`);
  await ack();
});

// イベント API
app.message("こんにちは", async ({ message, say }) => {
  await say(`:wave: こんにちは <@${message.user}>！`);
});

(async () => {
  await app.start();
  console.log('⚡️ Bolt app started');
})();
```

`npx node index.js` で起動してみてください。特にエラーが出なければ OK です。

アプリの準備ができましたので、インストールした Slack ワークスペースで、ボットユーザーをテスト用のチャンネルに招待して message イベントの受信やグローバルショートカットの実行を試してみてください。


#### Python で始める

Python で始める場合は Python 3.6 以上の環境さえあれば、以下の手順で仮想環境をつくって、すぐにアプリを立ち上げることができます。

```bash
python3 -m venv .venv
source .venv/bin/activate
echo 'slack_bolt>=1.2' > requirements.txt
pip install -U pip
pip install -r requirements.txt
```

次に `app.py` というファイル名で以下のコードを保存します。

```python
import logging
import os
from slack_sdk import WebClient
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

logging.basicConfig(level=logging.DEBUG)

app = App(token=os.environ["SLACK_BOT_TOKEN"])

# イベント API
@app.message("こんにちは")
def handle_messge_evnts(message, say):
    say(f"こんにちは <@{message['user']}> さん！")

# ショートカットとモーダル
@app.shortcut("socket-mode-shortcut")
def handle_shortcut(ack, body: dict, client: WebClient):
    ack()
    client.views_open(
        trigger_id=body["trigger_id"],
        view={
            "type": "modal",
            "callback_id": "modal-id",
            "title": {"type": "plain_text", "text": "タスク登録"},
            "submit": {"type": "plain_text", "text": "送信"},
            "close": {"type": "plain_text", "text": "キャンセル"},
            "blocks": [
                {
                    "type": "input",
                    "block_id": "input-task",
                    "element": {
                        "type": "plain_text_input",
                        "action_id": "input",
                        "multiline": True,
                        "placeholder": {
                            "type": "plain_text",
                            "text": "タスクの詳細・期限などを書いてください",
                        },
                    },
                    "label": {"type": "plain_text", "text": "タスク"},
                }
            ],
        },
    )

@app.view("modal-id")
def handle_view_submission(ack, view, logger):
    logger.info(f"Submitted data: {view['state']['values']}")
    ack()

if __name__ == "__main__":
    handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
    handler.start()
```

あとはトークンを環境変数に設定して `python app.py` を実行するだけで起動できます。

```
export SLACK_APP_TOKEN=xapp-<自分のトークンの値>
export SLACK_BOT_TOKEN=xoxb-<自分のトークンの値>
python app.py
```

デバッグログが出力されたかと思いますが、やっていることを解説すると以下のようになります。

```
# SLACK_BOT_TOKEN のチェック
Sending a request - url: https://www.slack.com/api/auth.test, query_params: {}, body_params: {}, files: {}, json_body: {}, headers: { ... }
Received the following response - status: 200, headers: { ... }, body: {'ok': True, 'url': 'https://xxx.slack.com/', 'team': 'xxx', 'user': 'socket-san', 'team_id': 'T03E94MJU', 'user_id': 'U111', 'bot_id': 'B111', 'is_enterprise_install': False}

# SLACK_APP_TOKEN を使って WebSocket 接続 URL を取得
Sending a request - url: https://www.slack.com/api/apps.connections.open, query_params: {}, body_params: {}, files: {}, json_body: None, headers: { ... }
Received the following response - status: 200, headers: { ... }, body: {'ok': True, 'url': 'wss://wss-primary.slack.com/link/?ticket=xxx&app_id=yyy'}

# WebSocket 接続を確立
A new session has been established (session id: edf6378e-852e-4b36-8720-22e0b2952a76)
⚡️ Bolt app is running!

# 最初に hello メッセージを受信
Starting to receive messages from a new connection (session id: edf6378e-852e-4b36-8720-22e0b2952a76)
on_message invoked: (message: {"type":"hello","num_connections":1,"debug_info":{"host":"applink-canary-xxx-yyy","build_number":10,"approximate_connection_time":18060},"connection_info":{"app_id":"A111"}})
A new message enqueued (current queue size: 1)
A message dequeued (current queue size: 0)
Message processing started (type: hello, envelope_id: None)
Message processing completed (type: hello, envelope_id: None)
```

より詳細なログを出したい場合は `trace_enabled` や `all_message_trace_enabled` を `True` に指定してみてください。

```python
if __name__ == "__main__":
    handler = SocketModeHandler(
        app=app,
        app_token=os.environ["SLACK_APP_TOKEN"],
        trace_enabled=True,
    )
    handler.start()
```

アプリの準備ができましたので、インストールした Slack ワークスペースで、ボットユーザーをテスト用のチャンネルに招待して message イベントの受信やグローバルショートカットの実行を試してみてください。

なお、 [websocket_client](https://pypi.org/project/websocket_client/), [aiohttp](https://pypi.org/project/aiohttp/), [websockets](https://pypi.org/project/websockets/) というライブラリを標準でサポートしていますので、そちらを使う場合は、[こちらのドキュメント](https://slack.dev/bolt-python/concepts#socket-mode)を参考にしてください。

#### Java で始める

基本的には[このドキュメントがすでに日本語になっています](https://tools.slack.dev/java-slack-sdk/ja-jp/guides/getting-started-with-bolt-socket-mode/)ので、そちらをみていただければ十分なのですが、上記の例と同じものはこのようになります。

JDK のバージョンは 8 以上で利用可能ですが 11 などより新しいバージョンを推奨します。

ここではビルドツールに Gradle を使いましょう（Maven の方がよければそれももちろん可能です）。`build.gradle` を以下の内容で保存してください。

```gradle
plugins {
  id("application")
}
repositories {
  mavenCentral()
}
dependencies {
  implementation("com.slack.api:bolt-socket-mode:1.5.0")
  implementation("org.glassfish.tyrus.bundles:tyrus-standalone-client:1.17")
  implementation("org.slf4j:slf4j-simple:1.7.30")
}
application {
  mainClassName = "hello.MyApp"
}
```

ソースコードは以下の一つだけです。 `src/main/java/hello/MyApp.java` というファイル名で保存します。

```java
package hello;

import com.slack.api.bolt.App;
import com.slack.api.bolt.socket_mode.SocketModeApp;
import com.slack.api.model.event.MessageEvent;
import com.slack.api.model.view.View;

import static com.slack.api.model.block.Blocks.asBlocks;
import static com.slack.api.model.block.Blocks.input;
import static com.slack.api.model.block.composition.BlockCompositions.plainText;
import static com.slack.api.model.block.element.BlockElements.plainTextInput;
import static com.slack.api.model.view.Views.*;

public class MyApp {
  public static void main(String[] args) throws Exception {
    System.setProperty("org.slf4j.simpleLogger.log.com.slack.api", "debug");

    // SLACK_BOT_TOKEN という環境変数が設定されている前提
    App app = new App();

    // イベント API
    app.event(MessageEvent.class, (req, ctx) -> {
      ctx.say(":wave: こんにちは <@" + req.getEvent().getUser() + ">！");
      return ctx.ack();
    });

    // ショートカットとモーダル
    app.globalShortcut("socket-mode-shortcut", (req, ctx) -> {
      View modalView = view(v -> v
        .type("modal")
        .callbackId("modal-id")
        .title(viewTitle(title -> title.type("plain_text").text("タスク登録").emoji(true)))
        .submit(viewSubmit(submit -> submit.type("plain_text").text("送信").emoji(true)))
        .close(viewClose(close -> close.type("plain_text").text("キャンセル").emoji(true)))
        .blocks(asBlocks(input(i -> i
          .blockId("input-task")
          .element(plainTextInput(pti -> pti.actionId("input").multiline(true)))
          .label(plainText(pt -> pt.text("タスクの詳細・期限などを書いてください")))
        )))
      );
      ctx.asyncClient().viewsOpen(r -> r
        .triggerId(req.getPayload().getTriggerId())
        .view(modalView)
      );
      return ctx.ack();
    });
    app.viewSubmission("modal-id", (req, ctx) -> {
      ctx.logger.info("Submitted data: {}", req.getPayload().getView().getState().getValues());
      return ctx.ack();
    });

    // SLACK_APP_TOKEN という環境変数が設定されている前提
    new SocketModeApp(app).start();
  }
}
```

以下の手順で起動してみてください。接続して hello メッセージを受信できるはずです。

```bash
export SLACK_APP_TOKEN=xapp-<自分のトークンの値>
export SLACK_BOT_TOKEN=xoxb-<自分のトークンの値>
gradle run
```

アプリの準備ができましたので、インストールした Slack ワークスペースで、ボットユーザーをテスト用のチャンネルに招待して message イベントの受信やグローバルショートカットの実行を試してみてください。

なお、Java-WebSocket というライブラリも WebSocket クライアントの実装としてサポートしていますので、そちらを使いたい場合は、切り替え手順を[こちら](https://slack.dev/java-slack-sdk/guides/ja/socket-mode)で確認してください。

#### Kotlin で始める

上の Java とほぼ同様ですが、Kotlin のビルド設定です。

```gradle
plugins {
  id("org.jetbrains.kotlin.jvm") version "1.4.21-2"
  id("application")
}
repositories {
  mavenCentral()
}
dependencies {
  implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
  implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
  implementation("com.slack.api:bolt-socket-mode:1.5.0")
  implementation("com.slack.api:slack-api-model-kotlin-extension:1.5.0")
  implementation("org.glassfish.tyrus.bundles:tyrus-standalone-client:1.17")
  implementation("org.slf4j:slf4j-simple:1.7.30") // または logback-classic など
}
application {
  mainClassName = "MyAppKt" // ソースファイル名の末尾、拡張子の代わりに "Kt" をつけた命名になります
}
```

ソースコードは `src/main/kotlin/MyApp.kt` として以下の内容で保存します。

```kotlin
import com.slack.api.bolt.App
import com.slack.api.bolt.socket_mode.SocketModeApp
import com.slack.api.model.event.MessageEvent
import com.slack.api.model.kotlin_extension.view.blocks
import com.slack.api.model.view.Views.*

fun main() {
  System.setProperty("org.slf4j.simpleLogger.log.com.slack.api", "debug")

  // SLACK_BOT_TOKEN という環境変数が設定されている前提
  val app = App()

  // イベント API
  app.event(MessageEvent::class.java) { req, ctx ->
    ctx.say("こんにちは <@" + req.event.user + ">！")
    ctx.ack()
  }

  // ショートカットとモーダル
  app.globalShortcut("socket-mode-shortcut") { req, ctx ->
    val modalView = view { v -> v
      .type("modal")
      .callbackId("modal-id")
      .title(viewTitle { it.type("plain_text").text("タスク登録") })
      .submit(viewSubmit { it.type("plain_text").text("送信") })
      .close(viewClose { it.type("plain_text").text("キャンセル") })
      .blocks {
        input {
          blockId("input-task")
          element {
            plainTextInput {
              actionId("input")
              multiline(true)
            }
          }
          label("タスクの詳細・期限などを書いてください")
        }
      }
    }
    ctx.asyncClient().viewsOpen { it.triggerId(req.payload.triggerId).view(modalView) }
    ctx.ack()
  }
  app.viewSubmission("modal-id") { req, ctx ->
    ctx.logger.info("Submitted data: {}", req.payload.view.state.values)
    ctx.ack()
  }

  // SLACK_APP_TOKEN という環境変数が設定されている前提
  SocketModeApp(app).start()
}
```

こちらも同様に `gradle run` で接続できるはずです。

```bash
export SLACK_APP_TOKEN=xapp-<自分のトークンの値>
export SLACK_BOT_TOKEN=xoxb-<自分のトークンの値>
gradle run
```

アプリの準備ができましたので、インストールした Slack ワークスペースで、ボットユーザーをテスト用のチャンネルに招待して message イベントの受信やグローバルショートカットの実行を試してみてください。


#### Go ではじめる

[Slack ソケットモードの最も簡単な始め方 (Go 編)](https://qiita.com/seratch/items/c7d9aeb60ead5c126c01) を参考にしてみてください。

## 次のステップ

ある程度動くようになったら、デプロイをどうするかを検討してみてください。

WebSocket で接続するアプリですので FaaS (Function as a Service) のようなソリューションはマッチしないでしょう。Docker イメージを作って ECS や Lightsail Containers などにデプロイしたり、アプリのホスティングサービスで常駐プロセスとして起動するのがよいと思います。

ソケットモードは、企業内でのユースケースでいうと、公開エンドポイントを用意することが必要なくなったことが最も嬉しい点ですが、ソケットモードの登場によって、個人的にちょっと試してみるときにも Slack アプリ開発はかつてのようにとても手軽になったと思います。

2、3 年前と比べると Slack プラットフォームは、ブロックキット、ショートカット、ホームタブなど多くの新機能が追加されています。この機会にぜひ試してみてください :wave: 

## ドキュメント

関連するドキュメントのリンクです。

日本語の情報は以下を参考にしてください。

* [Bolt for JavaScript での利用方法](https://slack.dev/bolt-js/ja-jp/concepts#socket-mode)
* [Bolt for Java でのソケットモード入門](https://slack.dev/java-slack-sdk/guides/ja/getting-started-with-bolt-socket-mode)
* [Bolt for Java でのソケットモード概要](https://slack.dev/java-slack-sdk/guides/ja/socket-mode)

以下はこの記事投稿時点で英語のみとなります。

* [Socket to me (Slack Platform Blog)](https://medium.com/slack-developer-blog/socket-to-me-3d122f96d955)
* [ソケットモードの導入](https://api.slack.com/apis/connections/socket)
* [ソケットモードクライアントの実装方法](https://api.slack.com/apis/connections/socket-implement)
* [Node Slack SDK のクライアント](https://slack.dev/node-slack-sdk/socket-mode)
* [Python Slack SDK のクライアント](https://slack.dev/python-slack-sdk/socket-mode/)
* [Bolt for Python での利用方法](https://slack.dev/bolt-python/concepts#socket-mode)