🏋️

社内の蔵書管理ツールを Slack Workflow apps で実装した話

2024/10/23に公開1

はじめに

社員が持っている技術書などをインターン生に貸し出せたら良いなと思い、社内に置いてある共有本の蔵書と貸出を管理するツールを Slack のワークフローとして TypeScript で実装しました。この記事では、Slack Workflow apps を用いて、分岐を伴う処理をインタラクティブなフォームで実現する方法を紹介します。
私自身、Slack Workflow apps を実装するのは初めてでしたが、公式ドキュメントに則ることでスムーズに開発を行うことができました。
開発したアプリは以下のリポジトリで公開しているのでぜひ参照してください。
https://github.com/ken2403/book-admin/

TL;DR

  • Slack Workflow apps を作成するために必要な3つの要素は Functions, Workflows, Triggers で、データの永続化には DynamoDB にホスティングされている Datastore を使うことができる。
  • Slack Workflow apps は Workflows ごとに定義される。Workflows は Functions を順番に並べていくことで定義される。 Fucntions は Slackが提供する「Slack functions」と自分で定義した「Custom functions」に大別される。定義された Workflows は Triggers をトリガーとして実行することができる。
  • Workflows の仕組み上、順番に Functions を並べていく必要があるため、フォームで選択した内容によって次に出すフォームの内容を出し分けるといった分岐を実現するためには、フォームとフォームの間に Functions を挟むことで、次のフォームの内容を制御する必要があった。

今回作ったもの

2つの Workflow apps を実装しました。

1つ目は、「蔵書登録」アプリです。本の内容をフォーム経由で入力することで、Datastore に本の持ち主と題名が永続化され蔵書として登録ができます。
蔵書管理

2つ目は、「貸出管理」アプリです。蔵書の中から本を選んで借りるか、借りている本の中から返す本を選ぶことができます。こちらのアプリでは、1つ目のフォームで選択した内容によって、次のフォームの内容が変わります。

  • 貸出
    貸出管理_貸出

  • 返却
    貸出管理_返却

Slack Workflow apps の実装方法

Slack Workflow apps は「オートメーションプラットフォーム機能」を利用して実装することができます。「オートメーションプラットフォーム機能」とは、より簡単でセキュアなアプリ開発体験を提供するアプリ開発・実行基盤のことでSlack 内でワークフローを作るために用いられます。
詳細は以下のドキュメントを参照してください。
https://api.slack.com/automation

Slack Workflow apps を構成する要素は大きく3つあります。

  • Functions
    ワークフローアプリの挙動を決めるもので、Slackが提供する「Slack functions」と「Custom functions」に大別される。「Slack functions」には例えば、チャットにメッセージを送る send_message関数などがある。
  • Workflows
    順番に実行される Functions で構成された Slack Workflow apps の本体となる部分。
  • Triggers
    Workflows を実行するためのトリガー。リンクを踏んだらアプリが起動するLink triggersや特定の時間間隔で実行される Scheduled triggersなどがある。

さらに、Workflow apps の内容を永続化する際には DynamoDB にホスティングされている Datastore を使うこともできます。今回のアプリでも、蔵書の情報を永続化する際に使用しています。

蔵書管理Slack Workflow apps の実装

準備

まずは Slack CLI をインストールします。
以下のページを参考に、slack loginで接続したいワークスペースへのログインまでを済ませます。
https://api.slack.com/automation/quickstart

設計

今回作成するワークフローで必要な機能は主に以下の2つです。

  • 蔵書登録
    • 蔵書の所有者と本の名前を登録する
  • 貸出管理
    • 現在貸出可能な本の一覧を取得する
    • 返却可能な本の一覧を取得する
    • 誰がいつどの本を借りたかを管理する

実装

slack create my-app

以上のコマンドを実行してインテラクティブに設定を決めていくと、サンプルのディレクトリが生成されます。詳しくはこちらを参照してください。

1. 永続化するデータ型を定義する

datastoresディレクトリ配下に定義ファイルを追加します。DefineDatastore関数を用いてスキーマを定義します。
詳しくはこちらを参照してください。

import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";

const BookDatastore = DefineDatastore({
  name: "BookDatastore",
  primary_key: "id",
  attributes: {
    // 本の名前をidとして管理する
    id: {
      type: Schema.types.string,
    },
    book_owner_user_id: {
      type: Schema.types.string,
    },
    // not_borrowed: 貸出可能
    // borrowed: 貸出中
    borrowed_status: {
      type: Schema.types.string,
      enum: [bookBorrowed, bookNotBorrowed],
    },
    // 貸出中のユーザーID
    // 貸出可能の場合はnull
    borrowed_user_id: {
      type: Schema.types.string,
      default: null,
    },
  },
});

2. Workflows を定義する

workflowsディレクトリ配下に定義を書きます。DefineWorkflowを用いて定義を書き、addStepメソッドを用いて Workflow で呼び出したい Functions を順番に追加していきます。
詳しくはこちらを参照して下さい。

貸出管理のアプリケーションの場合は以下のような形になります。

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";

const BookBorrowWorkflow = DefineWorkflow({
  callback_id: "book_borrow_workflow",
  title: "共有本を借りる / 返す",
  description: "共有本を借りる / 返すためのワークフロー",
  input_parameters: {
    properties: {
      interactivity: { type: Schema.slack.types.interactivity },
      user_id: { type: Schema.slack.types.user_id },
      channel_id: { type: Schema.types.string },
    },
    required: [
      "interactivity",
      "user_id",
      "channel_id",
    ],
  },
});

// 本を借りるか返すかを選択するフォームを開く
const selectForm = BookBorrowWorkflow.addStep(
  Schema.slack.functions.OpenForm,
  {
    interactivity: BookBorrowWorkflow.inputs.interactivity,
    title: "共有本を借りる / 返す",
    description: "共有本を借りるか返すか選択してください",
    submit_label: "次へ",
    fields: {
      elements: [
        {
          name: "action",
          title: "借りる / 返す を選ぶ",
          type: Schema.types.string,
          enum: [bookBorrow, bookReturn],
          default: bookBorrow,
        },
      ],
      required: [
        "action",
      ],
    },
  },
);

Funcstions によってフォームの分岐を実現する

functionsディレクトリ配下にDefineFunctionを用いて定義を書くことができます。

貸出管理の場合は、「借りる」場合と「返す」場合で次に出すフォームの内容が変わります。次のフォームの前に、次のフォームに出す内容を制御するための Function を追加することで、内容を制御しました。

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

export const BookBorrowOrRetrunPreprocessFunctionDefinition = DefineFunction({
  callback_id: "book_borrow_or_return_preprocess_function",
  title: "Book Admin preprocess the borrow or return book",
  description: "共有本の一覧を取得し、次のフォームに必要な情報を取得する関数",
  source_file: "functions/book_borrow_return_preprocess_function.ts",
  input_parameters: {
    properties: {
      interactivity: {
        type: Schema.slack.types.interactivity,
        description: "インタラクティブな情報",
      },
      action: {
        type: Schema.types.string,
        description: "借りるか返すか",
        enum: [bookBorrow, bookReturn],
      },
    },
    required: [
      "interactivity",
      "action",
    ],
  },
  output_parameters: {
    properties: {
      interactivity: {
        type: Schema.slack.types.interactivity,
        description: "インタラクティブな情報",
      },
      next_form_title: {
        type: Schema.types.string,
        description: "次のフォームのタイトル",
      },
      next_form_description: {
        type: Schema.types.string,
        description: "次のフォームの説明",
      },
      next_form_books_title: {
        type: Schema.types.string,
        description: "次のフォームの本を選択する部分のタイトル",
      },
      next_form_user_id_title: {
        type: Schema.types.string,
        description: "次のフォームのユーザーIDを選択する部分のタイトル",
      },
      books: {
        description: "選択可能な本の一覧",
        type: Schema.types.array,
        items: {
          type: Schema.types.string,
        },
      },
    },
    required: [
      "interactivity",
      "next_form_title",
      "next_form_description",
      "next_form_books_title",
      "next_form_user_id_title",
      "books",
    ],
  },
});

input_parametersとして「借りる」か「返す」のenumを受け取り、output_parametersとして次のフォームのタイトルと必要な本の情報の一覧を返します。

Custom Functions の中で Slack Functions のOpenForm関数をよしなに呼び出すことができなかったため、無理やりフォームの内容を出し分けるための Functions を追加しました。

Triggers を定義する

最後に、triggersディレクトリ配下にLinktriggerの定義を追加します。

import { Trigger } from "deno-slack-sdk/types.ts";
import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";
import BookBorrowWorkflow from "../workflows/book_borrow_workflow.ts";

const bookBorrowLinkTrigger: Trigger<typeof BookBorrowWorkflow.definition> = {
  type: TriggerTypes.Shortcut,
  name: "共有本貸出 ",
  description: "共有本を借りる",
  shortcut: {
    button_text: "book-borrow",
  },
  workflow: `#/workflows/${BookBorrowWorkflow.definition.callback_id}`,
  inputs: {
    interactivity: { value: TriggerContextData.Shortcut.interactivity },
    channel_id: { value: TriggerContextData.Shortcut.channel_id },
    user_id: { value: TriggerContextData.Shortcut.user_id },
  },
};

最後に

蔵書の削除機能の実装がまだなので、それも揃えて完全なアプリにしたいです。

参考

GENDA

Discussion

Kazuhiro SeraKazuhiro Sera

素晴らしい活用例ですね!

データストアのアクセス部分は私が個人的に開発しているこの O/R マッパーを使うとかなりシンプルに書けるようになるので、もし次に何かデータストアを使ったものを作ることがあれば試してみてください! https://github.com/seratch/deno-slack-data-mapper

割と複雑なアプリをつくったときにこの O/R マッパーを使って、かなり楽に開発することができました: