entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

Slack Platform(Deno)でチーム開発に便利なbotを作った話

こんにちは。ピクシブで新規事業部に所属しています、ああうえ(@_kwzr_)と申します。

最近、自分の所属する部署で飼っている便利botをSlack Platformで作り直したので、その紹介をしようと思います。

Slack Platformとは、Slack上にアプリケーションをホスティングできるサービスです。

最近オープンβ版が公開されて、Slackの有料プランを使っているユーザーであれば使えるようになりました。

便利bot「pastel」の紹介

便利botはpastelと名付けられていて、 @pastel 共有 内容 のようにbotに対してメンションを送るとその内容をNotionに記録してくれて、後からMTGのときに振り返ることができたり、

※こんな感じでNotionに反映されます。各共有内容にスプリントへのrelationが付いてくれるので、勝手にそのスプリントの共有内容としてまとめてくれるところが自慢ポイントです

朝会・夕会などのリマインドをしてくれたり、

フォームを表示して、今日の予定や日報を記録できたりします。

便利ですね。

他にも、バックログへのアイテムの追加や、メンバー情報の管理、ヘルプの表示などができるようになっています。

Slack Platformでできること

Slack Platformはまだβ版で、今後より機能は増えていくと思いますが、現時点でのできることは以下のような感じです。便利botを作るには全然困らない機能が揃っています💪

  • Slackのイベントを受け取るトリガーがある
    • appにmentionした
    • ユーザーがチャンネルにjoinした
    • など
  • スケジュールを設定して発火できるトリガーがある
    • 日・週・月ごと
    • 曜日や頻度
    • など
  • リンク(ボタン)を押して起動できるトリガーがある
  • Webhookで起動できるトリガーがある
  • フォームを表示できる
    • テキスト・数字入力・リストからの選択などが可能
    • テキストフィールドにplaceholderの設定はできなそう
  • Datastoreで簡易的な情報を書き込み・読み込みできる
  • Function内では、だいたい何でもできる
    • NotionなどサードパーティのAPIを呼び出したり

Slack Platformの良さ

開発体験が本当に良いと思います。

Denoで依存関係をインストールすることなく開発を始めることができて、フォーマットも勝手に効いてくれます。Slack PlatformのAPIもTypeScriptで型がしっかり(本当にしっかり)ついているので、実行前に書き間違えに気付くことができます。

自分はiOSアプリエンジニアをやっているので、がっつり触ったことがなかったのですが、TypeScriptの型推論の凄さに驚きました。例えば、FunctionやWorkflowに input_parametersoutput_parameters を設定するのですが、このrequiredに書いたものが引数に抜けてたりすると型エラーを起こしてくれます。

const XXXFunctionDefinition = DefineFunction({
  ...
  input_parameters: {
    properties: {
      channel_id: {
        type: Schema.slack.types.channel_id,
      },
    },
    required: ["channel_id"],
  },
  ...
}
const xxxFunctionStep = XXXWorkflow.addStep(
  XXXFunctionDefinition,
  {
    // channel_idがないと怒られる
  },
);

こうすればよかった話

サンプルを見ると、トリガーごとにファイルを作ってtriggersディレクトリに置いています。シンプルなトリガーだけであればそれで良いと思うのですが、トリガーをファイルで扱うことの問題として、環境変数を取得しにくいことがありました。例えば、トリガー内でSlackのチャンネルIDをenvから取ってきたい場合に困ってきます。

一応、ローカルの.envファイルはdotenvを使えば取得することができますが、デプロイ時は slack env で登録した環境変数を見に行って欲しいです。

// おそらく推奨されない例
import { config } from "https://deno.land/x/dotenv/mod.ts";

const env = config();

const xxxTrigger: Trigger<typeof XXXWorkflow.definition> = {
  ...
  event: {
    event_type: "slack#/events/app_mentioned",
    channel_ids: [env["SLACK_CHANNEL_ID"]],
  },
  ...
};

トリガーはランタイムでFunction内で作成することもできるため、環境変数にアクセスしたい場合はFunctionの引数のenvを使ってランタイムで生成すると良いでしょう。

export default SlackFunction(RegisterTriggerFunctionDefinition,
  async ({ token, env }) => {
    const client = SlackAPI(token, {});
    const newTrigger = await client.workflows.triggers.create<typeof XXXWorkflow.definition>({
      ...
        event: {
        event_type: "slack#/events/app_mentioned",
        channel_ids: [env["SLACK_CHANNEL_ID"]],
      },
        ...
    });
    ...
}

また、指定の時間にWorkflowを動かすscheduledトリガーもあるのですが、こちらは将来の時間しか設定できないため、トリガーの内容を更新したい場合は日付を書き換える必要があります。こちらも、Function内でランタイムにトリガーを生成するようにした方がデプロイの手間が省けます。

export default SlackFunction(
  RegisterTriggerFunctionDefinition,
  async ({ token, env }) => {
    const client = SlackAPI(token, {});

    // すでに設定されたトリガーがあれば取り出す
    const previousTrigger = await client.apps.datastore.get({
      datastore: "triggers",
      id: "xxx_schedule_trigger",
    });

    if (previousTrigger.item.triggerId) {
      await client.workflows.triggers.update<typeof XXXWorkflow.definition>({
        trigger_id: previousTrigger.item.triggerId,
        type: "scheduled",
        ...
        schedule: {
          start_time: /* 将来の日付 */,
          ...
        }
      });
    } else {
      const newTrigger = await client.workflows.triggers.create<typeof XXXWorkflow.definition>({
        type: "scheduled",
        ...
        schedule: {
          start_time: /* 将来の日付 */,
          ...
        }
      });

      await client.apps.datastore.put({
        datastore: "triggers",
        item: {
          id: "xxx_schedule_trigger",
          triggerId: newTrigger.trigger?.id,
        },
      });
    }
    ...
  },
);

こんな感じのDatastoreを作っておいて、現在のtriggerIdを記録しています。

const TriggerDatastore = DefineDatastore({
  name: "triggers",
  primary_key: "id",
  attributes: {
    id: {
      type: Schema.types.string,
    },
    triggerId: {
      type: Schema.types.string,
    },
    linkUrl: {
      type: Schema.types.string,
    },
  },
});

今のところtriggerのlistをランタイムで取得する方法はないので、triggerIdを記録しておくしかない感じです。 api.slack.com

This means that you'll need to have stored the trigger_id created for that instance.

まとめ

TypeScriptは普段使っていませんが、開発体験がよかったのですぐに運用するところまで持ってくることができました。

将来的には実行内容に応じて課金がされるようですが、手軽さを考えると手放したくないツールになりそうな予感がしています。今後もSlack Platformのアップデートが楽しみですね。

皆さんもSlack Platformでチーム開発を便利にするbotを作ってみてはいかがでしょうか。botは作りたくないけど、便利なbotを使いたいって方はぜひ一緒に働きましょう!

20191219012707
ああうえ
2017年4月新卒入社。pixivやpixiv Sketchなど、iOSやAndroidアプリの開発をしてきました。最近はプロダクトマネージャーをやっています。趣味はお絵かきと、うさぎです。