こんにちは、Slack の公式 SDK 開発と日本の Developer Relations を担当している瀬良 (@seratch) と申します
この記事は Slack の次世代プラットフォーム機能を少しずつ試しながら、ゆっくりと理解していくシリーズの記事です。
「次世代プラットフォーム機能って何?」という方は、以下の記事で詳しく解説しましたので、まずはそちらをお読みください。
前回の記事では、標準ファンクションだけを使って最もシンプルな Hello Wolrd を実装してみました。
今回の記事では、自分でファンクションを実装してみましょう。なお、次世代プラットフォーム機能を利用する上での前提条件はこちらをご確認ください。
ブランクプロジェクトを作成
前回の記事と同様、ブランクプロジェクトを作成してゼロからコードを足していきましょう。slack create
コマンドを実行して、選択肢から「Blank Project」を選択してください。作成したプロジェクトの構成は以下の通りです。
$ 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 チャンネルにメッセージを投稿する自前のファンクションです。
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
のところだけを書き換えます。
// -------------------------
// ワークフロー定義
// -------------------------
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
に追加しましょう。
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 年には次世代プラットフォームに対応した新しいワークフロービルダーがベータ版としてリリースされる予定となっています。このようにマニフェストに含めてあらかじめデプロイしておいたファンクションを、同じワークスペース内の他のワークフローでも利用することができるようになります。詳しくはこちらのブログ記事もぜひお読みください。
これが利用できるようになれば、ただ必要なファンクションを実装するだけで、あとは他のエンドユーザーに自分でワークフローを設定してもらうことができるようになります。ぜひご期待ください!
slack run
でアプリを起動して開発版アプリの設定を反映
さて、少し話がそれましたが、、ここまででコードの準備は完了しています。
slack run
でアプリを起動してみてください。そうすると、自動的に開発版アプリが Slack ワークスペースにインストールされ、manifest.ts
に追加しておいたワークフローやファンクションの設定が有効になります。
slack run
はこのまま立ち上げっぱなしで問題ありません。その場合は、次のコマンドを実行するために別のターミナルウィンドウを開いてください。
slack triggers create
でリンクトリガーを作成してチャンネルに追加
このワークフローを実行するには何らかのトリガーが必要です。以下のコマンドを実行してリンクトリガーを作成しましょう。
slack triggers create --trigger-def hello_world.ts
生成された https://slack.com/shortcuts/Ft04DEBXXXXX/YYYY
という URL をワークフローを起動したいチャンネルにメッセージで投稿するか、チャンネルのブックマークに追加します。
これで、すべての準備が完了です。ボタンをクリックすると、前回と同様「Hello World!」というメッセージが投稿されます。
標準出力には以下のような出力が表示されます。
$ 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 を指定することができます。
終わりに
今回の記事では、ファンクションの開発手順と Slack API の利用方法を解説しました。次回の記事では、Slack API 以外の API の利用とその注意点などを解説していきたいと思います。
それでは!