INTEGRITY Документация

Создайте Slackbot

В этом руководстве вы создадите Slack бота с использованием Cloudflare Workers. Бот будет использовать GitHub webhooks для отправки сообщений в канал Slack при обновлении или создании issue, а также позволит пользователям с помощью команды искать issue GitHub прямо из Slack.

После выполнения этого руководства вы сможете создать Slackbot, как в этом примере. Продолжайте чтение, чтобы создать свой Slackbot.

Это руководство рекомендуется тем, кто уже писал веб-приложения. Вы будете использовать TypeScript в качестве языка программирования и Hono в качестве веб-фреймворка. Если вы создавали приложение с помощью таких инструментов, как Node и Express, этот проект покажется вам очень знакомым. Если вы никогда не писали веб-приложения или давно хотели создать что-то вроде Slack-бота, но вас останавливала сложность развёртывания или настройки, Workers станут для вас способом сосредоточиться на написании кода и выпуске проектов.

Если перед тем как приступить к этому руководству вы хотите посмотреть код или увидеть, как бот работает в реальном канале Slack, финальная версия кодовой базы доступна на GitHub. Из GitHub можно добавить собственные ключи Slack API и развернуть приложение в своих каналах Slack для тестирования.


Прежде чем начать

Во всех руководствах предполагается, что вы уже выполнили Руководство по началу работы, который поможет вам настроить аккаунт Cloudflare Workers, C3, а также Wrangler.

Настройка Slack

Это руководство предполагает, что у вас уже есть аккаунт Slack и возможность создавать приложения Slack и управлять ими.

Настройка приложения Slack

Чтобы отправлять сообщения из своего Cloudflare Worker в канал Slack, нужно создать приложение в интерфейсе Slack. Для этого перейдите в раздел API Slack по адресу api.slack.com/apps, и выберите Создать новое приложение.

Чтобы создать Slackbot, сначала создайте Slack App

Приложения Slack обладают множеством функций. Вы задействуете две из них, Incoming Webhooks и Slash Commands, чтобы создать Slack-бота на основе Worker.

Incoming Webhook

Incoming Webhooks предоставляют URL-адреса, с помощью которых можно отправлять сообщения в каналы Slack. Ваш incoming webhook будет связан с поддержкой webhook в GitHub, чтобы отправлять сообщения в канал Slack при каждом обновлении issue в выбранном репозитории. Подробнее код вы увидите по мере создания приложения. Сначала создайте webhook Slack:

  1. На боковой панели интерфейса Slack выберите Incoming Webhooks.
  2. В URL-адреса вебхуков для вашего Workspace, выберите Добавление нового Webhook в Workspace.
  3. На следующем экране выберите канал, в который webhook будет отправлять сообщения (можно выбрать комнату, например #general или #code, либо получать сообщения напрямую от Slack-бота при вызове webhook).
  4. Авторизуйте новый URL вебхука.

После авторизации URL webhook вы будете перенаправлены на Incoming Webhooks странице и увидите новый URL вебхука. Вы добавите его в код Workers позже. Далее вы добавите второй компонент своего Slack-бота: команду Slash Command.

Чтобы добавить новый Webhook URL в панели Slack, выберите Add New Webhook to Workspace

Slash Command

Slash Command в Slack представляет собой настраиваемую команду, которую можно привязать к запросу URL. Например, если вы настроили /weather <zip>, Slack отправит запрос HTTP POST на настроенный URL, передав текст <zip> чтобы получить погоду для указанного почтового индекса. В своём приложении вы будете использовать /issue команду, чтобы искать issue в GitHub с помощью GitHub API. Типизация /issue cloudflare/wrangler#1 отправит текст cloudflare/wrangler#1 в HTTP-запросе POST к вашему приложению, которое использует его для поиска соответствующий issue в GitHub.

  1. На боковой панели Slack выберите Slash Commands.
  2. Создайте свою первую слэш-команду.

В этом руководстве вы будете использовать команду /issue. URL запроса должен быть /lookup путь по URL вашего приложения: например, если приложение будет размещено по адресу https://myworkerurl.com, URL запроса должен быть https://myworkerurl.com/lookup.

Необходимо создать Slash Command в панели управления Slack и привязать её к Request URL

Настройка GitHub Webhooks

Ваше приложение на Cloudflare Workers сможет обрабатывать входящие запросы от Slack. Оно также должно уметь получать события напрямую от GitHub. Если issue в GitHub создаётся или обновляется, вы можете использовать GitHub webhooks, чтобы отправить это событие в приложение Workers и опубликовать соответствующее сообщение в Slack.

Чтобы настроить вебхук:

  1. В репозитории GitHub перейдите на страницу Настройки > Webhooks > Добавить webhook.

Если у вас есть репозиторий вида https://github.com/user/repo, вы можете получить доступ к Webhooks страницу напрямую по адресу https://github.com/user/repo/settings/hooks.

  1. Укажите в качестве Payload URL значение /webhook путь по URL вашего Worker.

Например, если ваш Worker будет размещён по адресу https://myworkerurl.com, URL полезной нагрузки должен быть https://myworkerurl.com/webhook.

  1. В Тип контента раскрывающемся списке выберите application/json.

Тип контента для вашей полезной нагрузки может быть либо полезная нагрузка в формате URL-encoded (application/x-www-form-urlencoded) или JSON (application/json). В рамках этого руководства и для упрощения разбора данных, отправляемых в приложение, выберите JSON.

  1. В Какие события должны запускать этот webhook?, выберите Выбрать события по отдельности.

Веб-хуки GitHub позволяют указать, какие события должны отправляться на ваш веб-хук. По умолчанию веб-хук отправляет push события из вашего репозитория. Для целей данного руководства вы выберете Выбрать события по отдельности.

  1. Выберите Проблемы тип события.

Для вашего вебхука можно включить множество различных типов событий. Выбор Проблемы будет отправлять на ваш webhook все события, связанные с issue, включая их создание, редактирование, удаление и другие. Если в будущем вы захотите расширить приложение Slack-бота, вы можете выбрать больше таких событий после завершения этого руководства.

  1. Выберите Добавить webhook.
Создайте вебхук GitHub в панели управления GitHub

После создания webhook он попытается отправить тестовую полезную нагрузку в ваше приложение. Поскольку приложение ещё не развёрнуто, оставьте настройки как есть. Позже вы вернётесь в свой репозиторий, чтобы создать, отредактировать и закрыть несколько issue и убедиться, что webhook работает после развёртывания приложения.

Инициализация

Чтобы инициировать проект, используйте интерфейс командной строки C3 (create-cloudflare-cli).

npm create cloudflare@latest -- slack-bot

Чтобы создать проект Hono, выполните следующие шаги.

Перейдите в 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 Not Found с кодом состояния 404, если запрашивается любой другой путь или метод.

Чтобы запустить приложение на локальном компьютере, выполните следующую команду.

npm i -- dev

Доступ к http://localhost:8787 в браузере после запуска сервера, и вы увидите сообщение.

Hono помогает быстро и легко создавать приложения Workers.

Build

Теперь создадим Slack-бота на Cloudflare Workers.

Разделение файлов

Приложение можно создавать в нескольких файлах, а не описывать все эндпоинты и функции в одном файле. В 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;

Возвращает Posts! при обращении к GET /api/v1/posts.

У Slack-бота будет два дочерних приложения, каждое из которых называется "route".

  1. lookup маршрут будет принимать запросы от Slack (отправляемые, когда пользователь использует /issue команду) и ищет соответствующий issue с помощью GitHub API. Это приложение будет добавлено в /lookup в основном приложении.

  2. webhook маршрут будет вызываться при изменении issue в GitHub через настроенный вебхук. Это приложение будет добавлено в /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 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;
};

Создание маршрута поиска

Начните создавать маршрут поиска в src/routes/lookup.ts.

import { Hono } from "hono";

const app = new Hono();

export default app;

Чтобы понять, как правильно спроектировать эту функцию, нужно разобраться, как слэш-команды Slack отправляют данные на URL.

Согласно документация по слэш-командам Slack, Slack отправляет запрос HTTP POST на указанный вами URL с 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, вы можете разобрать эту строку на известные части данных (owner, repo, а также issue_number), и используйте его для отправки запроса к API GitHub, чтобы получить данные issue.

С помощью slash-команд Slack можно отвечать на входящую slash-команду, возвращая структурированные данные. В этом случае нужно использовать ответ API GitHub, чтобы представить отформатированную версию issue GitHub, включая такие данные, как заголовок issue, автора и дату создания. Новый Block Kit фреймворк позволит вам возвращать развёрнутый ответ с сообщением, формируя текстовые и графические блоки на основе данных из API GitHub.

Разбор слэш-команд

Для начала lookup маршрут должен разбирать сообщения, поступающие из Slack. Как упоминалось ранее, API Slack отправляет HTTP POST в формате URL Encoded. Вы можете получить переменную text разобрав его с помощью c.req.json().

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, вам следует разобрать этот текст и получить из него отдельные части для использования с API GitHub: owner, repo, а также issue_number.

Для этого создайте в приложении новый файл по пути src/utils/github.ts. Этот файл будет содержать несколько «служебных» функций для работы с API GitHub. Первой из них будет парсер строк с именем parseGhIssueString:

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

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

parseGhIssueString принимает text ввод, сопоставляет его с ghIssueRegex, и если совпадение найдено, возвращает groups объект из этого совпадения, используя owner, repo, а также 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;

Выполнение запросов к API GitHub

Имея эти данные, можно сделать первый запрос к API GitHub. Снова создайте новую функцию в src/utils/github.ts, чтобы выполнить fetch запрос к GitHub API за данными issue:

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 чтобы выполнить запрос к API GitHub и разобрать ответ:

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

После получения ответа от API GitHub последним шагом будет сформировать сообщение Slack с данными issue и вернуть его пользователю. Итоговый результат будет выглядеть примерно так:

Успешное сообщение Slack содержит перечисленные ниже компоненты

На скриншоте выше видно четыре элемента:

  1. Первая строка (выделена жирным) содержит ссылку на issue и показывает его заголовок
  2. Следующие строки (включая фрагменты кода) составляют тело issue
  3. В последней строке текста указаны статус issue, автор issue (со ссылкой на профиль пользователя на GitHub) и дата создания issue
  4. Фото профиля автора issue, справа

Упомянутый ранее Block Kit фреймворк поможет взять данные issue (в структуре, описанной в Документация REST API GitHub) и отформатировать её примерно так, как показано на скриншоте выше.

Создайте еще один файл, src/utils/slack.ts, чтобы содержать функцию constructGhIssueSlackMessage, функция, которая принимает данные issue и преобразует их в набор блоков (blocks). Блоки представляют собой объекты JavaScript, которые Slack использует для форматирования сообщения:

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, который принимает html_url свойство из GitHub API issue данные (в формате https://github.com/cloudflare/wrangler-legacy/issues/1), и issue_string отправленные из слэш-команды Slack, и объединяет их в кликабельную ссылку в сообщении 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. Первая строка представляет собой заголовок issue и ссылка на issue, а вторая это тело issue, а последней строкой является статус issue (например, open или closed), ссылка пользователя, и дата создания.

Когда текст сформирован, можно наконец собрать сообщение Slack, вернув массив блоков для Block Kit. В этом случае есть только один блок: раздел блок с текстом Markdown и дополнительным изображением пользователя, создавшего issue. Верните этот единственный блок внутри массива, чтобы завершить 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_type in_channel, как показано выше, приведёт к тому, что ответ появится у всех пользователей в канале.

Если вы хотите, чтобы сообщения оставались приватными, удалите response_type строку. Это приведёт к тому, что response_type по умолчанию принимать значение ephemeral.

Обработка ошибок

lookup маршрут почти готов, но в нем возможен ряд ошибок: например, при разборе тела запроса от Slack, при получении issue из 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(
		"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, вы повторно используете большую часть кода, уже написанного для маршрута поиска.

В начале этого руководства вы настроили webhook GitHub для отслеживания событий, связанных с issue в вашем репозитории. Например, когда issue открывается, вызывается функция, соответствующая пути /webhook в вашем приложении Workers должно принимать данные, отправленные из GitHub, и публиковать новое сообщение в настроенном канале Slack.

В src/routes/webhook.ts, определите пустое приложение Hono. Отличие от lookup маршрут заключается в том, что Bindings передается как параметр обобщенного типа для new Hono(). Это необходимо, чтобы присвоить подходящий тип TypeScript SLACK_WEBHOOK_URL который будет использован позже.

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

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

export default app;

Как и в случае с lookup маршрут, вам нужно будет разобрать входящий payload внутри request, получите из него нужные данные issue (см. документация GitHub API о IssueEvent для полной схемы полезной нагрузки), а также отправить отформатированное сообщение в Slack с указанием произошедших изменений. Итоговая версия будет выглядеть примерно так:

Пример успешного сообщения Webhook

Сравните этот формат сообщения с форматом, который возвращается, когда пользователь использует /issue слэш-команды. Вы увидите, что между ними есть только одно фактическое различие: добавление текста действия в первой строке в формате An issue was $action:. Это действие, отправляемое как часть IssueEvent от GitHub, будут использованы при формировании хорошо знакомого набора блоков с помощью 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, полезная нагрузка, отправляемая GitHub в рамках настройки вебхука, включает action (что произошло с issue: например, он был открыт, закрыт, заблокирован и так далее), issue сам по себе, и repository, среди прочего.

Используйте c.req.json() чтобы преобразовать тело запроса из JSON в обычный объект JS. Используйте деструктуризацию ES6, чтобы задать action, issue и repository как переменные, которые можно использовать в коде. prefix_text представляет собой строку, указывающую, что произошло с issue, и issue_string это привычная строка owner/repo#issue_number который вы уже видели: пока lookup маршрут напрямую использовал текст, отправленный из Slack, для заполнения issue_string, вы создадите его напрямую на основе данных, переданных в теле JSON.

Формирование и отправка сообщения Slack

Сообщения, которые ваш Slack-бот отправляет обратно в канал Slack из lookup и webhook маршруты очень похожи друг на друга. Благодаря этому вы можете повторно использовать существующий 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, добавив prefix_text в коллекцию text_lines в блоке сообщения, если оно было передано в функцию.

Добавьте служебную функцию, compact, который принимает массив и отфильтровывает все null или undefined значения из него. Эта функция будет использоваться для удаления prefix_text от text_lines если оно фактически не было передано в функцию, например при вызове из src/handlers/lookup.ts. Полная (и окончательная) версия 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, blocks которые возвращаются из constructGhIssueSlackMessage становится телом нового fetch запрос, HTTP-запрос методом POST на URL вебхука Slack. После завершения этого запроса верните ответ с кодом состояния 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 представляет собой Slack Webhook URL, который вы создали ещё в начале, в разделе Incoming Webhook раздел этого руководства.

Чтобы использовать эту константу в своём коде, используйте wrangler secret команда:

Настройка секрета SLACK_WEBHOOK_URL
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 обновления issue должны начать появляться в вашем Slack-канале, поскольку GitHub webhook теперь может успешно обращаться к маршруту webhook вашего Workers:

Когда вы создадите новый issue, в вашем канале Slack появится Slackbot

В этом руководстве вы создали и развернули приложение Cloudflare Workers, которое реагирует на события webhook GitHub и позволяет выполнять запросы к GitHub API прямо из Slack. Чтобы посмотреть полный исходный код приложения, вы найдёте репозиторий на GitHub.

Если вы хотите начать создавать собственные проекты, ознакомьтесь с существующим списком Шаблоны для быстрого старта.