コンテンツにスキップ

Slackbotの構築

Last reviewed: 4 months ago

このチュートリアルでは、Slack ボットを Cloudflare Workers を使用して構築します。あなたのボットは、GitHubのウェブフックを利用して、問題が更新または作成されたときにSlackチャンネルにメッセージを送信し、ユーザーがSlack内からGitHubの問題を検索するためのコマンドを入力できるようにします。

このチュートリアルに従うことで、あなたはこの例のようなSlackbotを作成できるようになります。Slackbotを構築するために読み進めてください。

このチュートリアルは、ウェブアプリケーションの作成に慣れている人に推奨されます。プログラミング言語としてTypeScriptを使用し、ウェブフレームワークとしてHonoを使用します。NodeExpressのようなツールでアプリケーションを構築したことがある場合、このプロジェクトは非常に馴染みやすいでしょう。ウェブアプリケーションの作成が初めてで、過去にSlackボットのようなものを構築したいと思っていたが、デプロイや設定に intimidated されていた場合、Workersはコードの作成とプロジェクトの出荷に集中できる方法を提供します。

このチュートリアルを進める前に、コードやボットが実際のSlackチャンネルでどのように機能するかを確認したい場合は、最終版のコードベースにGitHubでアクセスできます。GitHubから、自分のSlack APIキーを追加し、テストのために自分のSlackチャンネルにデプロイできます。


Before you start

All of the tutorials assume you have already completed the Get started guide, which gets you set up with a Cloudflare Workers account, C3, and Wrangler.

Slackの設定

このチュートリアルでは、すでにSlackアカウントを持っており、Slackアプリケーションを作成および管理する能力があることを前提としています。

Slackアプリケーションの設定

Cloudflare WorkerからSlackチャンネルにメッセージを投稿するには、SlackのUIでアプリケーションを作成する必要があります。これを行うには、SlackのAPIセクションに移動し、api.slack.com/apps新しいアプリを作成を選択します。

Slackbotを作成するには、まずSlackアプリを作成します。

Slackアプリケーションには多くの機能があります。あなたは、Incoming WebhooksとSlash Commandsの2つを利用して、Workerを活用したSlackボットを構築します。

Incoming Webhook

Incoming Webhooksは、Slackチャンネルにメッセージを送信するために使用できるURLです。あなたのIncoming Webhookは、特定のリポジトリ内の問題に更新があるたびにSlackチャンネルにメッセージを送信するために、GitHubのWebhookサポートとペアになります。アプリケーションを構築する際に、コードをより詳細に見ることができます。まず、Slack Webhookを作成します。

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

Webhook URLを承認した後、Incoming Webhooksページに戻り、新しいWebhook URLを表示できます。後でこのURLをWorkersコードに追加します。次に、Slackボットの2つ目のコンポーネントであるSlash Commandを追加します。

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

Slash Command

SlackのSlash Commandは、URLリクエストに添付できるカスタム設定されたコマンドです。たとえば、/weather <zip>を設定した場合、Slackは指定されたURLにHTTP POSTリクエストを行い、指定された郵便番号の天気を取得するために<zip>のテキストを渡します。あなたのアプリケーションでは、GitHub APIを使用してGitHubの問題を検索するために/issueコマンドを使用します。/issue cloudflare/wrangler#1と入力すると、テキストcloudflare/wrangler#1がHTTP POSTリクエストとしてアプリケーションに送信され、アプリケーションは関連するGitHubの問題を見つけるためにそれを使用します。

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

このチュートリアルでは、コマンド/issueを使用します。リクエストURLは、アプリケーションURLの/lookupパスである必要があります。たとえば、アプリケーションがhttps://myworkerurl.comでホストされる場合、リクエストURLはhttps://myworkerurl.com/lookupである必要があります。

SlackのダッシュボードでSlash Commandを作成し、Request URLに接続する必要があります。

GitHub Webhooksの設定

あなたのCloudflare Workersアプリケーションは、Slackからの受信リクエストを処理できるようになります。また、GitHubから直接イベントを受信することもできる必要があります。GitHubの問題が作成または更新された場合、GitHubのWebhookを利用してそのイベントをWorkersアプリケーションに送信し、Slackに対応するメッセージを投稿できます。

Webhookを設定するには:

  1. GitHubリポジトリのSettings > Webhooks > Add webhookに移動します。

リポジトリがhttps://github.com/user/repoのようなものであれば、直接https://github.com/user/repo/settings/hooksWebhooksページにアクセスできます。

  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に送信したいイベントを指定できます。デフォルトでは、Webhookはリポジトリからpushイベントを送信します。このチュートリアルの目的のために、Let me select individual eventsを選択します。

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

Webhookに対して有効にできるさまざまなイベントタイプがあります。Issuesを選択すると、問題が開かれたとき、編集されたとき、削除されたときなど、すべての問題関連のイベントがWebhookに送信されます。将来的にSlackボットアプリケーションを拡張したい場合は、チュートリアルの後にこれらのイベントをさらに選択できます。

  1. Add webhookを選択します。

GitHubのダッシュボードでGitHub Webhookを作成します。

Webhookが作成されると、アプリケーションにテストペイロードを送信しようとします。アプリケーションはまだ実際にデプロイされていないため、設定はそのままにしておきます。後でリポジトリに戻り、いくつかの問題を作成、編集、閉じて、アプリケーションがデプロイされたときにWebhookが機能していることを確認します。

初期化

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

Terminal window
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ディレクトリに移動します:

Terminal window
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 run 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ルートは、Slackからのリクエストを受け取り(ユーザーが/issueコマンドを使用したときに送信される)、GitHub APIを使用して対応する問題を検索します。このアプリケーションは、メインアプリケーションの/lookupに追加されます。

  2. webhookルートは、GitHub上の問題が変更されたときに、設定されたWebhookを介して呼び出されます。このアプリケーションは、メインアプリケーションの/webhookに追加されます。

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

新しいフォルダとファイルを作成
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の問題の型で、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にHTTP POSTリクエストを送信し、application/x-www-form-urlencodedコンテンツタイプを持ちます。たとえば、誰かが/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のような文字列を解析し、GitHubのAPIを使用して問題データを取得するために必要なデータ(ownerrepoissue_number)を得ることができます。

Slackのスラッシュコマンドを使用すると、受信したスラッシュコマンドに対して構造化されたデータを返すことで応答できます。この場合、GitHubのAPIからの応答を使用して、問題のタイトル、作成者、作成日などのデータを含むGitHubの問題のフォーマットされたバージョンを提示する必要があります。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;

text変数がcloudflare/wrangler#1のようなテキストを含む場合、そのテキストを解析し、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 に対してマッチさせ、マッチが見つかった場合は、そのマッチから groups オブジェクトを返します。これは、正規表現で定義された ownerrepo、および issue_number のキャプチャグループを利用します。この関数を 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 に新しい関数を作成し、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からのレスポンスを受け取った後、最終ステップは問題データを使ってSlackメッセージを構築し、それをユーザーに返すことです。最終結果は次のようになります。

成功したSlackメッセージには以下のコンポーネントが含まれます

上記のスクリーンショットには、4つの異なる部分が見えます。

  1. 最初の行(太字)は問題へのリンクで、問題のタイトルを表示します。
  2. 次の行(コードスニペットを含む)は問題の本文です。
  3. 最後の行のテキストは問題のステータス、問題の作成者(ユーザーのGitHubプロフィールへのリンク付き)、および問題の作成日を示します。
  4. 問題の作成者のプロフィール画像が右側に表示されます。

前述のBlock Kitフレームワークは、問題データ(GitHubのREST APIドキュメントに記載された構造)を取り込み、上記のスクリーンショットのような形式に整形するのに役立ちます。

別のファイル src/utils/slack.ts を作成し、問題データを受け取り、それをブロックのコレクションに変換する関数 constructGhIssueSlackMessage を含めます。ブロックは、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を受け入れ、リンクは <https://yoururl.com|表示テキスト> の形式で表示されます。

その形式を考慮して、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ユーザーへのクリック可能なリンクを構築します。

最後に、issue.created_at を解析し、ISO 8601文字列をJavaScriptの Date インスタンスに変換し、MM/DD/YY 形式のフォーマットされた文字列に変換します。

これらの変数が整ったら、text_lines はSlackメッセージの各行のテキストの配列になります。最初の行は 問題のタイトル問題のリンク、2行目は 問題の本文、最後の行は 問題の状態(例えば、オープンまたはクローズ)、ユーザーリンク、および 作成日 です。

テキストが構築されたら、最終的にSlackメッセージを構築し、SlackのBlock Kit用のブロックの配列を返します。この場合、1つのブロックのみがあります:Markdownテキストを持つセクションブロックと、問題を作成したユーザーのアクセサリー画像です。その単一のブロックを配列内に返して、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,
},
},
];
};

ルックアップルートの完成

src/handlers/lookup.ts で、constructGhIssueSlackMessage を使用して 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 です。デフォルトでは、スラッシュコマンドへのレスポンスはエフェメラルであり、スラッシュコマンドを記述したユーザーのみが見ることができます。上記のように response_typein_channel に設定すると、レスポンスがチャンネル内のすべてのユーザーに表示されます。

メッセージをプライベートに保ちたい場合は、response_type 行を削除します。これにより、response_type はデフォルトで ephemeral になります。

エラー処理

lookup ルートはほぼ完成していますが、ルート内で発生する可能性のあるいくつかのエラーがあります。たとえば、Slackからのボディの解析、GitHubからの問題の取得、または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(
"ああ!提供された問題が見つかりませんでした。 " +
"次の形式の公開問題のみを見つけることができます:`owner/repo#issue_number`。",
);
});
export default app;

Webhookルートの作成

これで、Workersアプリケーションのルートを実装する途中まで来ました。次のルート src/routes/webhook.ts を実装する際には、すでに書いたルックアップルートのコードを多く再利用します。

このチュートリアルの最初に、リポジトリ内の問題に関連するイベントを追跡するためにGitHubのWebhookを設定しました。たとえば、問題が開かれたとき、Workersアプリケーションの /webhook パスに対応する関数は、GitHubから送信されたデータを受け取り、設定されたSlackチャンネルに新しいメッセージを投稿する必要があります。

src/routes/webhook.ts で、空のHonoアプリケーションを定義します。ルックアップルートとの違いは、Bindingsnew Hono() のためのジェネリクスとして渡されることです。これは、後で使用される SLACK_WEBHOOK_URL に適切なTypeScript型を与えるために必要です。

import { Hono } from "hono";
import { Bindings } from "../types";
const app = new Hono<{ Bindings: Bindings }>();
export default app;

ルックアップルートと同様に、request 内の受信ペイロードを解析し、そこから関連する問題データを取得する必要があります(完全なペイロードスキーマについては、GitHub APIドキュメントの IssueEventを参照してください)。そして、何が変わったのかを示すためにSlackにフォーマットされたメッセージを送信します。最終版は次のようになります。

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

このメッセージ形式は、ユーザーが /issue スラッシュコマンドを使用したときに返される形式と非常に似ています。2つの間の唯一の実際の違いは、最初の行にアクションテキストが追加されることです。形式は 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;

IssueEvent は、Webhook設定の一部としてGitHubから送信されるペイロードで、action(問題に何が起こったか:たとえば、オープンされた、クローズされた、ロックされたなど)、issue 自体、および repository などが含まれます。

c.req.json() を使用して、リクエストのペイロードボディをJSONからプレーンなJSオブジェクトに変換します。ES6の分割代入を使用して、actionissue、および repository をコード内で使用できる変数として設定します。prefix_text は、問題に何が起こったかを示す文字列であり、issue_string は以前に見たおなじみの文字列 owner/repo#issue_number です。ルックアップルートでは、Slackから送信されたテキストを直接使用して issue_string を埋めましたが、ここではJSONペイロードで渡されたデータに基づいて直接構築します。

Slackメッセージの構築と送信

lookupwebhook ルートからSlackボットがSlackチャンネルに送信するメッセージは非常に似ています。このため、既存の constructGhIssueSlackMessage を再利用して、src/handlers/webhook.ts を引き続き埋めることができます。関数を src/utils/slack.ts からインポートし、問題データを渡します。

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 の使用は、関数に1つの追加引数 prefix_text を加えます。関数が渡された場合、src/utils/slack.ts 内の対応する関数を更新し、メッセージブロックの text_lines コレクションに prefix_text を追加してください。

配列を受け取り、null または undefined の値をフィルタリングするユーティリティ関数 compact を追加します。この関数は、src/handlers/lookup.ts から呼び出されたときのように、実際に関数に渡されていない場合に text_lines から prefix_text を削除するために使用されます。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 が新しい fetch リクエストのボディとなり、Slack ウェブフック URL への 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 ウェブフック URL を表します。

この定数をコードベース内で使用するには、wrangler secret コマンドを使用します:

Set the SLACK_WEBHOOK_URL secret
npx wrangler secret put SLACK_WEBHOOK_URL
Enter a secret value: https://hooks.slack.com/services/abc123

エラー処理

lookup ルートと同様に、webhook ルートには基本的なエラー処理を含めるべきです。lookup が直接 Slack にレスポンスを送信するのに対し、ウェブフックで何か問題が発生した場合、実際にエラー応答を生成し、それを 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 アプリケーションのバンドル、アップロード、およびリリースをサポートしています。これを行うには、次のコマンドを実行してコードをビルドおよびデプロイします。

Deploy your application
npm run deploy

あなたの Workers アプリケーションをデプロイすると、GitHub ウェブフックが正常にあなたの Workers ウェブフックルートに到達できるようになり、問題の更新があなたの Slack チャンネルに表示されるようになります:

新しい問題を作成すると、Slack チャンネルに Slackbot が表示されるようになります

関連リソース

このチュートリアルでは、GitHub ウェブフックイベントに応答し、Slack 内で GitHub API ルックアップを可能にする Cloudflare Workers アプリケーションを構築してデプロイしました。このアプリケーションの完全なソースコードを確認したい場合は、リポジトリを GitHub で見つけることができます。

独自のプロジェクトを構築するために始めたい場合は、既存の Quickstart テンプレート のリストを確認してください。