Slack Next-gen platformの機能たんまり使って業務自動化してみた

AvatarPosted by

この記事は GRIPHONE Advent Calendar 2022 21日目の記事になります。

初めまして。2022年に新卒として入社したサーバーサイドエンジニアの桑添と申します!
今回は、2022年の9月ごろから利用可能になったSlackのNext-gen platformを使って業務の一部を自動化してみました。TypeScript初心者ながら、Next-gen platform機能をある程度網羅した(と思っている)ので、ほんの一部をハンズオン形式で記事に残します。どなたかの参考になれば幸いです。

あらかじめGitHubのリンクを貼っておきます。

この記事の対象者

  • SlackのNext-gen platformに興味がある方
  • TypeScript初心者の方
  • 自動化の手法について興味がある方

初めに言っておきますが、SlackのNext-gen platformは有料ワークスペースでなければ利用できません。ご注意ください。
この記事はNext-gen platformを丁寧に説明する記事ではありません。全ての機能について触れているわけでもありません。また、TypeScriptと記事執筆の初心者なのでコードの質に関してはご容赦下さい🙌

Slack Next-gen platformとは?

公式ドキュメントに機能は様々書いてありますが、簡単に言えば、今まで開発者側が用意する必要があったサーバーなどの環境をSlack側が用意してくれるようになったため、デプロイから何から簡単になったよ、というのが大きな魅力です。Node.js を生み出した Ryan Dahl が Node.js の設計の反省点を活かして新たに生み出した JavaScript / TypeScript ランタイムであるDenoで設計されています。Denoという文字列をいじるとNodeになるのは有名な話ですね。

大事な概念として、Workflow、Function、Triggerというものがあります。Workflowというのは一連の流れのことで、定義した複数のFunctionを順番に実行していくことで成り立っています。また、このWorkflowを実行するためにTriggerが用意されており、例えば、作成したアプリにメンションが飛んできた時をTriggerとして定義しておくと、定義しておいたWorkflowが実行される、と言った流れになります。

今回自動化する業務フロー

弊社では、機能を実装する上で必要な検証環境を用意しています。ここでテストをしながら実装することで、その機能が想定通り動作しているかを確認しています。

それら検証環境のことを「サンドボックス」と呼び、基本1機能を実装する上で1つのサンドボックスを確保する必要があるためサンドボックスは数十個用意されています。サンドボックスの数には限りがあるので、譲り合って利用しています。
さて、サンドボックスを確保する際に、どのサンドボックスを誰が、どの目的で、いつまで利用するのかを把握したい場面が多く出てきます。埋まっていた際は開放できそうな人に声をかけなければいけません。
今まではプランナーも含めた資料共有スペースで各々が確保、開放をしていたのですが、同期に遅延があり、普段のチャットツールとして利用しているSlack上で全て完結出来ればこれ以上のことはありません。

そのため今回は、新しく導入されたNext-gen platformを利用してこの業務を自動化していきます。

全体設計 & 本記事で見ていくもの

全要件の概要

  • サンドボックスのDB(Datastore)を初期化する
  • サンドボックス環境の状況が常に一覧で見れる
  • サンドボックス環境の確保
  • サンドボックス環境の開放
  • サンドボックス環境の更新
  • 毎日期日が過ぎたサンドボックスのアナウンスをしてくれる

全て記事にすると長くなり過ぎてしまうので、今回はほどよくNext-gen platformの機能を利用できた、一番上の「DBを初期化するフロー」を実装&解説していこうと思います。それでも文量としてはとんでもない規模になってしまうので、一部だけハンズオン形式で記述します。
残りのコードも気になる方はGitHubのリンクから見てみてください🙏

完成物

先に完成物のイメージを載せておきます。
こちらはGitHubのリンクのものとなりますので、ハンズオンするのはこの中のさらに一部です🙏

実装は下記のような流れになります。

ユーザー視点のフロー

  1. ブックマークバーからクリックで起動
  2. フォームを入力する
  3. 初期化開始メッセージが送信される
  4. (見えないが、Datastoreが初期化される)
  5. 過去のピン留めメッセージがUnPinされる
  6. 最新のサンドボックス状態を示すメッセージ(「状態メッセージ」とする)を送信
  7. 6をピン留め
  8. 初期化終了のメッセージが送信される

Function単位のフロー

  1. フォームをOpenして初期化理由を記入
  2. 初期化開始メッセージを送信
  3. Datastoreの初期化(サンドボックスDBの初期化、再挿入)
  4. オープンしているサンドボックスを全て取得
  5. トピックを更新
  6. (過去に初期化されていた場合)状態メッセージの取得
  7. (6で取得できたら)取得したアイテムをUnPin
  8. 親となるメッセージを送信
  9. 取得したitemsから状態メッセージを生成
  10. 生成した状態メッセージをスレッドとして送信
  11. 状態メッセージを次UnPinするためにDatastoreにSave
  12. 状態メッセージをPin留め
  13. 結果のメッセージを2のスレッドとして送信

この記事ではFunction単位のフローの1~3と13のみをハンズオン形式で記述します。残りはサンプルコードでお許しくださいませ🙏

事前準備

VSCodeで環境を作る

エディタはVSCodeをお勧めします。
拡張機能のDenoは無料ながら非常に強力な補完機能が備わっており、補完がなければ定義がないか、大体自分が間違っていることに気づくことが出来ます。

Slack CLIインストール&ログイン

公式のQuickStartを踏んで、Step3(2022/12/19 現在)までしていただければ大丈夫です。

  • Slack CLIをインストールする
    MacまたはLinuxユーザーの方は以下をターミナルで実行しましょう。
    windowsはmanual installが必要な様です。今回は環境がないため割愛させていただきます。
curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash
  • slackを利用してauthする
    以下のコマンドを実行して、発行されたslackauthticketを利用したいワークスペースの任意の場所で送信すればログインできます。簡単ですね。
slack login
  • appをcreateする
    下記のコマンドを入力してアプリをCreateしましょう。私はslack_sandbox_botとしました。
slack create お好きなアプリ名

↓選択を迫られます。なんでもいいですが、ほどよくディレクトリ構成があるHello Worldがおすすめです。

~
% slack create slack_sandbox_bot
? Select a template to build from: Hello World

Creating new Slack app at /Users/hogehoge/slack_sandbox_bot

📦 Installed project dependencies

✨ slack_sandbox_bot successfully created

🧭 Explore your project's README.md for documentation and code samples, and at any time run slack help to display a list of available commands

🧑‍🚀 Follow the steps below to try out your new project

1️⃣  Change into your project directory with: cd slack_sandbox_bot

2️⃣  Develop locally and see changes in real-time with: slack run

3️⃣  When you're ready to deploy for production use: slack deploy

4️⃣  Create a trigger to interact with your app: slack triggers create
   Learn more: https://api.slack.com/future/triggers

🔔 If you leave the workspace, you won’t be able to manage any apps you’ve deployed to it. Apps you deploy will belong to the workspace even if you leave the workspace
  • slack runして動くか確認する
    下記コマンドを打つとauthしたワークスペース内で開発中アプリとして動作させることが出来ます。コマンド1つでdev appとしてワークスペースにインストールされます。簡単で素晴らしいですね。
    runした状態でいると、変更を検知してトリガー以外のデプロイをしてくれます。そのままにしておき、必要なターミナルは別で起動して作業することをおすすめします。
slack run

↓注意文は気にしなくて大丈夫です

? Choose a workspace  ワークスペース名  xxxxxxxxxxx 
   App is not installed to this workspace

Updating dev app install for workspace "ワークスペース名"

⚠️  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
✨  user_name of ワークスペース名
Connected, awaiting events

ここからも(devとして)インストールされていることが確認できますので、アクセスしてみてください。

実装やっていき

結論:ディレクトリ構成

予めディレクトリ構成を載せておきます。ほぼテンプレ通りですが、参考にしてください。

│  
├── .github
│   └── workflows
│       ├── deno.yml
│       └── udd-update-dependencies.yml
├── .slack
│   ├── apps.dev.json
│   └── apps.json
├── .vscode
│   └── settings.json
├── assets
│   └── default_new_app_icon.png
├── datastore
│   ├── pinned_message.ts
│   └── sandbox.ts
├── functions
│   ├── datastore
│   │   ├── pinned_message
│   │   │   ├── find.ts
│   │   │   └── save.ts
│   │   ├── sandbox
│   │   │   ├── load.ts
│   │   └── init.ts
│   ├── sandbox
│   │   ├── output_sandbox_state.ts
│   │   └── update_topic.ts
│   └── unpin_message.ts
├── triggers
│   └── shortcut
│       ├── init_datastore.ts
├── types
│   ├── pinned_message.ts
│   ├── sanboxies.ts
│   └── sandbox.ts
├── workflows
│   ├── init_datastore.ts
├── .gitignore
├── deno.jsonc
├── import_map.json
├── LICENSE
├── manifest.ts
├── README.md
└── slack.json

Step1:Manifestの実装

まずはManifestを定義しましょう。

Manifestとは、SlackAppの概要定義書のようなものです。 作成するアプリの名前や説明文、アイコンなどもこちらで容易に編集が可能です。 加えて、Manifest上で使用するWorkflowやFunction、Datastoreなどもここで定義する必要があることに注意です。
実装して必要なものはこちらに加えることを把握した上で、今は必要なら名前と説明文だけ変更しましょう。

slack_sandbox_bot/manifest.ts

import { Manifest } from "deno-slack-sdk/mod.ts";
import GreetingWorkflow from "./workflows/greeting_workflow.ts";

/**
 * The app manifest contains the app's configuration. This
 * file defines attributes like app name and description.
 * https://api.slack.com/future/manifest
 */
export default Manifest({
  name: "slack_sandbox_bot", //そのまま
  description: "サンドボックスを健気に管理してくれる可愛い子です。", //修正した!
  icon: "assets/default_new_app_icon.png",
  workflows: [GreetingWorkflow],
  outgoingDomains: [],
  botScopes: ["commands", "chat:write", "chat:write.public"],
});

↓saveすると、検知してreinstallしてくれる

File change detected: /Users/hogehoge/slack_sandbox_bot/manifest.ts, reinstalling app...

⚠️  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
App successfully reinstalled

↓saveしただけなのにしっかり変わってる!すごい!

Step2:フォームをOpenする

まずはフォームをオープンする実装を行います。

Workflowの定義

workflowを定義しましょう。
初期から入っているworkflowの名前等を編集しても構いません。

workflows/init_datastore.ts

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

/**
 * A Workflow is a set of steps that are executed in order.
 * Each step in a Workflow is a function.
 * https://api.slack.com/future/workflows
 */
const InitDatastoreWorkflow = DefineWorkflow({
  callback_id: "init_datastore_workflow",
  title: "サンドボックスを初期化する",
  description: "サンドボックス確保状態を初期化します。一部のユーザーしか使用できません。",
  input_parameters: {
    properties: {
      interactivity: {
        type: Schema.slack.types.interactivity,
      },
      channel: {
        type: Schema.slack.types.channel_id,
      },
      user_id: {
        type: Schema.slack.types.user_id,
      },
      timestamp: {
        type: Schema.slack.types.timestamp,
      },
    },
    required: ["interactivity", "channel", "user_id", "timestamp"],
  },
});

細かい仕様を見たい方は公式(Workflows)を見ていただくのが一番早いと思います。
DefineWorkflowで定義が可能です。callback_idとtitleが必須項目で、その他は任意項目となります。
一番大事なのはinput_parametersです。これはWorkflow内で利用するプロパティの中で、Triggerから受け取れるものを定義します。これらの値は後述されるTriggerから入力されるものとなります。

Triggerの定義&実装

先ほど定義したWorkflowを起動するためのTriggerを定義&実装しましょう。
こちらも初期からあるものを編集して構いません。

triggers/shortcut/init_datastore.ts

import { Trigger } from "deno-slack-api/types.ts";
import InitDatastoreWorkflow from "../../workflows/init_datastore.ts";

/**
 * Triggers determine when Workflows are executed. A trigger
 * file describes a scenario in which a workflow should be run,
 * such as a user pressing a button or when a specific event occurs.
 * https://api.slack.com/future/triggers
 */
const initDatastoreLinkTrigger: Trigger<
  typeof InitDatastoreWorkflow.definition
> = {
  type: "shortcut",
  name: "SB確保状況初期化",
  description: "サンドボックスの確保状況を初期化するワークフローを起動するトリガーです",
  workflow: "#/workflows/init_datastore_workflow",
  inputs: {
    interactivity: {
      value: "{{data.interactivity}}",
    },
    channel: {
      value: "{{data.channel_id}}",
    },
    user_id: {
      value: "{{data.user_id}}",
    },
    timestamp: {
      value: "{{event_timestamp}}",
    },
  },
};

export default initDatastoreLinkTrigger;

こちらも細かいことは公式(Triggers)を見ていただくのが一番分かりやすいです。いくつかTriggerは種類があるのですが、今回はLink Triggersを利用しています。
Slack内の任意の場所でリンクを送信すれば起動するTriggerです。また、一度起動した後はブックマックバーに保存されるため非常に便利なTriggerとなっています。入力可能なパラメーターは公式(Triggers/inputs)からご確認ください。

Functionの定義&実装

自前でFunctionを実装するためには定義からコードを用意する必要がありますが、一部の汎用的なFunctionは予めSlack側が用意してくれており、公式(functions)から確認できます。嬉しいですね。今回はそれを利用してみましょう。
そのため、新たにファイルを作成する必要はなく、ワークフローに追加する形になります。workflows/init_datastore.tsに下記を追加しましょう。
ちなみにですが、この後カスタムFunctionも実装します。


// 1:フォームオープン
const approve = InitDatastoreWorkflow.addStep(
  Schema.slack.functions.OpenForm, //用意されているFunction
  {
    title: "初期化確認画面",
    interactivity: InitDatastoreWorkflow.inputs.interactivity,
    submit_label: "初期化する",
    description: "初期化を行う場合は、理由を記入してボタンを押下してください。",
    fields: {
      elements: [{
        name: "description",
        title: "初期化理由",
        type: Schema.types.string,
      }],
      required: ["description"],
    },
  },
);

最初に定義したworkflowにaddStepしていくことでFunctionを追加していきます。第二引数としてtitleなどが包含されたオブジェクトを入力します。用意された各Functionにも全て公式ドキュメントが用意されているため、必要なパラメーターもここから確認が可能です。

※Trigger以外の実装物は全てManifestに登録しよう!

これ忘れがちなので注意です。
実装したTrigger以外の全てのものはManifestに登録する必要があります。
今はWorkflow1つだけですが、今後実装したものは全て登録しましょう。

import { Manifest } from "deno-slack-sdk/mod.ts";

/**
 * The app manifest contains the app's configuration. This
 * file defines attributes like app name and description.
 * https://api.slack.com/future/manifest
 */
export default Manifest({
  name: "slack_sandbox_bot",
  description: "サンドボックスを健気に管理してくれる可愛い子です。",
  icon: "assets/default_new_app_icon.png",
  workflows: [InitDatastoreWorkflow], //追加した!
  functions: [],                      //実装したら必ず追加!
  outgoingDomains: [],
  datastores: [],                     //実装したら必ず追加!
  types: [],                          //実装したら必ず追加!
  botScopes: ["commands", "chat:write", "chat:write.public"],
});

Step3:初期化開始メッセージの送信

同様に初期化が開始されたことを示すメッセージを送信するFunctionもworkflows/init_datastore.tsに追加してみましょう。
メッセージを送信するFunctionも既に用意されています。

// 2:初期メッセージ送信
const sendRunMessage = initDatastoreWorkflow.addStep(
  Schema.slack.functions.SendMessage, //用意されているFunction
  {
    channel_id: initDatastoreWorkflow.inputs.channel,
    message: `<@${initDatastoreWorkflow.inputs.user_id}> \n承りました。初期化を開始します...`,
  },
);

Step4:Datastoreを初期化する

さて、ここまでは簡潔でしたが、ここから少し複雑になります(まだあるんかい)。そもそもここまで読めている人はいるのでしょうか。
Next-gen platform機能の目玉の一つであるDatastoreを用いて、サンドボックス環境の初期化フローを実装していきます。

Workflowに追加

まずは分かりやすさのためにWorkflowにFunctionを追加しましょう。
この時点ではエラーが発生していますが、気にしなくて大丈夫です。

// 3:Datastore初期化
const init = initDatastoreWorkflow.addStep(
InitDatastoreFunctionDefinition //これこのあとに実装する
, {
  user_id: initDatastoreWorkflow.inputs.user_id,
  timestamp: initDatastoreWorkflow.inputs.timestamp,
  description: approve.outputs.fields.description,
});

CustomTypeの実装

Next-gen platformにはSlack側が事前に用意してくれた型(Built-in Typeという)がたくさんあるのですが、Built-in Typeで足りない分は自作する必要があります。今回私はDatastoreの型とFunction等で利用する型を共通化したい!という願いのもとCustom Typeを利用しました。

types/sandbox.ts

import {
  DefineType,
  Schema,
} from "https://deno.land/x/deno_slack_sdk@1.4.3/mod.ts";

//{{status}}
//1→空き
//2→確保済み
//3→使用不可
const SandboxType = DefineType({
  title: "Sandbox Type",
  description: "Use for definition type of sandbox on slack",
  name: "sandbox",
  type: Schema.types.object,
  properties: {
    id: {
      type: Schema.types.integer,
    },
    name: {
      type: Schema.types.string,
    },
    user_id: {
      type: Schema.types.string,
    },
    user_name: {
      type: Schema.types.string,
    },
    description: {
      type: Schema.types.string,
    },
    due_date: {
      type: Schema.slack.types.date,
    },
    status: {
      type: Schema.types.integer,
    },
    server_branch: {
      type: Schema.types.string,
    },
    client_branch: {
      type: Schema.types.string,
    },
    updated_at: {
      type: Schema.slack.types.timestamp,
    },
  },
  required: [],
});

export default SandboxType;

このようにDefineTypeを使用して定義します。各パラメーターはBuilt-in Typeを用いて定義されています。
また、このオブジェクトの配列型を扱いたい場合が何度かあったため、それも定義しました。

types/sanboxies.ts

import {
  DefineType,
  Schema,
} from "https://deno.land/x/deno_slack_sdk@1.4.3/mod.ts";
import SandboxType from "./sandbox.ts";

//sandboxの配列型
const SandboxiesType = DefineType({
  title: "Sandboxies Type",
  description: "Use for definition type of sandboxies on slack",
  name: "sandboxies",
  type: Schema.types.array,
  items: {
    type: SandboxType,
  },
});

export default SandboxiesType;

このようにCustomTypeを定義することで、後述するDatastoreの定義にも、Functionのinput_parameterの型にも流用することができ、管理しやすくなると思われます。

先ほどから言っている通り、実装したCustom TypeはManifestに記述する必要があるため注意してください。

Datastoreの実装

先ほど実装したCustom Typeを用いてDatastoreを定義しましょう。

datastore/sandbox.ts

import { DefineDatastore } from "deno-slack-sdk/mod.ts";
import SandboxType from "../types/sandbox.ts";

const SandboxDatastore = DefineDatastore({
  name: "sandbox",
  primary_key: "name",
  attributes: SandboxType.definition.properties,
});

export default SandboxDatastore;

先ほどCustomTypeを定義したことで簡潔なコードになっていますね。これだけでDatastoreの実装は終了です。

Functionの実装

ではこれらを用いてFunctionを実装し、Workflowが動く様にしましょう。

functions/datastore/init.ts

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

/**
 * Functions are reusable building blocks of automation that accept
 * inputs, perform calculations, and provide outputs. Functions can
 * be used independently or as steps in Workflows.
 * https://api.slack.com/future/functions/custom
 */
export const InitDatastoreFunctionDefinition = DefineFunction({
  callback_id: "init_datastore_function",
  title: "init datastore function",
  description: "サンドボックスの確保状態を初期化します",
  source_file: "functions/datastore/init.ts",
  input_parameters: {
    properties: {
      user_id: {
        type: Schema.slack.types.user_id,
      },
      timestamp: {
        type: Schema.slack.types.timestamp,
      },
      description: {
        type: Schema.types.string,
      },
    },
    required: ["user_id", "timestamp", "description"],
  },
  output_parameters: {
    properties: {
      completed: {
        type: Schema.types.boolean,
        description: "正常に初期化が完了したか",
      },
      message: {
        type: Schema.types.string,
        description: "スレに返答する内容",
      },
    },
    required: ["completed", "message"],
  },
});

export default SlackFunction(
  InitDatastoreFunctionDefinition,
  async ({ inputs, client }) => {
    const sandboxName = [
      "sb01",
      "sb02",
      "sb03",
      "sb04",
      "sb05",
      "sb06",
      "sb07",
      "sb08",
      "sb09",
      "sb10",
      "sb11",
      "sb12",
      "sb13",
      "sb14",
      "sb15",
      "sb16",
      "sb17",
      "sb18",
      "sb19",
      "sb20",
      "sb21",
      "sb22",
      "sb23",
      "sb24",
      "sb25",
      "sb26",
      "sb27",
      "sb28",
      "sb29",
      "sb30",
      "sb31",
      "sb32",
    ];
    let index = 1;
    let completed = true;
    let message = "正常に初期化されました!";
    const date = "2099-01-01";

    for (const element of sandboxName) {
      const putResponse = await client.apps.datastore.put<
        typeof SandboxDatastore.definition
      >({
        datastore: "sandbox",
        item: {
          id: index,
          name: element,
          user_id: "",
          user_name: "",
          description: inputs.description,
          due_date: date,
          status: 1,
          server_branch: "",
          client_branch: "",
          updated_at: inputs.timestamp,
        },
      });

      if (!putResponse.ok) {
        completed = false;
        message = "エラーが発生しました...";
        console.log(`${index} : error`);
        break;
      } else {
        console.log(`${index} : ok`);
        index++;
      }
    }

    return { outputs: { completed, message } };
  },
);

async ({ inputs, client }) => {}部分で色々な値を受け取れます。受け取れる値はこちらから見れますが、主に使用するのはenv, inputs, clientかなと思います。

Step5:結果メッセージを送信する

最後に結果のメッセージを送信します。

// 13:完了のメッセージを2のスレッドとして送信
initDatastoreWorkflow.addStep(Schema.slack.functions.SendMessage, {
  channel_id: initDatastoreWorkflow.inputs.channel,
  message: init.outputs.message,
  thread_ts: sendRunMessage.outputs.message_ts,
});

これで実装は完了です!最後に挙動を確認してみましょう!
Triggerをcreateして…

slack trigger create --trigger-def ./triggers/shortcut/init_datastore.ts

発行されたリンクをSlackに送信して起動!

無事動いていますね!実際には1分くらい時間がかかってます。
CLIから見ると分かりますが、しっかりとDatastoreも初期化されています。
お疲れ様です!

最後に

いかがでしたでしょうか?
とんでもなく長く読みづらい記事になってしまい、恥ずかしい限りです。
これでも正直全く書き足らず、開発する上で困惑したことやTipsなど、共有したいことがまだ山ほどあります。

今回紹介した機能はほんの一部で、CLIから環境変数やDatastoreをいじったり、Function内でSlackAPIを併用することも可能です。
Next-gen platformを用いることで今までよりも分かりやすく、柔軟に、さまざまな自動化を実現することが可能になりました。

皆様もこの機会に一度、業務の自動化を考えてみてはいかがでしょうか?
最後までお読みいただきありがとうございます。どなたかの役に立てばそれ以上の喜びはありません。