FastComments.com

Dodaj komentarze na żywo do aplikacji Val Town

Val Town uruchamia TypeScript na Deno, więc val jest prawdziwym serwerem. To sprawia, że jest to dobre dopasowanie do FastComments: widget jest tagiem skryptu na stronie, a wszystko, co wymaga sekretu, jak Secure SSO czy weryfikacja webhooka, może działać po stronie serwera w tym samym val.

Ten przewodnik opisuje dodawanie widgetu komentarzy do HTTP val, wyświetlanie liczby komentarzy na stronie indeksu, logowanie użytkowników przy użyciu konta Val Town, które już posiadają, oraz odbieranie webhooków komentarzy.

Nie potrzebujesz konta, aby to wypróbować. Przykłady używają tenantId: "demo", współdzielonego sandboxu, a krok 2 opisuje przejście na własny.

Liczba komentarzy na stronie indeksu Internal Link

Na stronie indeksu nie renderuj jednego widżetu liczenia komentarzy na wiersz. To powoduje jedno żądanie na każdy post. Użyj liczenia zbiorczego, które wymaga jednego żądania dla całej strony.

Oznacz każdy wiersz urlId, którego używa wątek, a następnie załaduj widżet zbiorczy jednorazowo:

Zbiorcze liczenie komentarzy na indeksie
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

Skrypt znajduje każdy element .fast-comments-count na stronie i wstawia jego liczbę.

data-fast-comments-url-id musi odpowiadać urlId, którego używa widżet komentarzy posta. Jeśli widżet używa slug, znacznik używa slug. Niezgodność powoduje wyświetlenie zera w wątku, który ma komentarze.

Skrypt nasłuchuje window.FastCommentsBulkCountConfig, więc nie ma znaczenia, czy ustawisz konfigurację przed, czy po znaczniku skryptu.

Bezpieczne SSO z std/oauth Internal Link

Jeśli Twój val już wie, kim jest odwiedzający, Secure SSO przekazuje tę tożsamość widgetowi, dzięki czemu nigdy nie zobaczy on drugiego ekranu logowania. Nie ma endpointów do zbudowania ani niczego do wywołania w czasie działania: obliczasz trzy wartości po stronie serwera i przekazujesz je w konfiguracji widgetu.

Val Town oferuje logowanie bez konfiguracji dzięki std/oauth, więc odwiedzający może zalogować się kontem Val Town, które już posiada. Zamień to na to, czego używa Twoja aplikacja; część FastComments się nie zmienia.

Zbuduj payload na serwerze

API secret podpisuje payload i nigdy nie może trafić do kodu przeglądarki. Zainstaluj SDK z npm, które działa na runtime Deno Val Town bez zmian:

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() zwraca { userDataJSONBase64, verificationHash, timestamp }. Tylko te trzy wartości trafiają do przeglądarki. Secret podpisuje je, a następnie jest odrzucany, więc nic na stronie nie pozwoli czytelnikowi podrobić innego użytkownika.

Przekaż go do widgetu

Konfiguracja widgetu z 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 dodaje za Ciebie GET /auth/login, GET /auth/callback i POST /auth/logout. Zauważ, że logout to POST, podczas gdy widget przechodzi do logoutURL za pomocą GET, więc skieruj logoutURL na małą własną trasę, która wysyła ten POST.

Gdy odwiedzający jest wylogowany, przekaż sso zawierające tylko loginURL. Widget wyświetli wtedy monit o logowanie zamiast anonimowego pola komentarzy.

Rzeczy, które mogą pójść nie tak

timestamp to epoch w milisekundach, nie może być w przyszłości i nie może być starszy niż dwa dni. Generuj go na serwerze w tym samym żądaniu, w którym obliczasz hash. Generowanie go w przeglądarce to klasyczny błąd: wartość różni się od tej, która została podpisana, i każdy komentarz jest odrzucany.

Nigdy nie ustawiaj isAdmin ani isModerator na podstawie dostawcy tożsamości. Zalogowanie się kontem Val Town nie mówi nic o tym, kto powinien moderować Twoją stronę.

Zobacz przewodnik SSO po pełną listę pól, wątki ograniczone grupami oraz odznaki.

Odbieranie webhooków Internal Link

A val jest naturalnym odbiorcą webhooków: ma stabilny URL, może weryfikować podpis i ma wbudowaną bazę SQLite oraz przechowywanie blobów.

FastComments podpisuje ${timestamp}.${body} przy użyciu sekretu API Twojego konta i wysyła dwa nagłówki:

Nagłówki webhooka
Copy CopyRun External Link
1
2X-FastComments-Timestamp: 1789004710 unix seconds, not milliseconds
3X-FastComments-Signature: sha256=<hex>
4

Metoda przenosi zdarzenie: PUT dla utworzonego lub zaktualizowanego komentarza, DELETE dla usuniętego.

Weryfikacja dostawy
Copy CopyRun External Link
1
2import { createHmac, timingSafeEqual } from "node:crypto";
3
4async function receive(c) {
5 // Dokładne bajty, które nadeszły. Nie używaj c.req.json() i nie serializuj ponownie.
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 // Odrzuć przestarzałe dostawy, aby przechwycony request nie mógł być odtworzony później.
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

Dwie rzeczy, które gryzą

Zweryfikuj surowe bajty. Parsowanie JSON i ponowne serializowanie zmienia kolejność kluczy oraz białe znaki, więc hash się różni i każda dostawa kończy się niepowodzeniem bez oczywistej przyczyny. To najczęstszy powód, dla którego odbiorca webhooka „po prostu nie działa”.

Porównuj w stałym czasie. Zwykłe === na podpisie ujawnia, ile bajtów się zgadza, co wystarczy, aby podrobić jeden bajt naraz.

Obsługa zdarzeń

Odpowiadaj szybko. FastComments ponawia próbę przy kodzie innym niż 2xx, a endpoint, który ciągle zawodzi, zostaje ostatecznie automatycznie wyłączony, więc wykonuj rzeczywistą pracę po odpowiedzi, a nie w linii.

Uczyń to działanie idempotentnym względem identyfikatora komentarza. Ponowna próba jest podpisywana ponownie z nowym znacznikiem czasu, a ten sam identyfikator komentarza pojawia się ponownie przy edycji i usunięciu, więc nie ma nic stabilnego, na czym można by deduplikować.

Przykładowe Vals Internal Link

Cztery publiczne valy do remiksu, z których każdy obejmuje jeden element tego przewodnika.

Blog with comments (live) to blog w Markdown z wątkiem komentarzy pod każdym wpisem i zbiorczymi licznikami komentarzy na stronie głównej. Działa od razu po remiksie, a jedna zmienna środowiskowa wskazuje go na Twoje własne konto.

SSO demo (live) loguje odwiedzającego za pomocą konta Val Town i przekazuje tę tożsamość do widgetu, więc nie ma drugiego logowania.

Webhook receiver (live) weryfikuje podpis HMAC przy każdej dostawie i zapisuje zdarzenia w SQLite. Ma przycisk, który podpisuje testowy payload i dostarcza go do samego siebie, więc możesz zobaczyć, jak weryfikacja działa, zanim skonfigurujesz prawdziwy webhook.

Agent skills (live) to biblioteka umiejętności agenta FastComments obejmująca widget, SSO, REST API, moderację i migrację z Disqus. Zremiksuj ją, a agent Val Town, Townie, automatycznie pobierze umiejętności z skills/, więc Twój agent będzie wiedział, jak podłączyć komentarze, bez wklejania dokumentacji do czatu.

Te same umiejętności można zainstalować gdziekolwiek indziej za pomocą npx skills add fastcomments/skills.