Published on

Puppeteer × Discord:レポートのスクリーンショットを送信する

Authors
  • avatar
    Name
    Nomad Dev Life
    Twitter
2025-01-18-title

ひとりSlackをDiscordに移行した

ひとりSlack愛用者でしたが、Free Planだと90日しかメッセージが表示できなくなってしまったので、Discordに移行しました。

せっかくDiscordに移行したので、これまで作成していたインフルエンザのレポートをDiscordに送信するコードを書いてみました。

やること

今回やることは、以下の3点です。

  1. Discordの送信先チャンネルにWebhookを作成する
  2. Puppeteerでインフルエンザのページのスクリーンショットを撮る
  3. 撮ったスクリーンショットをDiscordのWebhookのURLに送信する

Webhookを作成する

Discordの送信先チャンネルにWebhookを作成します。送信先チャネルを選んだら、左ペインのチャンネル名の横にある"チャンネルの編集"→"連携サービス"→"ウェブフック"をクリックします。 そして"新しいウェブフック"をクリックし、"お名前"を設定して、"ウェブフックURLをコピー"をクリックして、URLを控えておきます。

2025-01-13-image

スクリーンショットを撮り、Discordに送信する

Puppeteerでインフルエンザのページのスクリーンショットを撮り、DiscordのWebhook URLに送信します。コードは、TypeScriptで書きました。

  • package.json
{
  "name": "image-to-discord",
  "version": "1.0.0",
  "main": "index.js",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "form-data": "^4.0.1",
    "node-fetch": "^2.7.0",
    "puppeteer": "^24.1.0"
  },
  "devDependencies": {
    "@types/node": "^22.10.7",
    "@types/node-fetch": "^2.6.12",
    "typescript": "^5.7.3"
  },
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
  • index.ts
import puppeteer, { Browser, Page } from 'puppeteer';
import fetch from 'node-fetch';
import FormData from 'form-data';
import { promises as fs } from 'fs';
import { Blob } from 'node:buffer';


async function sendImageToDiscord(
  url: string,
  chartSelectorForWait: string,
  chartSelectorCapture: string,
  outputImage: string,
  discordWebhookURL: string
): Promise<void> {
  let browser: Browser | null = null;
  try {
    browser = await puppeteer.launch();
    const page: Page = await browser.newPage();

    await page.setViewport({
      width: 1600,
      height: 900,
    });

    // 1. ページを表示
    await page.goto(url);

    // 2. チャートが表示されるまで待機
    await page.waitForSelector(chartSelectorForWait);

    // 3. チャートをキャプチャ
    const element = await page.$(chartSelectorCapture);
    if (!element) {
      throw new Error(`Chart element not found with selector: ${chartSelectorCapture}`);
    }
    await element.screenshot({ path: outputImage });

    // 4. Discordに画像を送信
    const formData = new FormData();
    const fileBuffer = await fs.readFile(outputImage);

    const blob = new Blob([fileBuffer]);
    const buffer = await blob.arrayBuffer();
    formData.append('file', Buffer.from(buffer), outputImage);

    formData.append('content', '2024-25年 インフルエンザ報告数');
    
    const response = await fetch(discordWebhookURL, {
        method: 'POST',
        body: formData
      });

    if (!response.ok) {
      throw new Error(`Failed to send message: ${response.status} ${response.statusText}`);
    }

    console.log(`${outputImage} sent to Discord.`);

  } catch (err: any) {
      console.error(err);
  } finally{
    if (browser){
      await browser.close();
    }
  }
}

(async () => {
  const pageURL = 'https://flu.nomad-dev-life.net/flu_2024.html';
  const discordWebhookURL = '...'; // Discord Webhook URL

  try{
      await sendImageToDiscord(pageURL, '.chart-wrapper', 'html', 'flu.png', discordWebhookURL);
  } catch (err){
    console.error(err)
  }
})();

インフルエンザのレポートはstliteというwasmの仕組みを使っています。今回スクショを撮る際に、wasmが出力するHTMLのDOM(.chart-wrapper)をチェックしてチャートが全部表示されたことを確認した後、スクショを撮るようにしています。

また、Viewportを1600x900で設定しています。この設定が無いと、スクショの画像が途中で切れてしまいます。

あとはnpm run buildでビルドし、npm startで実行することで、Discordのチャンネルに以下のように表示されます。

2025-01-13-image

おわりに

今回は、インフルエンザのレポートのスクショをDiscordのチャンネルに送信しました。あとは、前回のようにGithub Actionに仕込んで定期的に実行されるようにしておけば良いでしょう。

レポートのスクショが送られてくるようになっていると、レポートの内容が大きく変わっていない限り、ブラウザでページを開いて見に行かなくて良いので楽です。