Slack 次世代プラットフォーム機能を少しずつ試す - 初めてのカスタムファンクション編

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

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

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

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

前回の記事では、標準ファンクションだけを使って最もシンプルな Hello Wolrd を実装してみました。

https://qiita.com/seratch/items/8c91bb367bb942dc0ffe

今回の記事では、自分でファンクションを実装してみましょう。なお、次世代プラットフォーム機能を利用する上での前提条件は[こちら](https://qiita.com/seratch/items/8c91bb367bb942dc0ffe#%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6)をご確認ください。

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

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

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

前回と同様、シンプルな変更を加えながらファンクションの実装方法を学んでいきましょう。

### ファンクション `my_send_message.ts` を作成

以下の内容で `my_send_message.ts` という新しいファイルを作成してください。これは、標準ファンクションの SendMessage と同じ inputs を受けとって Slack チャンネルにメッセージを投稿する自前のファンクションです。

```typescript
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";

// DefineFunction でファンクションのメタデータを定義
export const def = DefineFunction({
  callback_id: "my-send-message",
  title: "My SendMessage",
  // プロジェクトのルートディレクトリからのパスを指定する必要がある
  // functions/ 配下に配置するときは functions/foo.ts のようになる
  source_file: "my_send_message.ts",
  // inputs の名前と型を一覧で定義、必須のものは required の配列に列挙
  // ファンクションのコードの中で inputs.channel_id のように型安全にアクセスできるようになる
  input_parameters: {
    properties: {
      channel_id: { type: Schema.slack.types.channel_id },
      message: { type: Schema.types.string },
    },
    required: ["channel_id", "message"],
  },
  // このファンクションが返すべき outputs を定義
  // required のものがファンクションコードから返されていない場合
  // コンパイルがエラーとなる
  output_parameters: {
    properties: { ts: { type: Schema.types.string } },
    required: ["ts"],
  },
});

// SlackAPI というユーティリティを使って Slack API クライアントを作成する
// 詳しくは https://api.slack.com/future/apicalls を参照
import { SlackAPI } from "deno-slack-api/mod.ts";

// SlackFunction の定義を export default することで
// このファンクションのハンドラーを有効化し、それをワークフロー側で import できるようにする
export default SlackFunction(def, async ({
  // これらがサポートされているすべての引数
  inputs, // input_parameters に定義されている値
  env, // slack env コマンドで事前に登録された値（SLACK_API_URL は組み込みの値）
  team_id, // ワークスペースの ID（必ず存在する）
  enterprise_id, // Enterprise Grid の場合の OrG の ID（Grid でない場合は空文字となる）
  token, // Slack API 呼び出しに使える bot token
}) => {
  // 標準出力に token 以外を書き出してみている例
  const loggingOutput = {
    inputs,
    env,
    team_id,
    enterprise_id,
  };
  console.log(JSON.stringify(loggingOutput, null, 2));

  // Slack API を初期化
  const client = SlackAPI(token);
  // 自前で chat.postMessage API を呼び出して、指定されたチャンネルにメッセージ投稿
  const newMessageResponse = await client.chat.postMessage({
    channel: inputs.channel_id,
    text: inputs.message,
  });
  console.log(
    `chat.postMessage response: ${JSON.stringify(newMessageResponse, null, 2)}`,
  );
  // 投稿したメッセージの ts を outputs に設定して処理を終了
  const ts = newMessageResponse.ts;
  return { outputs: { ts } };
});
```

### ワークフローとトリガーを `hello_world.ts` に定義する

前回の記事と同様、ワークフローとトリガーを `./hello_world.ts` に定義します。後半で紹介したリンクトリガーの例をそのまま持ってきて、`workflow.addStep` のところだけを書き換えます。

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

// この定義オブジェクトを manifest.ts で参照するのを忘れずに！
export const workflow = DefineWorkflow({
  callback_id: "hello-world-workflow",
  title: "Hello World Workflow",
  input_parameters: {
    properties: {
      // リンクトリガーから受け取るチャンネル ID
      channel_id: { type: Schema.slack.types.channel_id },
    },
    required: ["channel_id"],
  },
});

// 前回の記事で見た標準ファンクションを使った例
// workflow.addStep(Schema.slack.functions.SendMessage, {
//  channel_id: workflow.inputs.channel_id,
//  message: "Hello World!",
//});

// 自前の ./my_send_message.ts を import してきて、それを使ってメッセージを投稿
import { def as MySendMessage } from "./my_send_message.ts";
workflow.addStep(MySendMessage, {
  // ワークフロー全体のトリガーからの入力を指定
  // リンクトリガーの場合はそれを起動したチャンネルの ID が渡される
  channel_id: workflow.inputs.channel_id,
  message: "Hello World!",
});

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

// リンクトリガー
const trigger: Trigger<typeof workflow.definition> = {
  type: "shortcut",
  name: "Hello World Trigger",
  workflow: `#/workflows/${workflow.definition.callback_id}`,
  inputs: {
    // クリックしたチャンネルの ID が設定される
    channel_id: { value: "{{data.channel_id}}" },
  },
};

// トリガーの作成には `slack triggers create --trigger-def [ファイルパス]` を実行する
// Trigger 形の定義オブジェクトを export default さえしていれば
// そのソースファイルを使用できる
export default trigger;
```

そのまま貼り付ければ、コンパイルエラーは発生しないはずです。

### 定義したワークフローを `manifest.ts` に追加する

この手順は前回の記事と同様です。HelloWorld ワークフローを `manifest.ts` に追加しましょう。

```typescript
import { Manifest } from "deno-slack-sdk/mod.ts";
import { def as MySendMessage } from "./my_send_message.ts";
import { workflow as HelloWorld } from "./hello_world.ts";

export default Manifest({
  name: "affectionate-panther-654",
  description: "Hello World",
  icon: "assets/default_new_app_icon.png",
  functions: [MySendMessage],
  workflows: [HelloWorld],
  outgoingDomains: [],
  botScopes: ["commands", "chat:write", "chat:write.public"],
});
```

また、今回は新しくファンクションも作成しましたので、将来再利用できるよう `functions` にも追加してみました。

ただし、この記事投稿時点（2022 年 12 月）では、コードで定義したワークフローの中でのみファンクションを利用できますので、実際にはこのファンクション単体ではまだ使い道がありません。

今後、2023 年には次世代プラットフォームに対応した新しいワークフロービルダーがベータ版としてリリースされる予定となっています。このようにマニフェストに含めてあらかじめデプロイしておいたファンクションを、同じワークスペース内の他のワークフローでも利用することができるようになります。詳しくは[こちらのブログ記事](https://slack.com/intl/ja-jp/blog/productivity/efficiency-workflow-builder)もぜひお読みください。

これが利用できるようになれば、ただ必要なファンクションを実装するだけで、あとは他のエンドユーザーに自分でワークフローを設定してもらうことができるようになります。ぜひご期待ください！

#### `slack run` でアプリを起動して開発版アプリの設定を反映

さて、少し話がそれましたが、、ここまででコードの準備は完了しています。

`slack run` でアプリを起動してみてください。そうすると、自動的に開発版アプリが Slack ワークスペースにインストールされ、`manifest.ts` に追加しておいたワークフローやファンクションの設定が有効になります。

`slack run` はこのまま立ち上げっぱなしで問題ありません。その場合は、次のコマンドを実行するために別のターミナルウィンドウを開いてください。

### `slack triggers create` でリンクトリガーを作成してチャンネルに追加

このワークフローを実行するには何らかのトリガーが必要です。以下のコマンドを実行してリンクトリガーを作成しましょう。

```bash
slack triggers create --trigger-def hello_world.ts
```

生成された `https://slack.com/shortcuts/Ft04DEBXXXXX/YYYY` という URL をワークフローを起動したいチャンネルにメッセージで投稿するか、チャンネルのブックマークに追加します。

これで、すべての準備が完了です。ボタンをクリックすると、前回と同様「Hello World!」というメッセージが投稿されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/c3e7d611-8f25-1147-a62e-0300314d769d.png" width=500 />

標準出力には以下のような出力が表示されます。

```bash
$ slack run
? Choose a workspace  seratch  T03E94MJU
   affectionate-panther-654 A04DLQR53EZ

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
{
  "inputs": {
    "channel_id": "C03E94MKS",
    "message": "Hello World!"
  },
  "env": {
    "SLACK_API_URL": "https://slack.com/api/"
  },
  "team_id": "T03E94MJU",
  "enterprise_id": ""
}
chat.postMessage response: {
  "ok": true,
  "channel": "C03E94MKS",
  "ts": "1670220375.097869",
  "message": {
    "bot_id": "B04DPP52M26",
    "type": "message",
    "text": "Hello World!",
    "user": "U04DPL2MMFV",
    "ts": "1670220375.097869",
    "app_id": "A04DLQR53EZ",
    "team": "T03E94MJU",
    "bot_profile": {
      "id": "B04DPP52M26",
      "app_id": "A04DLQR53EZ",
      "name": "affectionate-panther-654 (dev)",
      "icons": {
        "image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
        "image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
        "image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
      },
      "deleted": false,
      "updated": 1670217238,
      "team_id": "T03E94MJU"
    }
  }
}
2022-12-05 15:06:14 [info] [Fn04DPL2M6AF] (Trace=Tr04D9BA2RSB) Function execution started for workflow function 'Hello World Workflow'
2022-12-05 15:06:14 [info] [Wf04E2CHG57B] (Trace=Tr04DLUURHEH) Executing workflow step 1 of 1
2022-12-05 15:06:14 [info] [Fn04DPP527FU] (Trace=Tr04DLUURHEH) Function execution started for app function 'My SendMessage'
2022-12-05 15:06:15 [info] [Fn04DPP527FU] (Trace=Tr04DLUURHEH) Function execution completed for function 'My SendMessage'
2022-12-05 15:06:16 [info] [Wf04E2CHG57B] (Trace=Tr04DLUURHEH) Execution completed for workflow step 'My SendMessage'
2022-12-05 15:06:17 [info] [Fn04DPL2M6AF] (Trace=Tr04D9BA2RSB) Function execution completed for function 'Hello World Workflow'
2022-12-05 15:06:17 [info] [Wf04E2CHG57B] (Trace=Tr04DLUURHEH) Execution completed for workflow 'Hello World Workflow'
```

ワークフローの開始のログよりもファンクションの中に埋めた `console.log` が先に出力されているのを不思議に思われたかもしれません。これは確かに紛らわしいのですが、時系列としてはもちろんワークフローが先に開始されてから手元のファンクションが実行されています。順序が入れ替わっている理由は、手元の `console.log` は即時で出力されるのに対して、サーバー側のログが取得されて手元の標準出力に出力されるまでに若干のラグがあるためです。

次世代プラットフォームの CLI や SDK は現在アクティブに改善が続けられていますので、正式リリースまでに何らかの改善をお知らせできるかもしれません。

#### メッセージ送信者のアイコンの問題

なお、標準の SendMessage では `manifest.ts` で指定したアイコンイメージがメッセージ投稿者に使われているのに対して、今回のように `chat.postMessage` API を自前で実行したときにはそれが使われてないことに気づかれた方もいらっしゃるかもしれません（私自身もこの記事を書いていて気づいたのですが・・）。

社内で確認したところ、これは**開発版アプリでのみ発生している既知の不具合でした**。`slack deploy` でデプロイした本番アプリの方では発生しません。開発版であってもこの問題を回避したいという場合 `manifest.ts` の `botScopes` に `chat:write.customize` も追加した上で `chat.postMessage` のパラメーターに [icon_url](https://api.slack.com/methods/chat.postMessage#arg_icon_url) を指定することができます。

### 終わりに

今回の記事では、ファンクションの開発手順と Slack API の利用方法を解説しました。次回の記事では、Slack API 以外の API の利用とその注意点などを解説していきたいと思います。

それでは！