JMDC TECH BLOG

JMDCのエンジニアブログです

細かすぎて伝わらない Slack 次世代プラットフォームよもやま話選手権

みなさんこんにちは! プロダクト開発部分析システムグループの新保です。

以前からクローズドベータで公開されていた、Slackアプリを作るための新しいプラットフォーム……いわゆる「次世代プラットフォーム」が9月下旬にオープンベータになりました。

ちょうどチーム内の勤怠を管理するためのSlackアプリを作ろうとしていたところだったので、この次世代プラットフォームを全面採用し、色々使い込んでみました。この記事ではその開発過程でぶつかった様々な問題を書いてみようと思います。

ちなみに完成したアプリはこちら。出退勤を管理するためのメッセージを毎朝送ってくれる、雄鶏の Rooster くんです。

Roosterのメッセージ (黒塗りの部分にはメンバーのアイコンと名前が並んでいます)

注意

Slack次世代プラットフォームはオープンベータ版です。この記事はバージョン1.14.0をベースにしています。 最新の情報は公式のChangelogを参照してください。

api.slack.com

ユーザートークンが使えない

今回ハマった一番大きい落とし穴がこれでした。本当は勤怠入力に連動してユーザーのステータスやログイン状況を更新したかったのですが、これらのAPIを叩くにはユーザーの認証を得て得られる「ユーザートークン」と呼ばれる種類のトークンが必要なのだそうです。 しかし現状、次世代プラットフォームではこの種類のトークンを発行するフローは、どうやら存在しません。

あちこち情報を探し回った結果、Slack PlatformのTwitterで「今のところユーザートークンを使えるようにする予定はない」という証言を探し出しました。

そもそもSlackのトークンには「BOTトークン」「ユーザートークン」「アプリレベルトークン」などのさまざまなトークンが乱立しているそうなのですが、次世代プラットフォームで作ったアプリで使えるトークンはこれらとも異なるまた別のトークンを発行しているようです。

// トークンのプレフィックスの比較

//  BOTトークン
xoxb-...

// ユーザートークン
xoxp-...

// アプリレベルトークン
xapp-...

// 次世代プラットフォームで発行されたトークン
xwfp-...

「ユーザートークンを使えるようにする予定はない」というのが、「将来的に対応する気はあるが具体的な予定が立っていない」という意味なのか、「トークンシステムを作り直そうとしているので現状のユーザートークンに対応する予定はない」なのかわかりませんが、とりあえず今のところはBOTトークン相当の機能しか使えないという認識でよさそうです。

データストアは便利だけど、操作はまだ貧弱

次世代プラットフォームには Datastoreという、簡易的なデータベースがビルトインされています。やはりちょっと機能を作り込んでいくとデータベースは欲しくなるものだと思うので、これはとてもありがたいですね。

基本的なCRUD操作には対応済みのようで、特に query メソッドは DynamoDBの query メソッドと同様のシンタックスが使えるなど、なかなか便利に使えそうです。

ただ、値の更新に関してはDynamoDBでいうと putItem 相当の処理しか行えず、一部の項目のみを指定して更新する updateItem に対応する処理が行えないようです。(あったらぜひ教えてください!)

このあたり、そういった処理をしたければ他のサービスを使ってね的なスタンスなのか、まだベータ版だから実装されてないのかがわからなくて微妙なところですね。ひとまず現状では、自分でマージしてUPDATEする、Datastoreを分割するといった方法で対策するしかなさそうです。

タイムゾーン対応は諸々注意

次世代プラットフォームの最も大きな特徴は「Slackインフラ上で動く」ことでしょう。(日本語だとこのエコシステムを指すフレーズは「次世代プラットフォーム」が多いですが、英語圏だと “Run on Slack” がよく使われているようです)

さて、そうなると浮上するのがタイムゾーンの問題です。タイムゾーンを意識せずに開発を進めた場合、開発環境 (=ローカルPC) ではJSTで動いていたのに、本番環境 (=Slackインフラ) に持っていった途端UTCで動作することになり、予期せぬバグが起こる可能性があります。

日頃からグローバルなサービスを開発されている方であれば特に問題ないかもしれませんが、日頃JSTのみをターゲットにして開発している場合はこのあたりをきちんと意識しておいた方が良さそうです。

具体的には、

の三点を守っていれば大丈夫だと思われます。

また、定期的にワークフローを叩く “Scheduled Trigger” を使う際は timezoneAsia/Tokyo に設定しましょう。

    start_time: "2022-10-31T08:00:00",
    timezone: "Asia/Tokyo",
    frequency: {
      type: " weekly",
      on_days: ["Monday"],
    },

ファイルの分割に一工夫必要

次世代プラットフォームはシンプルな Function を複数組み合わせて複雑な Workflow を構築する、という設計がベースにあるようですが、実際に書くと、一つの Function も結構な長さになることがありました。

具体的な例を挙げると、「ボタンを押した時のコールバック」や「モーダルを submit した時のコールバック」は、それらを表示した Function に続けてメソッドチェーンの形で書く必要があるため、これらの処理をたくさん書くと単一の Function でもそれなりの長さになってしまいがちです。

正確に言えば、コールバック処理をメソッドチェーン以外の形に分割して書くことはできるのですが、その場合でも分割したコードを別ファイルに置くと、コンパイルはできても動作しないという状態になってしまいます。結局コールバックのコードは元の Function と同じファイルに書かなければならず、読みやすさという意味では正直メソッドチェーンと対して変わりません。

どうしてそうなるのか、ハッキリ原因が特定できているわけではありませんが、おそらく Function の定義にポイントがありそうです。

次世代プラットフォームの Function は DefineFunction で関数を定義し、実装を SlackFunction で行う、という二段構成になっていますが、この DefineFunctionsource_file というパラメータがあります。

export const SendActivityRequestFunctionDefinition = DefineFunction({
  callback_id: "send_activity_request",
  title: "勤怠入力メッセージを送信する",
  description: "勤怠入力を受けつけるためのメッセージをチャンネルに送信します",
  source_file: "functions/send_activity_request_function.ts",
});

この source_file は単一の文字列しか受け取ってくれないのですが、おそらくコンパイルの際にここで指定されたファイルだけを見にいくような仕様になっており、結果としてコールバック処理もすべてこのファイルに記述するしかない、という挙動になってしまっているのではないでしょうか。

将来的にはこれらのコールバック自体を別ファイルに切り出せる、あるいはコールバック自体を単独の Function として書けるような仕組みになればいいなと思いますが、現状ではこうした制限を踏まえて、1ファイルで頑張るか、コールバックから呼び出す処理のみを別ファイルに切り出すなどの回避策でしのぐしかなさそうです。

JSX-Slackの採用には注意

SlackのメッセージやモーダルでリッチなUIを作る場合、Block KitというUIフレームワークを使います。これは現行SDKでも次世代プラットフォームでも同様です。

しかしこのBlock Kit、中身は単なるJSONなので、複雑なUIを作ろうと思ったら結構大変です。

そこで、このBlock KitをJSXで書けるようにしてくれるサードパーティ製のライブラリがJSX-Slackです。しかも現行SDKのみならず、次世代プラットフォーム向けにDeno用のパッケージも配布されています。

これでどんなUIでも自由自在だぜ! ……と思ったのですが、現状では、次世代プラットフォームでの開発にガッツリ採用するのは避けたほうがいいかもしれません。

理由は主に二点です。

一つ目は、拡張子を .tsx にすると、 slack run のホットリロードが効かなくなるという点。

ローカルで開発版のSlackアプリを立ち上げる slack run コマンドを実行中、 .ts ファイルを更新すると即ホットリロードが実行され、インストール済みのアプリが更新されます。

これが大変便利なのですが、どうやら slack run コマンドがウォッチしているのは .ts ファイルのみで、 .tsx に対してはホットリロードがされないようです。調べた限りではホットリロード対象を変更することもできない模様。どなたか方法をご存知でしたら教えていただきたいです。

理由その二は、アプリの挙動が不安定になる場合があるという点です。

これは実際のところ本当の原因がどこにあるのかは不明ですが、Roosterで実際に発生した事例です。Block KitをどんどんJSX-Slackに置き換えていったところ、いつの間にかトリガーを実行してもFunctionが実行されないという状態が発生してしまいました。

当時のログは残っていないのですが、「Functionを実行した」というログは出ているが、Functionの先頭に仕込んだログは出力されない、という状態だったと記憶しています。

アプリの再インストールやトリガーの再設定など、思いつく限りの対応はしてみましたが改善されず、いったんJSX-Slackで実装した .tsx のファイルをすべて元に戻したところ挙動も元に戻ったため、上記ホットリロードの問題もあり、RoosterではいったんJSX-Slackの採用は見送ることにしました。

Roosterでは幸い自力でもなんとかできるぐらいのUIしか (今のところ) 実装していないのでなんとかなりましたが、今後機能を拡張していくことも考えて早くJSX-Slackに移行したいと考えています。 せめて .tsx ファイルもホットリロードしてほしい!

スラッシュコマンドは作れない

アプリのUIをどうしようか考えていた時、「Slackといえばスラッシュコマンドだよな」と漠然と考えていたのですが、次世代プラットフォームではスラッシュコマンドには対応していないし、対応する予定もないそうです。FAQで明言されています。

Slack的には新しい「リンクトリガー」という仕組みを使ってほしいそうですが、今のところ以下のように若干イケてないポイントがあって、まだ積極的に使いたいという感じではないですね。

  • メッセージ入力欄からCLI的に呼び出しにくい
  • メッセージ入力欄のショートカットメニュー (+ボタン) から呼び出せない
  • ショートカットはチャンネル上部にピン留めされるが、メッセージ欄からのカーソル移動距離が長い

どちらかといえば、メンションに反応して発火する ​​app_mentioned イベントトリガーの方がまだ使い勝手的には近い気がします。

おそらくリンクトリガーの使い勝手は今後どんどん改良されていくでしょうし、スラッシュコマンドのようなUIをメインにしたい場合はもうちょっと様子見をした方がいいかもしれません。逆にそういったCLI的なUIではなく、もっとボタンやモーダルを多用した対話的なUIを使いたい場合は、現状でもある程度のアプリは作れるはずです。

イベントループ処理は未実装

SlackのモーダルをSubmitした時のコールバックには、3秒以内にレスポンスを返さないといけないという制限があります。そのため、現行SDKで凝ったアプリを作る場合は内部でイベントキューを持っておき、コールバックで積んだイベントをイベントループが非同期で処理していく、という設計が主流のようです。

この仕組みが次世代プラットフォームにはビルトインされていないため、ちょっと凝ったことをしようと思ったらひと工夫必要になります。

Roosterでは時間のかかる処理を分割してバラけさせたことでそれぞれの時間は3秒以内に短縮できたのですが、もっと本格的な処理をしようと思ったらやはりイベントキューが必要になってくる気がします。

外部サービスでなんとかできるのかもしれませんが、そうなると結局「自分でインフラを用意しなくてもSlackインフラ上で動く」という利点があまり活きなくなってくると思うので、標準の仕組みでなんとかしてほしいところですね。

ちなみに、時間のかかる処理を非同期で実行するようにすればとりあえずレスポンスは即返せてハッピーなんじゃないか? と思いましたが、結局 Function は並列実行する処理が終わるまでレスポンスを返さないようで、レスポンスの時間制限にはなんら影響がない、という残念な結果になってしまいました。

まとめ

以上、Roosterの開発で引っかかった様々な問題をざっと書き連ねてきました。

色々書きましたが、わたしはこの次世代プラットフォーム、かなり気に入っています。

Slackアプリ開発は結構ややこしそうなイメージがあったのですが、テンプレート作成からデプロイまでCLIで完結できるため、とても楽に開発が進められました。

次世代プラットフォームはまだオープンベータ版ですし、2週間に一度のペースでどんどん新しいバージョンがリリースされています。おそらくここで書いた内容もすぐに時代遅れになっていくでしょう。

ぜひみなさんも次世代プラットフォームでのアプリ作りに挑戦してみてください!