FastComments.com

Добавьте живой комментарий в приложения Val Town

Val Town запускает TypeScript на Deno, поэтому val — это настоящий сервер. Это делает его хорошим выбором для FastComments: виджет — это тег script на странице, а всё, что требует секрета, например Secure SSO или проверка вебхука, может выполняться на сервере в том же val.

В этом руководстве рассматривается добавление виджета комментариев в HTTP‑val, отображение количества комментариев на главной странице, вход пользователей с помощью уже существующей учётной записи Val Town и получение вебхуков комментариев.

Для попытки не требуется учётная запись. В примерах используется tenantId: "demo", общий песочничный сервер, а Шаг 2 описывает переход к вашему собственному.

Количество комментариев на странице индекса Internal Link


На странице индекса не рендерьте один виджет подсчёта комментариев на каждую строку. Это приводит к одному запросу на каждый пост. Используйте массовый подсчёт, который делает один запрос для всей страницы.

Отметьте каждую строку с помощью urlId, который использует её ветка, затем загрузите массовый виджет один раз:

Массовый подсчёт комментариев на индексе
Copy CopyRun External Link
1
2<ul>
3 {posts.map((post) => (
4 <li>
5 <a href={post.slug}>{post.title}</a>{" "}
6 <span class="fast-comments-count" data-fast-comments-url-id={post.slug}></span>
7 </li>
8 ))}
9</ul>
10
11<script
12 dangerouslySetInnerHTML={{
13 __html: `window.FastCommentsBulkCountConfig = ${
14 JSON.stringify({ tenantId: TENANT_ID })
15 };`,
16 }}
17/>
18<script src="https://cdn.fastcomments.com/js/embed-widget-comment-count-bulk.min.js"></script>
19

Скрипт ищет каждый элемент .fast-comments-count на странице и заполняет его счётчиком.

data-fast-comments-url-id должен соответствовать urlId, который использует виджет комментариев поста. Если виджет использует slug, маркер использует slug. Несоответствие приводит к отображению нуля в ветке, где есть комментарии.

Скрипт опрашивает window.FastCommentsBulkCountConfig, поэтому не имеет значения, задаёте вы конфигурацию до или после тега script.

Защищенный SSO с std/oauth Internal Link

Если ваш val уже знает, кто посетитель, Secure SSO передаёт эту идентификацию виджету, чтобы они никогда не видели второй вход. Не требуется создавать конечные точки и ничего вызывать во время выполнения: вы вычисляете три значения на сервере и передаёте их в конфигурацию виджета.

Val Town поставляет вход без конфигурации с std/oauth, поэтому посетитель может войти с учётной записью Val Town, которую он уже имеет. Замените это на то, что использует ваше приложение; часть FastComments не меняется.

Создание полезной нагрузки на сервере

Секрет API подписывает полезную нагрузку и никогда не должен попадать в код браузера. Установите SDK из npm, который работает в среде Deno от Val Town без изменений:

sso.ts
Copy CopyRun External Link
1
2import { SecureSSOPayloadBuilder } from "npm:fastcomments-sdk/server";
3
4export function buildSSOPayload(user) {
5 // id must be stable for the same person, or they get a new comment identity on every login.
6 const id = `vt-${user.id}`;
7
8 return new SecureSSOPayloadBuilder(Deno.env.get("FASTCOMMENTS_API_SECRET"), {
9 id,
10 // email is required and must be unique.
11 email: user.email ?? `${id}@users.noreply.val.town`,
12 // username is required and cannot be an email.
13 username: user.username ?? id,
14 displayName: user.username ?? undefined,
15 avatar: user.links.profileImageUrl ?? undefined,
16 }).getPayload();
17}
18

getPayload() возвращает { userDataJSONBase64, verificationHash, timestamp }. Эти три значения — всё, что попадает в браузер. Секрет подписывает их и затем отбрасывается, поэтому ничего на странице не позволяет читателю подделать другого пользователя.

Передача в виджет

Конфигурация виджета с SSO
Copy CopyRun External Link
1
2import { getOAuthUserData, oauthMiddleware } from "https://esm.town/v/std/oauth/middleware.ts";
3
4app.get("/", async (c) => {
5 const session = await getOAuthUserData(c.req.raw);
6 const user = session?.user;
7
8 const config = {
9 tenantId: TENANT_ID,
10 urlId: "my-thread",
11 ...(user
12 ? { sso: { ...buildSSOPayload(user), logoutURL: "/logout" } }
13 : { sso: { loginURL: "/auth/login" } }),
14 };
15
16 // ...render the widget with this config
17});
18
19export default oauthMiddleware(app.fetch);
20

oauthMiddleware добавляет GET /auth/login, GET /auth/callback и POST /auth/logout за вас. Обратите внимание, что выход (logout) — это POST, в то время как виджет переходит к logoutURL с помощью GET, поэтому укажите logoutURL на небольшой маршрут вашего приложения, который выполняет POST.

Когда посетитель вышел из системы, передайте sso только с loginURL. Виджет тогда покажет запрос входа вместо анонимного поля комментария.

Возможные проблемы

timestamp — это эпоха в миллисекундах, не должна быть в будущем и не должна быть старше двух дней. Генерируйте её на сервере в том же запросе, где вычисляется хеш. Генерация в браузере — классическая ошибка: значение отличается от того, которое было захешировано, и каждый комментарий отклоняется.

Никогда не устанавливайте isAdmin или isModerator из провайдера идентификации. Вход с учётной записью Val Town ничего не говорит о том, кто должен модерировать ваш сайт.

Смотрите руководство по SSO для полного списка полей, потоков с групповыми ограничениями и значков.

Получение вебхуков Internal Link

A val — это естественный получатель веб‑хуков: у него стабильный URL, он может проверять подпись и имеет встроенные SQLite и блоб‑хранилище.

FastComments подписывает ${timestamp}.${body} с помощью API‑секрета вашего аккаунта и отправляет два заголовка:

Заголовки вебхука
Copy CopyRun External Link
1
2X-FastComments-Timestamp: 1789004710 unix seconds, not milliseconds
3X-FastComments-Signature: sha256=<hex>
4

Метод передаёт событие: PUT для созданного или обновлённого комментария, DELETE для удалённого.

Проверка доставки
Copy CopyRun External Link
1
2import { createHmac, timingSafeEqual } from "node:crypto";
3
4async function receive(c) {
5 // Точные байты, которые пришли. НЕ используйте c.req.json() и повторную сериализацию.
6 const rawBody = await c.req.raw.text();
7 const timestamp = c.req.raw.headers.get("X-FastComments-Timestamp");
8 const signature = c.req.raw.headers.get("X-FastComments-Signature");
9
10 if (!timestamp || !signature) return new Response("Missing headers", { status: 400 });
11
12 // Отклоняем устаревшие доставки, чтобы перехваченный запрос нельзя было воспроизвести позже.
13 if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) {
14 return new Response("Timestamp outside window", { status: 400 });
15 }
16
17 const expected = "sha256=" + createHmac("sha256", Deno.env.get("FASTCOMMENTS_API_SECRET"))
18 .update(`${timestamp}.${rawBody}`)
19 .digest("hex");
20
21 const a = new TextEncoder().encode(signature);
22 const b = new TextEncoder().encode(expected);
23 if (a.length !== b.length || !timingSafeEqual(a, b)) {
24 return new Response("Signature mismatch", { status: 401 });
25 }
26
27 // ...handle JSON.parse(rawBody)
28 return Response.json({ received: true });
29}
30
31app.put("/", receive);
32app.delete("/", receive);
33

Два момента, которые могут вызвать проблемы

Проверяйте сырые байты. Парсинг JSON и повторная сериализация меняют порядок ключей и пробелы, поэтому хеш отличается и каждая доставка завершается ошибкой без очевидной причины. Это обычная причина, почему получатель веб‑хука «просто не работает».

Сравнивайте за постоянное время. Обычное === по подписи раскрывает, сколько байтов совпало, чего достаточно, чтобы подделать один байт за раз.

Обработка событий

Отвечайте быстро. FastComments повторяет запрос при ответе, не являющемся 2xx, и конечная точка, постоянно вызывающая ошибки, в конце концов автоматически отключается, поэтому выполняйте реальную работу после отправки ответа, а не внутри него.

Сделайте работу идемпотентной по ID комментария. При повторе запрос подписывается заново с новым timestamp, и тот же ID комментария приходит снова при редактировании и удалении, поэтому нет стабильного признака для дедупликации.

Примеры Vals Internal Link

Четыре публичных vals, которые вы можете ремиксить, каждая охватывает одну часть этого руководства.

Блог с комментариями (вживую) — это Markdown‑блог с веткой комментариев под каждой записью и массовыми подсчётами комментариев в индексе. Он работает сразу после ремикса, и одна переменная окружения указывает его на ваш собственный аккаунт.

Демонстрация SSO (вживую) регистрирует посетителя с помощью их аккаунта Val Town и передаёт эту идентичность виджету, так что вторичный вход не требуется.

Получатель вебхуков (вживую) проверяет HMAC‑подпись каждой доставки и сохраняет события в SQLite. Он имеет кнопку, которая подписывает тестовый payload и отправляет его себе, так что вы можете увидеть успешную проверку перед настройкой реального вебхука.

Навыки агента (вживую) — это библиотека навыков агента FastComments, охватывающая виджет, SSO, REST API, модерацию и миграцию с Disqus. Ремикните её, и агент Val Town, Townie, автоматически подхватывает навыки из skills/, так что ваш агент знает, как подключить комментарии без вставки документации в чат.

Те же навыки можно установить где угодно с помощью npx skills add fastcomments/skills.