📺

🔥Hono + Cloudflare Workersで、テレビ番組を探してSlackに通知するbotを作った

2024/09/18に公開

Hono + CloudFlare Workersを試したく、1日1度テレビの番組表をスクレイピングして気になるワードに関する番組を探してくれるボットを作ってみました。

takara1356/tvshow-title-watcher: 当日のテレビ番組を特定のキーワードから検索し、Slack ワークフローに通知するアプリ

このような感じで通知してくれます。
あまりにもサクッと実装できて感動したので、簡単な実装の流れをメモがてら残しておきます。

事前準備

SlackワークフローのWebhook設定

Slackのワークフローを新規作成し、任意の文字列(本記事ではdetail)を受け取れるWebhookのエンドポイントを生やしておきます。

実装手順

Init Hono

  • Init
npm create hono@latest tvshow-title-watcher
  • テンプレートを聞かれるので、Cloudflare Workersを選択

ロジック実装

  • ChatGPTを使って最低限のコードでサクッと実装しました
import { Hono } from "hono";
import * as cheerio from "cheerio";

const app = new Hono();

// 作成したSlackワークフローのWebhook URLを設定する
const SLACK_WORKFLOW_URL = "";

// 検索キーワードを設定する
const KEYWORD = "テクノロジー";

const formatStartTime = (startTime: string): string => {
  const year = startTime.slice(0, 4);
  const month = startTime.slice(4, 6);
  const day = startTime.slice(6, 8);
  const hour = startTime.slice(8, 10);
  const minute = startTime.slice(10, 12);
  return `${year}/${month}/${day} ${hour}:${minute}`;
};

const sendSlackNotification = async (message: string) => {
  try {
    await fetch(SLACK_WORKFLOW_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ detail: message }),
    });
  } catch (error) {
    console.error("Slackへのリクエスト送信に失敗しました:", error);
  }
};

const scrapeTVProgram = async (keyword: string): Promise<string[]> => {
  const today = new Date().toISOString().split("T")[0].replace(/-/g, "");
  const URL = `https://bangumi.org/epg/td?broad_cast_date=${today}&ggm_group_id=42`;

  try {
    const response = await fetch(URL);
    const html = await response.text();
    const $ = cheerio.load(html);

    const matchedPrograms: string[] = [];
    $("li").each((_, el) => {
      const title = $(el).find(".program_title").text();
      const detail = $(el).find(".program_detail").text();
      const startTimeRaw = $(el).attr("s");

      if (startTimeRaw) {
        const startTime = formatStartTime(startTimeRaw);

        if (title.includes(keyword) || detail.includes(keyword)) {
          matchedPrograms.push(
            `🕒 開始時間: ${startTime} 🕒\n🎬 タイトル: ${title} 🎬\n📄 内容: ${detail} 📄\n`
          );
        }
      }
    });
    return matchedPrograms;
  } catch (error) {
    console.error("番組表のスクレイピングに失敗しました:", error);
    return [];
  }
};

// サーバーとして起動する場合はこちらを有効化する
// app.fire();

export default {
  async scheduled(event: ScheduledEvent) {
    const matchedPrograms = await scrapeTVProgram(KEYWORD);

    if (matchedPrograms.length > 0) {
      const message = `🔥 本日、${KEYWORD}に関連する番組が放送されます! 🔥\n\n${matchedPrograms.join(
        "\n"
      )}\nお見逃しなく!🚀`;
      await sendSlackNotification(message);
    } else {
      const message = `😞 本日は「${KEYWORD}」に関連する番組が見つかりませんでした。`;
      await sendSlackNotification(message);
    }
  },
};

実装のポイント

Cloudflare Workersにデプロイしたアプリをcronで動かす場合、2つ設定が必要です。

1.scheduledイベントリスナーの定義
コード内でcronトリガーの発火を検知するリスナー用のメソッドを追加します。

export default {
  async scheduled(event, env, ctx) {
    ctx.waitUntil(doSomeTaskOnASchedule());
  },
};

2.wrangler.tomlへの設定追加

下記のように追記すると、デプロイ時にトリガーの設定が反映されます。

[triggers]
crons = ["0 0 * * *"]

デプロイ

  • npm run deployコマンドでデプロイが走ります。
  • CLIからブラウザ経由でログインだけしたらすぐにデプロイ可能です。

感想

  • メインで使っている言語がRubyのためJS/TSは詳しくなかったのですが、Hono+Cloudflare functionsの環境構築がとにかく楽!

  • おかげで環境構築やデプロイ周りで時間を浪費せず、メインのロジック実装に集中でき、ChatGPTも使うことでトータル10時間以下で実装できました

  • 次回は同じくエッジの環境で動作するCloudflare PagesやD1も試してみたいです🔥

LCL Engineers

Discussion