Slack 次世代プラットフォーム機能を少しずつ試す - reaction_added イベントトリガー編

こんにちは、Slack の[公式 SDK 開発](https://github.com/slackapi)と日本の Developer Relations を担当している瀬良 (@seratch) と申します :wave:

この記事は [Slack の次世代プラットフォーム機能を少しずつ試しながら、ゆっくりと理解していくシリーズ](https://qiita.com/seratch/items/b1449132add003e61de6)の記事です。

「次世代プラットフォーム機能って何？」という方は、以下の記事で詳しく解説しましたので、まずはそちらをお読みください。

https://qiita.com/seratch/items/ecc16b845483c9d6f9e1

この記事では、[イベントトリガー（Event Trigger）](https://api.slack.com/future/triggers/event)の使い方をご紹介します。イベントトリガーは様々な種類がありますが、今回はチャンネル内のメッセージに誰かがリアクションをつけたときにイベントが発生する "reaction_added" イベントを使ってワークフローを起動してみましょう。

### ブランクプロジェクトを作成

いつものようにブランクプロジェクトを作成してゼロからコードを足していきましょう。`slack create` コマンドを実行して、選択肢から「Blank Project」を選択してください。作成したプロジェクトの構成は以下の通りです。

```bash
$ tree
.
├── LICENSE
├── README.md
├── assets
│   └── default_new_app_icon.png
├── deno.jsonc
├── import_map.json
├── manifest.ts
└── slack.json
```

今回はトリガーがメインの記事ですので、ワークフローは非常にシンプルなものにしましょう。

### ソースコードでトリガーを作る

以下の内容を `example.ts` として保存してください。

```typescript
// -------------------------
// ワークフロー定義
// -------------------------
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";

export const workflow = DefineWorkflow({
  callback_id: "example-workflow",
  title: "Example Workflow",
  input_parameters: {
    properties: {
      channel_id: { type: Schema.slack.types.channel_id },
      user_id: { type: Schema.slack.types.user_id },
      message_ts: { type: Schema.types.string },
      reaction: { type: Schema.types.string },
    },
    required: ["channel_id", "user_id", "message_ts", "reaction"],
  },
});

// -------------------------
// トリガー定義
// -------------------------
import { Trigger } from "deno-slack-api/types.ts";

const trigger: Trigger<typeof workflow.definition> = {
  type: "event",
  name: "Trigger the example workflow",
  workflow: `#/workflows/${workflow.definition.callback_id}`,
  event: {
    event_type: "slack#/events/reaction_added",
    channel_ids: ["C04DPBYUQUC"], // TODO: ここを書き換え
    // 特定のリアクションのときだけワークフローを実行するよう条件を指定
    filter: {
      version: 1,
      root: { statement: "{{data.reaction}} == eyes" },
    },
  },
  inputs: {
    channel_id: { value: "{{data.channel_id}}" },
    user_id: { value: "{{data.user_id}}" },
    message_ts: { value: "{{data.message_ts}}" },
    reaction: { value: "{{data.reaction}}" },
  },
};

export default trigger;
```

そして `manifest.ts` にこのワークフローを追加します。

```typescript
import { Manifest } from "deno-slack-sdk/mod.ts";
import { workflow as ExampleWorkflow } from "./example.ts";

export default Manifest({
  name: "distracted-bison-253",
  description: "Example workflow",
  icon: "assets/default_new_app_icon.png",
  workflows: [ExampleWorkflow],
  outgoingDomains: [],
  botScopes: [
    "commands", // 最低限一つの bot scope が必要
    "reactions:read", // reaction_added トリガーに必要
  ],
});
```

コードの中で `TODO: ここを書き換え` とコメントがついた行は変更が必要です。このワークフローを有効にしたいチャンネルを一つ選んで、UI 上のチャンネル名をクリックして開いたメニューから `C` はじまりのチャンネル ID をコピーして、それで書き換えてください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/9213c801-f18b-c626-3567-cc534ac2fff1.png" width=500 />

なお、この記事執筆時点でこの次世代プラットフォームはまだベータ版の段階で、イベントトリガーはパブリックチャンネルしかサポートしていません。正式リリース前にこの記事を読まれている方は、パブリックチャンネルでお試しください。

ということで、ワークフロー実行の準備が完了しました。

`slack triggers create --trigger-def ./example.ts` を実行します。選択肢から `(dev)` という接尾辞がついている方を選択してください。

```bash
$ slack triggers create --trigger-def ./example.ts
? Choose an app  seratch (dev)  T03E*****
   distracted-bison-253 (dev) A04FNE*****

⚡ Trigger created
   Trigger ID:   Ft04EJ8*****
   Trigger Type: event
   Trigger Name: Trigger the example workflow
```

トリガーが作成されたら、早速 `slack run` を実行してアプリがイベントを受けられるようにしましょう。そして、指定したチャンネルのメッセージのどれかに `:eyes:` (:eyes:) のリアクションをつけてみてください。以下のように Example Workflow の実行ログが出力されれば成功です。

```bash
$ slack run
? Choose a workspace  seratch  T03E94MJU
   distracted-bison-253 A04FACHPQ5R

Updating dev app install for workspace "Acme Corp"

⚠️  Outgoing domains
   No allowed outgoing domains are configured
   If your function makes network requests, you will need to allow the outgoing domains
   Learn more about upcoming changes to outgoing domains: https://api.slack.com/future/changelog
✨  seratch of Acme Corp
Connected, awaiting events

2022-12-15 10:03:50 [info] [Fn04FCVD67J8] (Trace=Tr04G077TW80) Function execution started for workflow function 'Example Workflow'
2022-12-15 10:03:50 [info] [Wf04FP576X3K] (Trace=Tr04FP5F70HX) Execution started for workflow 'Example Workflow'
2022-12-15 10:03:51 [info] [Fn04FCVD67J8] (Trace=Tr04G077TW80) Function execution completed for function 'Example Workflow'
2022-12-15 10:03:51 [info] [Wf04FP576X3K] (Trace=Tr04FP5F70HX) Execution completed for workflow 'Example Workflow'
```

`:wave:` (:wave:) など別のリアクションもつけてみてください。そちらに対しては何も反応しないはずです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/d8c21984-6163-d15c-e868-b09a1337af3b.png" width=500 />

とりあえずワークフローの起動まではできました。

次は、組み込みファンクションを実行して返信をしてみましょう。 `manifest.ts` の `botScopes` を以下のように書き換えてください。

```typescript
  botScopes: [
    "commands", // 最低限一つの bot scope が必要
    "reactions:read", // reaction_added トリガーに必要
    "chat:write", // メッセージを投稿するための基本的な権限
    "chat:write.public", // public channel に参加することなくメッセージを投稿する権限
  ],
```

そして `example.ts` のワークフロー定義の後に以下のコードを追加してください。

```typescript
workflow.addStep(Schema.slack.functions.SendMessage, {
  channel_id: workflow.inputs.channel_id,
  message: `Hi <@${workflow.inputs.user_id}>!`,
});
```

これでまたメッセージに :eyes: リアクションをつけてみてください。リアクションをつけた人をメンションする形で以下のように返信が投稿されるはずです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/f4392cb2-58b2-3c9d-aecb-878b39438cca.png" width=400 />

`slack run` のコンソールの方には、以下のようにログが出力されます。

```bash
$ slack run
? Choose a workspace  seratch  T03E94MJU
   distracted-bison-253 A04FACHPQ5R

Updating dev app install for workspace "Acme Corp"

⚠️  Outgoing domains
   No allowed outgoing domains are configured
   If your function makes network requests, you will need to allow the outgoing domains
   Learn more about upcoming changes to outgoing domains: https://api.slack.com/future/changelog
✨  seratch of Acme Corp
Connected, awaiting events

2022-12-15 10:15:32 [info] [Fn04FCVD67J8] (Trace=Tr04F3UY8KT8) Function execution started for workflow function 'Example Workflow'
2022-12-15 10:15:32 [info] [Wf04FP576X3K] (Trace=Tr04FADT098T) Execution started for workflow 'Example Workflow'
2022-12-15 10:15:33 [info] [Wf04FP576X3K] (Trace=Tr04FADT098T) Executing workflow step 1 of 1
2022-12-15 10:15:33 [info] [Fn0102] (Trace=Tr04FADT098T) Function execution started for builtin function 'Send a message'
2022-12-15 10:15:34 [info] [Fn0102] (Trace=Tr04FADT098T) Function execution completed for function 'Send a message'
2022-12-15 10:15:34 [info] [Wf04FP576X3K] (Trace=Tr04FADT098T) Execution completed for workflow step 'Send a message'
2022-12-15 10:15:35 [info] [Fn04FCVD67J8] (Trace=Tr04F3UY8KT8) Function execution completed for function 'Example Workflow'
2022-12-15 10:15:35 [info] [Wf04FP576X3K] (Trace=Tr04FADT098T) Execution completed for workflow 'Example Workflow'
```

たったこれだけのコードで、リアクションをつけたら返信をしてくれるシンプルなアプリを作ることができました。

### より高度なワークフローを実装するために

より高度なことをしたい場合、もう少し工夫が必要となりますので、簡単にいくつかのポイントを解説しておきます。

#### 他のチャンネルでも有効にしたい

このトリガーのソースコードにはチャンネル ID の配列をハードコードしなければなりません。ですので、他のチャンネルを追加するには、**そのソースコードを書き換えた上で　`slack triggers create` でトリガーを作り直さなければなりません**。

トリガーの作り直しの手順です。

* `slack triggers list` で削除対象のトリガーの ID を取得
* `slack triggers delete --trigger-id {ここにトリガーの ID}` を実行
* 更新したチャンネル ID のリストを含むソースコードを使って `slack triggers create --trigger-def ./example.ts` を実行

チャンネル ID 以外のメタデータを書き換えた場合でも、同様に再作成するまではその内容は反映されないのでご注意ください。

・・・ただ、正直なところ、大量のチャンネルを指定したり、頻繁に変更が必要な場合、このやり方はちょっと厳しいものがありますよね。

そのような場合には、**トリガーを作成するための別の管理用のワークフロー**をつくることをおすすめします。先日私が公開した DeepL API を使った公式のサンプルアプリでこのテクニックを利用しています。

https://qiita.com/seratch/items/23e22b4eed489cdd04d7

このトピックについては、別途記事を書く予定ですが、すぐに内容をお知りになりたい方は `configurator` という名前のついた[こちらのワークフロー](https://github.com/slack-samples/deno-message-translator/blob/main/workflows/configurator.ts)の処理を読んでみてください。

#### チャンネル内のより詳細な情報を取得する

リアクションがついたメッセージのデータをすべて取得するためには、このアプリの bot user がそのチャンネルに [`conversations.join` API](https://api.slack.com/methods/conversations.join) を使って参加した上で、[`conversations.history` API](https://api.slack.com/methods/conversations.history) を実行して情報を取得する必要があります。

これらは、この記事投稿時点で標準のファンクションではできませんので、`botScopes` にさらなる権限の追加と、自前のファンクション実装が必要です。また、将来、標準のファンクションがこれらの操作に対応した場合にも `manifest.ts` に必要な `botScopes` を追加する必要があります。

ちなみにこの次世代プラットフォーム以前から存在するイベント API（Events API）を利用されたことがある方は、

**「あれ？？？ ボットユーザーをそのチャンネルに参加させていなくてもイベントが送信されてくるの？？？」**

と驚かれたかもしれません。

はい、次世代プラットフォームの（チャンネルに対して設定される）イベントトリガーの仕様は、この点において従来のイベント API とは大きく異なります。

**トリガーに設定されたチャンネルからのイベントは、アプリのボットがチャンネルのメンバーかどうかには関わらず、ワークフローの実行の開始に使われるのです**。

ですので、イベント API のときのアプリと同様の挙動を実現したい場合、この違いを意識しておく必要があります。

### 終わりに

いかがだったでしょうか？イベントトリガーは、いかにも Slack らしい連携アプリを実装するためには必須となるトリガーであると思います。

基本的には既存のイベント API とほぼ同様なのですが、上で述べたチャンネルのメンバーシップに関するイベント API との仕様の違いだけでなく、この記事投稿時点で認識されている既知の不具合などもあります。その辺もカバーできるよう、他の代表的なイベントトリガーについてもそれぞれ記事にして解説をしていきます。

現在利用可能なイベントトリガーの一覧については以下のページをご参照ください。

https://api.slack.com/future/triggers/event

それでは！