Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Slackbot を構築する

最終更新 Markdown で表示Agent セットアップ

このチュートリアルでは、Cloudflare Workers を使って Slack ボットを構築します。ボットは GitHub webhook を使い、Issue が更新または作成されたときに Slack チャンネルへメッセージを送ります。また、Slack 内からコマンドを書いて GitHub Issue を検索できます。

このチュートリアルを終えると、この例のような Slackbot を作成できます。続けて読み、Slackbot を構築してください。

このチュートリアルは、Web アプリケーションの作成に慣れている人向けです。プログラミング言語は TypeScript、Web フレームワークは Hono を使います。NodeExpress でアプリケーションを作ったことがあれば、このプロジェクトはなじみやすいです。Web アプリケーションが初めての場合や、以前 Slack ボットのようなものを作りたかったがデプロイや設定が負担だった場合、Workers ならコードを書いてプロジェクトを公開することに集中できます。

先にコードや、実際の Slack チャンネルでのボットの動きを確認したい場合は、完成版のコードベースを GitHub で参照できます。GitHub から、自分の Slack API キーを追加し、テスト用の Slack チャンネルへデプロイできます。


始める前に

すべてのチュートリアルは、Cloudflare Workers アカウント、C3、および Wrangler のセットアップが完了している前提です。セットアップは Get started ガイド で行います。

Slack を設定する

このチュートリアルでは、すでに Slack アカウントがあり、Slack アプリケーションを作成・管理できることを前提とします。

Slack アプリケーションを設定する

Cloudflare Worker から Slack チャンネルへメッセージを投稿するには、Slack の UI でアプリケーションを作成する必要があります。Slack の API セクション api.slack.com/apps を開き、Create New App を選びます。

Slackbot を作るには、まず Slack App を作成します

Slack アプリケーションには多くの機能があります。Workers で動く Slack ボットを作るために、Incoming Webhooks と Slash Commands の 2 つを使います。

Incoming Webhook

Incoming Webhooks は、Slack チャンネルへメッセージを送るために使える URL です。Incoming Webhook を GitHub の webhook と組み合わせ、指定したリポジトリの Issue に更新があったときに Slack チャンネルへメッセージを送ります。アプリケーションを組み立てるなかで、コードをより詳しく見ます。まず Slack webhook を作成します。

  1. Slack の UI のサイドバーで Incoming Webhooks を選びます。
  2. Webhook URLs for your WorkspaceAdd New Webhook to Workspace を選びます。
  3. 次の画面で、webhook がメッセージを送るチャンネルを選びます(#general や #code などのルーム、または webhook 呼び出し時に Slack ボットから直接メッセージを受け取ることもできます)。
  4. 新しい webhook URL を承認します。

webhook URL を承認すると Incoming Webhooks ページに戻り、新しい webhook URL を確認できます。あとで Workers のコードに追加します。次に、Slack ボットの 2 つ目の要素である Slash Command を追加します。

Slack のダッシュボードで Add New Webhook to Workspace を選び、新しい Webhook URL を追加します

Slash Command

Slack の Slash Command は、URL リクエストに紐づけられるカスタムコマンドです。たとえば /weather <zip> を設定すると、Slack は設定した URL へ HTTP POST リクエストを送り、指定した郵便番号の天気を得るためにテキスト <zip> を渡します。このアプリケーションでは、GitHub API で GitHub Issue を検索するために /issue コマンドを使います。/issue cloudflare/wrangler#1 と入力すると、テキスト cloudflare/wrangler#1 が HTTP POST リクエストでアプリケーションに送られ、アプリケーションはそれを使って 該当する GitHub Issue を探します。

  1. Slack のサイドバーで Slash Commands を選びます。
  2. 最初のスラッシュコマンドを作成します。

このチュートリアルではコマンド /issue を使います。Request URL は、アプリケーション URL の /lookup パスにしてください。たとえばアプリケーションを https://myworkerurl.com でホストする場合、Request URL は https://myworkerurl.com/lookup です。

Slack のダッシュボードで Slash Command を作成し、Request URL に紐づけます

GitHub Webhooks を設定する

Cloudflare Workers アプリケーションは、Slack からの受信リクエストを処理できます。加えて、GitHub から直接イベントを受け取れるようにします。GitHub Issue が作成または更新されたとき、GitHub webhook を使ってそのイベントを Workers アプリケーションへ送り、Slack に対応するメッセージを投稿できます。

webhook を設定する手順は次のとおりです。

  1. GitHub リポジトリの Settings > Webhooks > Add webhook を開きます。

https://github.com/user/repo のようなリポジトリなら、Webhooks ページは https://github.com/user/repo/settings/hooks から直接開けます。

  1. Payload URL を、Worker URL の /webhook パスに設定します。

たとえば Worker を https://myworkerurl.com でホストする場合、Payload URL は https://myworkerurl.com/webhook です。

  1. Content type のドロップダウンで application/json を選びます。

ペイロードの Content type は、URL エンコード(application/x-www-form-urlencoded)または JSON(application/json)です。このチュートリアルでは、アプリケーションへ送られるペイロードの解析を簡単にするため、JSON を選びます。

  1. Which events would you like to trigger this webhook?Let me select individual events を選びます。

GitHub webhook では、webhook へ送るイベントを指定できます。既定では、リポジトリの push イベントが送られます。このチュートリアルでは Let me select individual events を選びます。

  1. Issues イベントタイプを選びます。

webhook で有効にできるイベントタイプは多数あります。Issues を選ぶと、Issue のオープン、編集、削除などを含め、Issue 関連のすべてのイベントが webhook へ送られます。このチュートリアルのあとで Slack ボットを拡張したい場合は、ほかのイベントも選べます。

  1. Add webhook を選びます。
GitHub ダッシュボードで GitHub Webhook を作成します

webhook を作成すると、アプリケーションへテストペイロードを送ろうとします。まだデプロイしていないので、設定はそのままにしておきます。デプロイ後に webhook が動くことを確認するため、あとでリポジトリに戻って Issue の作成、編集、クローズを行います。

初期化

プロジェクトを始めるには、コマンドラインインターフェース C3 (create-cloudflare-cli) を使います。

npm create cloudflare@latest -- slack-bot

次の手順で Hono プロジェクトを作成します。

  • What would you like to start with では、Framework Starter を選びます。
  • Which development framework do you want to use? では、Hono を選びます。
  • Do you want to deploy your application? では、No を選びます。

slack-bot ディレクトリへ移動します。

cd slack-bot

エディターで src/index.ts を開くと、次のコードがあります。

import { Hono } from "hono";

type Bindings = {
	[key in keyof CloudflareBindings]: CloudflareBindings[key];
};

const app = new Hono<{ Bindings: Bindings }>();

app.get("/", (c) => {
	return c.text("Hello Hono!");
});

export default app;

これは Hono を使った最小のアプリケーションです。パス / へ GET が来ると、テキスト Hello Hono! のレスポンスを返します。ほかのパスやメソッドへアクセスすると、ステータスコード 404 のメッセージ 404 Not Found を返します。

ローカルマシンでアプリケーションを動かすには、次のコマンドを実行します。

npm i -- dev

サーバー起動後、ブラウザーで http://localhost:8787 を開くと、メッセージを確認できます。

Hono を使うと、Workers アプリケーションを簡単かつすばやく作れます。

構築する

ここから、Cloudflare Workers 上に Slack ボットを作ります。

ファイルを分ける

エンドポイントと関数を 1 ファイルに書かず、複数ファイルでアプリケーションを作れます。Hono では、app.route() 関数で子アプリケーションのルーティングを親アプリケーションへ追加できます。

たとえば、次のような Web API アプリケーションを考えます。

import { Hono } from "hono";

const app = new Hono();

app.get("/posts", (c) => c.text("Posts!"));
app.post("/posts", (c) => c.text("Created!", 201));

export default app;

ルートを /api/v1 の下に追加できます。

import { Hono } from "hono";
import api from "./api";

const app = new Hono();

app.route("/api/v1", api);

export default app;

GET /api/v1/posts へアクセスすると Posts! が返ります。

Slack ボットには、「ルート」と呼ぶ子アプリケーションが 2 つあります。

  1. lookup ルートは、ユーザーが /issue コマンドを使ったときに Slack から送られるリクエストを受け取り、GitHub API で対応する Issue を検索します。このアプリケーションは、メインアプリケーションの /lookup に追加します。

  2. webhook ルートは、設定した webhook 経由で GitHub 上の Issue が変わったときに呼び出されます。このアプリケーションは、メインアプリケーションの /webhook に追加します。

routes というディレクトリにルートファイルを作成します。

フォルダーとファイルを作成するsh
mkdir -p src/routes
touch src/routes/lookup.ts
touch src/routes/webhook.ts

次にメインアプリケーションを更新します。

import { Hono } from "hono";
import lookup from "./routes/lookup";
import webhook from "./routes/webhook";

const app = new Hono();

app.route("/lookup", lookup);
app.route("/webhook", webhook);

export default app;

TypeScript の型を定義する

実際の関数を実装する前に、このプロジェクトで使う TypeScript の型を定義します。アプリケーション内に src/types.ts を新規作成し、コードを書きます。Bindings は Cloudflare Workers の環境変数を表す型です。Issue は GitHub Issue の型、User は GitHub ユーザーの型です。あとで使います。

export type Bindings = {
	SLACK_WEBHOOK_URL: string;
};

export type Issue = {
	html_url: string;
	title: string;
	body: string;
	state: string;
	created_at: string;
	number: number;
	user: User;
};

type User = {
	html_url: string;
	login: string;
	avatar_url: string;
};

lookup ルートを作成する

src/routes/lookup.ts で lookup ルートの作成を始めます。

import { Hono } from "hono";

const app = new Hono();

export default app;

この関数の設計を理解するには、Slack のスラッシュコマンドが URL へデータを送る方法を知る必要があります。

Slack スラッシュコマンドのドキュメント によると、Slack は指定した URL へ、application/x-www-form-urlencoded の Content-Type で HTTP POST リクエストを送ります。たとえば誰かが /issue cloudflare/wrangler#1 と入力した場合、次の形式のデータペイロードが想定されます。

token=gIkuvaNzQIHg97ATvDxqgjtO
&team_id=T0001
&team_domain=example
&enterprise_id=E0001
&enterprise_name=Globular%20Construct%20Inc
&channel_id=C2147483705
&channel_name=test
&user_id=U2147483697
&user_name=Steve
&command=/issue
&text=cloudflare/wrangler#1
&response_url=https://hooks.slack.com/commands/1234/5678
&trigger_id=13345224609.738474920.8088930838d88f008e0

このペイロード本体を解析し、text キーの値を取得する必要があります。その text(例: cloudflare/wrangler#1)を既知のデータ(ownerrepoissue_number)に分解し、GitHub API へリクエストして Issue データを取得します。

Slack のスラッシュコマンドでは、受信したスラッシュコマンドへのレスポンスとして構造化データを返すことで応答できます。ここでは GitHub API のレスポンスを使い、Issue のタイトル、作成者、作成日などのデータを含む、整形した GitHub Issue を提示します。Slack の Block Kit フレームワークを使うと、GitHub API のデータからテキストブロックと画像ブロックを組み立て、詳細なメッセージレスポンスを返せます。

スラッシュコマンドを解析する

まず、lookup ルートは Slack から来るメッセージを解析します。前述のとおり、Slack API は URL エンコード形式で HTTP POST を送ります。c.req.json() で解析すると、変数 text を取得できます。

import { Hono } from "hono";

const app = new Hono();

app.post("/", async (c) => {
	const { text } = await c.req.parseBody();
	if (typeof text !== "string") {
		return c.notFound();
	}
});

export default app;

cloudflare/wrangler#1 のようなテキストを含む text 変数があるので、そのテキストを解析し、GitHub API で使う個別の部分(ownerrepoissue_number)を取り出します。

そのために、アプリケーション内に src/utils/github.ts を新規作成します。このファイルには、GitHub API 向けの「ユーティリティ」関数をいくつか置きます。最初の関数は、parseGhIssueString という文字列パーサーです。

const ghIssueRegex =
	/(?<owner>[\w.-]*)\/(?<repo>[\w.-]*)\#(?<issue_number>\d*)/;

export const parseGhIssueString = (text: string) => {
	const match = text.match(ghIssueRegex);
	return match ? (match.groups ?? {}) : {};
};

parseGhIssueStringtext 入力を受け取り、ghIssueRegex と照合します。一致した場合は、正規表現で定義した ownerrepoissue_number キャプチャグループを使い、その一致の groups オブジェクトを返します。この関数を src/utils/github.ts からエクスポートすると、src/handlers/lookup.ts で使えます。

import { Hono } from "hono";
import { parseGhIssueString } from "../utils/github";

const app = new Hono();

app.post("/", async (c) => {
	const { text } = await c.req.parseBody();
	if (typeof text !== "string") {
		return c.notFound();
	}

	const { owner, repo, issue_number } = parseGhIssueString(text);
});

export default app;

GitHub API へリクエストする

このデータを使い、GitHub への最初の API 参照ができます。再び src/utils/github.ts に関数を追加し、Issue データのために GitHub API へ fetch リクエストします。

const ghIssueRegex =
	/(?<owner>[\w.-]*)\/(?<repo>[\w.-]*)\#(?<issue_number>\d*)/;

export const parseGhIssueString = (text: string) => {
	const match = text.match(ghIssueRegex);
	return match ? (match.groups ?? {}) : {};
};

export const fetchGithubIssue = (
	owner: string,
	repo: string,
	issue_number: string,
) => {
	const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`;
	const headers = { "User-Agent": "simple-worker-slack-bot" };
	return fetch(url, { headers });
};

src/handlers/lookup.ts に戻り、fetchGitHubIssue で GitHub API へリクエストし、レスポンスを解析します。

import { Hono } from "hono";
import { fetchGithubIssue, parseGhIssueString } from "../utils/github";
import { Issue } from "../types";

const app = new Hono();

app.post("/", async (c) => {
	const { text } = await c.req.parseBody();
	if (typeof text !== "string") {
		return c.notFound();
	}

	const { owner, repo, issue_number } = parseGhIssueString(text);
	const response = await fetchGithubIssue(owner, repo, issue_number);
	const issue = await response.json<Issue>();
});

export default app;

Slack メッセージを組み立てる

GitHub API からレスポンスを受け取ったあと、最後のステップは Issue データで Slack メッセージを組み立て、ユーザーへ返すことです。完成形は次のようになります。

成功した Slack メッセージには、次に挙げる要素があります

上記のスクリーンショットには 4 つの部分があります。

  1. 最初の行(太字)は Issue へリンクし、Issue タイトルを表示します
  2. 続く行(コードスニペットを含む)は Issue 本文です
  3. 最後のテキスト行は、Issue の状態、Issue 作成者(ユーザーの GitHub プロフィールへのリンク付き)、Issue の作成日を表示します
  4. 右側は Issue 作成者のプロフィール画像です

前述の Block Kit フレームワークは、Issue データ(GitHub の REST API ドキュメント に示された構造)を受け取り、上記のスクリーンショットのような形式にします。

別ファイル src/utils/slack.ts を作成し、関数 constructGhIssueSlackMessage を置きます。Issue データを受け取り、ブロックの集まりへ変換する関数です。ブロックは、Slack がメッセージの書式に使う JavaScript オブジェクトです。

import { Issue } from "../types";

export const constructGhIssueSlackMessage = (
	issue: Issue,
	issue_string: string,
	prefix_text?: string,
) => {
	const issue_link = `<${issue.html_url}|${issue_string}>`;
	const user_link = `<${issue.user.html_url}|${issue.user.login}>`;
	const date = new Date(Date.parse(issue.created_at)).toLocaleDateString();

	const text_lines = [
		prefix_text,
		`*${issue.title} - ${issue_link}*`,
		issue.body,
		`*${issue.state}* - Created by ${user_link} on ${date}`,
	];
};

Slack メッセージは Markdown の一種を受け付けます。太字はアスタリスク(*bolded text*)、リンクは <https://yoururl.com|Display Text> 形式です。

この形式を使い、issue_link を組み立てます。GitHub API の issue データにある html_url プロパティ(形式は https://github.com/cloudflare/wrangler-legacy/issues/1)と、Slack スラッシュコマンドから送られた issue_string を組み合わせ、Slack メッセージ内のクリック可能なリンクにします。

user_link も同様です。issue.user.html_url(形式は https://github.com/signalnerve のような GitHub ユーザー)とユーザーの GitHub ユーザー名(issue.user.login)を使い、GitHub ユーザーへのクリック可能なリンクを組み立てます。

最後に、ISO 8601 文字列である issue.created_at を解析し、JavaScript の Date インスタンスへ変換して、MM/DD/YY 形式の文字列にします。

これらの変数が揃ったら、text_lines は Slack メッセージの各行の配列です。1 行目は Issue タイトルIssue リンク、2 行目は Issue 本文、最後の行は Issue の状態(open や closed など)、ユーザーリンク作成日 です。

テキストができたので、Slack メッセージを組み立て、Slack の Block Kit 向けにブロックの配列を返します。この場合、ブロックは 1 つだけです。Markdown テキストを持つ section ブロックと、Issue 作成者の画像アクセサリーです。その 1 ブロックを配列に入れて返し、constructGhIssueSlackMessage 関数を完成させます。

import { Issue } from "../types";

export const constructGhIssueSlackMessage = (
	issue: Issue,
	issue_string: string,
	prefix_text?: string,
) => {
	const issue_link = `<${issue.html_url}|${issue_string}>`;
	const user_link = `<${issue.user.html_url}|${issue.user.login}>`;
	const date = new Date(Date.parse(issue.created_at)).toLocaleDateString();

	const text_lines = [
		prefix_text,
		`*${issue.title} - ${issue_link}*`,
		issue.body,
		`*${issue.state}* - Created by ${user_link} on ${date}`,
	];

	return [
		{
			type: "section",
			text: {
				type: "mrkdwn",
				text: text_lines.join("\n"),
			},
			accessory: {
				type: "image",
				image_url: issue.user.avatar_url,
				alt_text: issue.user.login,
			},
		},
	];
};

lookup ルートを完成させる

src/handlers/lookup.tsconstructGhIssueSlackMessage を使い blocks を組み立て、スラッシュコマンドが呼ばれたときに c.json() で新しいレスポンスとして返します。

import { Hono } from "hono";
import { fetchGithubIssue, parseGhIssueString } from "../utils/github";
import { constructGhIssueSlackMessage } from "../utils/slack";
import { Issue } from "../types";

const app = new Hono();

app.post("/", async (c) => {
	const { text } = await c.req.parseBody();
	if (typeof text !== "string") {
		return c.notFound();
	}

	const { owner, repo, issue_number } = parseGhIssueString(text);
	const response = await fetchGithubIssue(owner, repo, issue_number);
	const issue = await response.json<Issue>();
	const blocks = constructGhIssueSlackMessage(issue, text);

	return c.json({
		blocks,
		response_type: "in_channel",
	});
});

export default app;

レスポンスに渡す追加パラメーターが response_type です。既定では、スラッシュコマンドへのレスポンスは ephemeral です。コマンドを書いたユーザーにだけ見えます。上記のように response_typein_channel にすると、チャンネル内のすべてのユーザーにレスポンスが表示されます。

メッセージを非公開のままにしたい場合は、response_type の行を削除します。response_type は既定の ephemeral になります。

エラーを扱う

lookup ルートはほぼ完成していますが、Slack からの本文解析、GitHub からの Issue 取得、Slack メッセージ自体の組み立てなど、ルート内で起きうるエラーはいくつかあります。Hono アプリケーションは何もしなくてもエラーを扱えますが、返すレスポンスは次のようにカスタマイズできます。

import { Hono } from "hono";
import { fetchGithubIssue, parseGhIssueString } from "../utils/github";
import { constructGhIssueSlackMessage } from "../utils/slack";
import { Issue } from "../types";

const app = new Hono();

app.post("/", async (c) => {
	const { text } = await c.req.parseBody();
	if (typeof text !== "string") {
		return c.notFound();
	}

	const { owner, repo, issue_number } = parseGhIssueString(text);
	const response = await fetchGithubIssue(owner, repo, issue_number);
	const issue = await response.json<Issue>();
	const blocks = constructGhIssueSlackMessage(issue, text);

	return c.json({
		blocks,
		response_type: "in_channel",
	});
});

app.onError((_e, c) => {
	return c.text(
		"Uh-oh! We couldn't find the issue you provided. " +
			"We can only find public issues in the following format: `owner/repo#issue_number`.",
	);
});

export default app;

webhook ルートを作成する

Workers アプリケーションのルート実装は、ここで半分まで来ています。次のルート src/routes/webhook.ts では、lookup ルートですでに書いたコードの多くを再利用します。

このチュートリアルの冒頭で、リポジトリの Issue 関連イベントを追跡する GitHub webhook を設定しました。たとえば Issue がオープンされたとき、Workers アプリケーションのパス /webhook に対応する関数は、GitHub から送られたデータを受け取り、設定した Slack チャンネルへ新しいメッセージを投稿します。

src/routes/webhook.ts に、空の Hono アプリケーションを定義します。lookup ルートとの違いは、new Hono() のジェネリクスとして Bindings を渡すことです。あとで使う SLACK_WEBHOOK_URL に適切な TypeScript の型を付けるために必要です。

import { Hono } from "hono";
import { Bindings } from "../types";

const app = new Hono<{ Bindings: Bindings }>();

export default app;

lookup ルートと同様に、request 内の受信ペイロードを解析し、関連する Issue データを取り出します(ペイロードのスキーマ全体は GitHub API の IssueEvent ドキュメント を参照してください)。そのうえで、何が変わったかを示す整形済みメッセージを Slack へ送ります。完成形は次のようになります。

成功した Webhook メッセージの例

このメッセージ形式を、ユーザーが /issue スラッシュコマンドを使ったときの形式と比べます。実際の違いは 1 つだけです。最初の行に、An issue was $action: 形式のアクションテキストが加わることです。このアクションは GitHub の IssueEvent の一部として送られ、Slack の Block Kit でよく似たブロックの集まりを組み立てるときに使います。

イベントデータを解析する

ルートを埋めていく最初のステップとして、JSON 形式のリクエスト本文をオブジェクトへ解析し、いくつかのヘルパー変数を組み立てます。

import { Hono } from "hono";
import { constructGhIssueSlackMessage } from "../utils/slack";

const app = new Hono();

app.post("/", async (c) => {
	const { action, issue, repository } = await c.req.json();
	const prefix_text = `An issue was ${action}:`;
	const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`;
});

export default app;

webhook 設定の一部として GitHub から送られるペイロード IssueEvent には、action(Issue に何が起きたか。たとえば opened、closed、locked など)、issue 自体、repository などが含まれます。

c.req.json() でリクエストのペイロード本文を JSON からプレーンな JS オブジェクトへ変換します。ES6 の分割代入で、actionissuerepository をコード内で使える変数にします。prefix_text は Issue に何が起きたかを示す文字列、issue_string はこれまで見てきた owner/repo#issue_number です。lookup ルートは Slack から送られたテキストをそのまま issue_string に使いましたが、ここでは JSON ペイロードのデータを基に直接組み立てます。

Slack メッセージを組み立てて送る

Slack ボットが lookupwebhook の各ルートから Slack チャンネルへ送るメッセージは、とてもよく似ています。そのため、既存の constructGhIssueSlackMessage を再利用して src/handlers/webhook.ts を埋めていけます。src/utils/slack.ts から関数をインポートし、Issue データを渡します。

import { Hono } from "hono";
import { constructGhIssueSlackMessage } from "../utils/slack";

const app = new Hono();

app.post("/", async (c) => {
	const { action, issue, repository } = await c.req.json();
	const prefix_text = `An issue was ${action}:`;
	const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`;
	const blocks = constructGhIssueSlackMessage(issue, issue_string, prefix_text);
});

export default app;

重要なのは、このハンドラーでの constructGhIssueSlackMessage の使い方に、追加引数 prefix_text があることです。src/utils/slack.ts 内の対応する関数を更新し、関数へ渡された場合はメッセージブロックの text_linesprefix_text を追加します。

ユーティリティ関数 compact を追加します。配列を受け取り、nullundefined の値を取り除きます。src/handlers/lookup.ts から呼ばれたときのように、prefix_text が実際には渡されていない場合に、text_lines から取り除くために使います。src/utils/slack.ts の完成版(最終版)は次のとおりです。

import { Issue } from "../types";

const compact = (array: unknown[]) => array.filter((el) => el);

export const constructGhIssueSlackMessage = (
	issue: Issue,
	issue_string: string,
	prefix_text?: string,
) => {
	const issue_link = `<${issue.html_url}|${issue_string}>`;
	const user_link = `<${issue.user.html_url}|${issue.user.login}>`;
	const date = new Date(Date.parse(issue.created_at)).toLocaleDateString();

	const text_lines = [
		prefix_text,
		`*${issue.title} - ${issue_link}*`,
		issue.body,
		`*${issue.state}* - Created by ${user_link} on ${date}`,
	];

	return [
		{
			type: "section",
			text: {
				type: "mrkdwn",
				text: compact(text_lines).join("\n"),
			},
			accessory: {
				type: "image",
				image_url: issue.user.avatar_url,
				alt_text: issue.user.login,
			},
		},
	];
};

src/handlers/webhook.ts に戻り、constructGhIssueSlackMessage から返った blocks を、Slack webhook URL への新しい fetch リクエスト(HTTP POST)の本文にします。そのリクエストが完了したら、ステータスコード 200 と本文テキスト "OK" のレスポンスを返します。

import { Hono } from "hono";
import { constructGhIssueSlackMessage } from "../utils/slack";
import { Bindings } from "../types";

const app = new Hono<{ Bindings: Bindings }>();

app.post("/", async (c) => {
	const { action, issue, repository } = await c.req.json();
	const prefix_text = `An issue was ${action}:`;
	const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`;
	const blocks = constructGhIssueSlackMessage(issue, issue_string, prefix_text);

	const fetchResponse = await fetch(c.env.SLACK_WEBHOOK_URL, {
		body: JSON.stringify({ blocks }),
		method: "POST",
		headers: { "Content-Type": "application/json" },
	});

	return c.text("OK");
});

export default app;

定数 SLACK_WEBHOOK_URL は、このチュートリアルの Incoming Webhook の節で作成した Slack Webhook URL を表します。

コードベース内でこの定数を使うには、wrangler secret コマンドを使います。

SLACK_WEBHOOK_URL シークレットを設定するsh
npx wrangler secret put SLACK_WEBHOOK_URL
Enter a secret value: https://hooks.slack.com/services/abc123

エラーを扱う

lookup ルートと同様に、webhook ルートにも基本的なエラー処理を入れます。lookup と違い、レスポンスは Slack へ直接送りません。webhook で問題が起きたときは、エラーレスポンスを生成して GitHub へ返すと役立つことがあります。

そのために app.onError() でカスタムエラーハンドラーを書き、ステータスコード 500 の新しいレスポンスを返します。src/routes/webhook.ts の最終版は次のとおりです。

import { Hono } from "hono";
import { constructGhIssueSlackMessage } from "../utils/slack";
import { Bindings } from "../types";

const app = new Hono<{ Bindings: Bindings }>();

app.post("/", async (c) => {
	const { action, issue, repository } = await c.req.json();
	const prefix_text = `An issue was ${action}:`;
	const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`;
	const blocks = constructGhIssueSlackMessage(issue, issue_string, prefix_text);

	const fetchResponse = await fetch(c.env.SLACK_WEBHOOK_URL, {
		body: JSON.stringify({ blocks }),
		method: "POST",
		headers: { "Content-Type": "application/json" },
	});

	if (!fetchResponse.ok) throw new Error();

	return c.text("OK");
});

app.onError((_e, c) => {
	return c.json(
		{
			message: "Unable to handle webhook",
		},
		500,
	);
});

export default app;

デプロイする

ここまでの手順で、Slack ボットのコードは完成です。アプリケーションをデプロイできます。

Wrangler は、Cloudflare Workers アプリケーションのバンドル、アップロード、リリースを標準でサポートしています。次のコマンドを実行すると、コードがビルドされデプロイされます。

npm i -- deploy

Workers アプリケーションをデプロイすると、GitHub webhook が Workers の webhook ルートへ届くようになり、Issue の更新が Slack チャンネルに表示され始めます。

新しい Issue を作成すると、Slack チャンネルに Slackbot が表示されます

関連リソース

このチュートリアルでは、GitHub webhook イベントに応答し、Slack 内から GitHub API を検索できる Cloudflare Workers アプリケーションを構築してデプロイしました。このアプリケーションのソースコード全体を確認したい場合は、GitHub 上のリポジトリ を参照してください。

自分のプロジェクトを始めたい場合は、既存の クイックスタートテンプレート を確認してください。

役に立ちましたか?