😽

Slack Web APIを使ってslackに任意の画像を複数枚投稿する

2022/06/18に公開

以前、Incoming Webhookを使ってslackにメッセージを送信するを書いたのですが、Incoming Webhookでは画像の投稿はできません。
そこで今回はNext.jsで作ったアプリからslackに画像を投稿する方法について書きます。

以下のように画像とメッセージを合わせて送ることができる方法です。

ポイントは以下の2つなので、順に書いていきます。
①webアプリからSlack Web APIに画像を送り、画像を閲覧するためのpermalinkを作成する
②取得したpermalinkをメッセージとともにまとめてslackに投稿する

前提として、予めSlack Web APIを使うためのTokenと投稿したいチャンネルのChannelIdを取得しておく必要があります。(https://api.slack.com/apps から発行できます。)

下準備

まずはSlack Web APIを使うためのnpmパッケージをインストールします。

yarn add @slack/web-api

これをインストールすることで、以下のような関数でSlack Web APIを簡単に呼び出して使うことができます。

const web = new WebClient(token);
await web.chat.postMessage({
  text: 'Hello world!',
  channel: conversationId,
});

あとは、画像をアップロードするだけなのですが、画像はmultipart/form-dataで送る必要があります。(参考: https://api.slack.com/methods/files.upload)

そのため、formDataを扱いやすくするformidableをインストールしておきます。

yarn add -D formidable @types/formidable

クライアント側でユーザーが画像をアップロードするためのinputタグを設置します。

<input type="file" multiple accept="image/*" onChange={inputFileChangeHandler} />

formDataを作成して、inputFileChangeHandlerの中で受け取った画像データを格納します。

const formData = new FormData();
const inputFileChangeHandler = (
  event: React.ChangeEvent<HTMLInputElement>
) => {
  if (!event.target.files?.length) {
    return;
  }
  Array.from(event.target.files).map((file) => {
    const formData = new FormData();
    formData.append('file', file);
  });
};

テキストも一緒に送りたい場合は、テキストを更新する部分でformDataにappendしておきます。

formData.append(
  'message',
  'ここにメッセージを入れる'
);

あとはこれらをまとめて以下のように送ることができるようなapiを作成します。

await fetch('/api/slack', {
  method: 'POST',
  body: formData,
});

webアプリからSlack Web APIに画像を送り、画像を閲覧するためのpermalinkを作成する

api側では、クライアント側から送られてきた画像データを取得して画像を閲覧するためのpermalinkを発行する必要があります。
これは、Slack Web APIでは画像をメッセージと一緒に送るためにはURLが必要だからです。

Slack Web APIの機能の一つに、画像をアップロードするためのfiles.uploadがあります。

files.uploadのドキュメントを読むと、返却値にpermalinkが返っていることがわかります。

これだと画像が見にくいので、permalinkを利用して、画像をメッセージの中で表示をさせるようにします。

files.uploadはchannelを指定せずに使えばslackに投稿はされないので、それを利用して、permalinkだけを発行します。

files.uploadに画像をアップロードするために、formidableを利用して、クライアント側から送られたformDataを展開します。uploadDir: __dirnameをいれるのがミソです。

pages/api/slack.ts
  await new Promise(function (resolve, reject) {
    const form = new formidable.IncomingForm({
      keepExtensions: true,
      multiples: true,
      uploadDir: __dirname,
    });
    form.parse(req, async (err, fields, files) => {
      const message = fields.message;
      if (err) reject({ err });
      for (const key in files) {
        if (key) {
          const file: formidable.File = files[key] as formidable.File;
          const filePath = file.filepath;
          const upload = await web.files.upload({
            file: createReadStream(filePath),
          });
          if (!upload.file) {
            console.warn('Something wrong with the uploaded file!');
            return;
          }
        }
      }
      resolve({ fields, files, text });
    });
  })

uploadDir: __dirnameを指定していることで、files.uploadで画像をアップロードする際にStream型に変換する必要があるのですが、簡単にcreateReadStream(filePath)と書くだけで変換できます。

取得したpermalinkをメッセージとともにまとめてslackに投稿する

あとは取得したpermalinkをマークダウンを利用して整形します。

Formatting text for app surfacesを参考に書きます。

let text = message;
text += `<${upload.file.permalink}| >`;
web.chat.postMessage({ channel: channelId, text: data.text });

pages/api/slack.ts の全文

完成形は以下です。

pages/api/slack.ts
import { WebClient } from '@slack/web-api';
import formidable from 'formidable';
import { createReadStream } from 'fs';
import type { NextApiRequest, NextApiResponse } from 'next';

const token = process.env.SLACK_TOKEN;
const channelId = process.env.SLACK_CHANNEL_ID;
const web = new WebClient(token);

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    res.status(405).send({ message: 'Only POST requests allowed' });
    return;
  }
  if (!token) {
    console.warn('Token must not be undefined');
    res.writeHead(405).end('Token must not be undefined');
  }

  if (!channelId) {
    console.warn('ChannelId must not be undefined');
    res.writeHead(405).end('ChannelId must not be undefined');
  }

  await new Promise(function (resolve, reject) {
    const form = new formidable.IncomingForm({
      keepExtensions: true,
      multiples: true,
      uploadDir: __dirname,
    });
    form.parse(req, async (err, fields, files) => {
      const message = fields.message;
      let text = message;
      if (err) reject({ err });
      for (const key in files) {
        if (key) {
          const file: formidable.File = files[key] as formidable.File;
          const filePath = file.filepath;
          const upload = await web.files.upload({
            file: createReadStream(filePath),
          });
          if (!upload.file) {
            console.warn('Something wrong with the uploaded file!');
            return;
          }
          text += `<${upload.file.permalink}| >`;
        }
      }
      resolve({ fields, files, text });
    });
  })
    .then((data) => {
      web.chat.postMessage({ channel: channelId, text: data.text });
      res.status(200).send({ message: 'ok' });
    })
    .catch((err) => {
      res.status(500).send({ message: err });
    });
};

これで、クライアント側からこのapiを叩きたい際は以下のように書くことができます。

const inputFileChangeHandler = (
  event: React.ChangeEvent<HTMLInputElement>
) => {
  if (!event.target.files?.length) {
    return;
  }
  Array.from(event.target.files).map((file) => {
    const formData = new FormData();
    formData.append('file', file);
  });
};
const postSlack = async () => {
  formData.append(
    'message',
    'ここにメッセージを入れる'
  );
  await fetch('/api/slack', {
    method: 'POST',
    body: formData,
  });
}

あとがき

Slack Web APIで画像を投稿している例はいくつかありますが、画像をファイル添付としてではなく、メッセージと一緒に複数枚送っている例はあまり見かけなかったので書いてみました。

なにかの参考になれば嬉しいです。

Discussion