MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

小規模から始めるSlack Boltでのアプリケーション開発

はじめに

こんにちは。システム開発部 新規プロダクト開発ユニット(NDU)の東です。

NDUはその名の通り、新規プロダクトを開発するユニットです。 新規プロダクトの立ち上げにおいて、開発部門は効率的な開発で迅速なローンチを行うことを目標の1つとして開発をしています。

この記事では、Slack Bolt for JavaScript (Node.js) を使って、小さなSlackアプリケーションを開発する第一歩を踏み出す方法をご紹介します。

Slack Boltとは

Slack Boltは、Slackが公式に提供しているアプリケーション開発のためのフレームワークです。 Boltを利用することで、Slack APIの複雑な部分を意識することなく、直感的にアプリケーションを構築できます。

Boltの主な利点は以下です。

  • 記述量が少ない: イベントの受信や応答といった定型的な処理を、数行のコードでシンプルに記述できる。
  • 直感的なAPI: メッセージへの応答、ボタンクリックへの反応など、やりたいことが分かりやすいメソッド名で提供されている。
  • ソケットモード対応: 従来の方法(Slack SDK)では必要であった外部へ公開するエンドポイントの設定が、ソケットモード対応により不要になった。
    • 今回使用するアプリケーションでは弊社のデータセンター内にある情報へアクセスする必要があり、外部へ公開する必要がないためより安全にアクセスできるようになった。

事業部が抱えていた課題

弊社が提供する広告配信プラットフォームUNIVERSE Adsでは、通常、管理画面から広告の予算を設定します。 しかし、一部の運用担当者は非常に多くのアカウントを管理しており、日々の予算設定にかかる管理画面の操作が大きな負担となっていました。

この課題を解決するため、複数のアカウントの予算を一括で設定する機能を開発することにしました。 ただし、この機能の利用者は多くのアカウントを抱えているごく一部の社内担当者に限定されていました。

また、社内のコミュニケーションツールとしてSlackを採用しており、Slackアプリには馴染みやすいという点があります。

多くのユーザーに影響があり、大規模な改修となる管理画面への機能追加ではなく、より迅速に開発・リリースできるSlack Boltを採用しました。

Slackアプリの準備

実装へ取り掛かる前に、まずはSlackアプリを作成します。 Slackアプリの作成方法はこちらを参照して下さい。

作成したアプリの認証情報は、コード中で環境変数として読み取るようにします。

実装

今回実装する機能の処理の流れとしては以下の通りです。

  1. ショートカットコマンドを受信して、予算設定が記述されたCSVをアップロードするための入力フォームを起動
  2. 入力フォームの情報を受信
  3. CSVの内容をパースしてバリデーションを行う。
  4. バリデーションが通過した場合、予算設定の情報をMySQLのテーブルに登録

今回は特にSlackが関わる部分として、1、2の実装を紹介します。

ショートカットコマンドを受信して、予算設定が記述されたCSVをアップロードするための入力フォームを起動

import { App } from '@slack/bolt'
import type { View } from '@slack/types'
import { modal } from './modal'  // モーダルのJSONオブジェクト
// slackアプリ初期化
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN,
})

// ショートカットコマンドをリッスンして入力フォームのモーダルを開く
app.command('/campaign', async ({ ack, body, client, logger }) => {
  await ack()

  try {
    const result = await client.views.open({
      trigger_id: body.trigger_id,
      view: {
        ...modal,  // モーダルのJSONオブジェクトを反映
        private_metadata: body.channel_id // app.viewではチャンネルの情報を受け取れないので、メタデータとしてチャンネルIDを設定
      } as View
    });
    logger.info(result)
  }
  catch (error) {
    logger.error(error)
  }
})

特定のチャンネルだけで実行させる工夫

今回のアプリケーションでは、特定の運用担当者だけが参加するプライベートチャンネルでのみ操作を許可するという要件がありました。 しかし、Slackのショートカット機能は、デフォルトではどのチャンネルからでも呼び出すことができてしまいます。 これでは、意図しないチャンネルで重要な操作が実行されてしまう可能性があります。

そこで、「許可されたプライベートチャンネルから実行された場合のみ、処理を続行する」という制御を入れることにしました。

この制御を実現する上で、1つ技術的な課題がありました。 ユーザーがモーダル(入力フォーム)で情報を送信した際、そのデータを受け取る後述のapp.view()という関数では、どのチャンネルから操作が開始されたのかを直接知ることができません。

この問題を解決するため、Slack Boltのprivate_metadataという機能を利用します。 モーダルを開く最初の段階で、このprivate_metadata実行元のチャンネルIDをこっそり含めておきます。 こうすることで、後続の処理で「このリクエストは、許可されたあのチャンネルからのものだ」と正しく判断できるようになります。

UIの生成

入力フォームのモーダルのUIは、ブラウザ上で編集しながらUIを表示できるSlack Block Kit Builderを用いると簡単に作ることができます。

生成したUIのJSONオブジェクトをclient.views.openの引数内にあるviewへ渡すことで動作します。

Slack Block Kit Builder

入力フォームの情報を受信

// 入力フォームの情報を受信
app.view("campaign_daily_charge_limit", async ({ ack, context, view, client, body }) => {
  await ack()

  // メタデータとして受け取ったチャンネルIDにレスポンスを返す。なければDMで直接送信
  const channelId = view.private_metadata || body.user.id

  // ファイルを履歴としてアップロードするため、ファイル名と内容を保持
  let filename: string | undefined = undefined
  let csvContents: string | undefined = undefined

  const mySqlConnection = await mysql.createConnection(getMySQLConfig())

  try {
    const values = view.state.values
    const files = values.input_csv_file.input_csv_file.files
    if (!files || files.length !== 1) {
      throw new Error('csvファイルを1つ添付してください。')
    }

    // アップロードされたcsvファイルをパース
    const service = new Service()
    const fileInfo = await client.files.info({ file: files[0].id })
    if (!fileInfo.file || fileInfo.file.mimetype !== 'text/csv') {
      throw new Error('csvファイルを添付してください。')
    }
    filename = fileInfo.file.name
    csvContents = await service.downloadFile(fileInfo.file?.url_private_download, process.env.SLACK_BOT_TOKEN!)
    const csvRecords = service.parseCsv(csvContents)

    // 3. CSVの内容をパースしてバリデーションを行う。
    const updates = await service.validation(csvRecords, channelId, config, mySqlConnection)

    // 4. バリデーションが通過した場合、予算設定の情報をMySQLに登録<figure class="figure-image figure-image-fotolife" title="モーダル">[f:id:microad-developers:20250926180015p:plain:alt=モーダル]<figcaption>モーダル</figcaption></figure>
    await service.updateBudget(updates, mySqlConnection)

    // 成功したメッセージを送る
    const successAccountIds = [...new Set(updates.map((u) => u.accountId))].sort()
    await client.chat.postMessage({
      token: context.botToken,
      channel: channelId,
      text: `<@${body.user.id}>\n成功しました。\nアカウントID: ${successAccountIds.join('、')}`,
    })
  } catch (error) {
    // エラーメッセージを送る
    await client.chat.postMessage({
      token: context.botToken,
      channel: channelId,
      text: `<@${body.user.id}> \n失敗しました。\nエラー内容: ${error}`,
    })
  } finally {
    // ファイル情報がある場合はslackチャンネルにファイルをアップロードする
    if (filename && csvContents) {
      await client.filesUploadV2({
        channel_id: channelId,
        filename: filename,
        file: Buffer.from(csvContents),
      })
    }
    // MySQLの接続を終了
    await mySqlConnection.end()
  }
});

// アプリを起動
(async () => {
  await app.start(process.env.PORT || 3000)
  app.logger.info('⚡️ Bolt app is running!')
})()

動作している様子

ショートカットコマンド(今回の場合、/campaign)を入力すると、予算が設定されているCSVファイルをアップロードするためのモーダルが出現します。

予算が設定されているCSVファイルをアップロードします。 成功すると実行したユーザーのメンションと成功した旨の内容をメッセージで送信します。 また、履歴としてアップロードしたファイル情報を添付しています。

※ 表示されているアカウントIDなどの情報はテスト用で、実際のものではありません。

終わりに

この記事では、Slack Boltを活用して、特定の社内業務を効率化するアプリケーションの開発事例を紹介しました。

今回の開発のポイントは、利用者がごく一部に限られるという課題に対し、管理画面に大規模な改修を加えるのではなく、迅速に開発できるSlack Boltを選択した点です。 また、private_metadataにチャンネルIDを保持させることで、特定のチャンネルからのみ操作を許可するという、より実践的な実装方法についても触れました。

Slack Boltを使えば、今回のような「特定のチームの、ちょっとした不便」を解決するツールを、驚くほど手軽に開発できます。 この記事が、皆さんのチームが抱える課題を解決するヒントになれば幸いです。 ぜひ、身近な業務の自動化からSlackアプリ開発に挑戦してみてはいかがでしょうか。