🔔

AWS Lambda + HonoでGitHub Trendingを毎朝Slack通知する

2024/05/11に公開

はじめに

OSSなどのコードを読みたくなることはありませんか?そんなとき、自分含め、GitHub Trendingで盛り上がっているリポジトリを漁る方もいらっしゃると思います。
そこで、興味のあるリポジトリに出会える機会を増やすため、毎日GitHub Trendingのリポジトリ情報をSlackに流すようにしました。

https://github.com/nashiusagi/gihub-trending-slack-bot/tree/master

機能要件

GitHub Trendingで見られるリポジトリ情報はAPI提供がされていないため、スクレイピングで抽出する必要があります。言語ごとのトレンドはhttps://github.com/trending/{言語名}?since=dailyから取ることができます。

また、今回はリポジトリ名とURLが最低限分かれば良いので、Slack通知する内容は

  • ランキング順位
  • ユーザー名/リポジトリ名
  • リポジトリURL

としています。

このような要件を踏まえた結果、アーキテクチャはこのような感じとなりました。

最初はCloudflare Workersの無料枠を使用していたのですが、

  • 制限の10msをスクレイピングのみで軽く超えてしまう
  • 運用コスト
  • Edgeを利用したくなる場面もそこまでなさそう

の3点を考慮して、AWS Lambdaに乗り換えました。
https://developers.cloudflare.com/workers/platform/pricing/
https://aws.amazon.com/jp/lambda/pricing/?p=pm&c=la&z=4

技術スタック

言語はTypeScriptで、以下の構成で作成をしています。

  • メインロジック
    • Bun
    • Hono
  • IaC
    • AWS CDK

Honoの公式ドキュメントが丁寧なので、そちらに従って準備をすることができます。
https://hono.dev/getting-started/aws-lambda

GitHub Trendingからリポジトリ情報を取得

スクレイピングでは、GitHub Trendingのページを解析して、必要な情報のみを取り出します。
ページを眺めたところ、以下の2つの方法を検討することができます。

  1. Box-rowクラスを持つarticle要素を全て取り出した後、それぞれについてh2要素内のテキストを取り出す
  2. h2要素内のテキストを全て取り出した後、"/"を含むもののみを選択する

1の手法はより頑健ではありますが、2の手法の方がより高速なため、今回は2の手法を選択しています。
コアな実装は以下のような感じとなります。

import { parse, type HTMLElement } from "node-html-parser";

const root: HTMLElement = parse(body);

const articles: Array<HTMLElement> = root.getElementsByTagName("h2");

//  h2タグにはリポジトリ名以外のものも含まれるので、それを排除する。
const dirtyRepoTitles: Array<string> =
  selectRepoNamesFromHtmlElements(articles);

const trends = await Promise.all(
  dirtyRepoTitles.map((textDirty, idx) => {
    // spanタグなど由来の\nや\sが紛れ込むので取っ払う
    const repoName = textDirty ? cleanInnerText(textDirty) : "";

    return {
      rank: idx + 1,
      title: repoName,
      link: `https://github.com/${repoName}`,
    };
  }),
);

return trends;

スクレイピング時はquerySelectorAllよりgetElementsByTagNameの方が高速なため、今回は後者を使用しています。返却するオブジェクトが異なる点は注意が必要です。
https://qiita.com/ari-chel/items/b06c68aec8849d0409dd

Slack通知

アクセストークンを使用して、スクレイピングしたデータを特定のチャンネルにポストをさせます。
アクセストークン取得とアプリの設定はこちらに倣って準備をしています。
https://zenn.dev/kou_pg_0131/articles/slack-api-post-message

実装としては、リクエストヘッダにトークンなどの情報を付加した上で、POSTリクエストを送るシンプルなものとなっています。

import { useFetch } from "./lib/useFetch";

/**
 * lang: 対象となるプログラミング言語名文字列
 * repos: スクレイピング結果をJSON化したオブジェクト
 */
const attachment: Attachment = {
  title: `GitHub Trending [ ${lang} ] `,
  text: JSON.stringify(repos),
  author_name: "GitHub Trending Feeder",
  color: "#00FF00",
};

const payload: Payload = {
  channel: slackBotTargetChannelName,
  attachments: [attachment],
};

await useFetch({
  url: "https://slack.com/api/chat.postMessage",
  options: {
    method: "POST",
    body: JSON.stringify(payload),
    headers: {
      "Content-Type": "application/json; charset=utf-8",
      Authorization: `Bearer ${slackBotToken}`,
      Accept: "application/json",
    },
  },
});

ここまでで、ローカルでスクレイピング+Slack通知までを行うことができるようになりました👍

AWS Lambdaでサーバレス化する

AWSのサーバレス環境で実行するためにLambdaを、特定の時間に実行がされるようにスケジューリングするためにEventBridgeが必要になります。
スケジューリングはcron式で、毎朝9時(UTCで0:00)に設定します。

// Lambda
const fn = new NodejsFunction(this, "lambda", {
  entry: "lambda/index.ts",
  handler: "handler",
  runtime: lambda.Runtime.NODEJS_20_X,
});
fn.addFunctionUrl({
  authType: lambda.FunctionUrlAuthType.NONE,
});
new apigw.LambdaRestApi(this, "myapi", {
  handler: fn,
});

// Eventbridge rules
new Rule(this, "schedule-cron-github-trends", {
  schedule: Schedule.cron({ minute: "0", hour: "0" }),
  targets: [new LambdaFunction(fn)],
});

以上で、毎朝GitHub Trendingのリポジトリ情報がSlack通知されるようになりました!✨

最後に

低コストでGitHub Trendingを追えるようになりました!🙌
Honoは初めて扱ったのですが、書きやすさに驚かされました🔥

GitHubで編集を提案

Discussion