FastComments.com

Adicionar Comentários ao Vivo aos Apps Val Town

Val Town executa TypeScript no Deno, então um val é um servidor real. Isso o torna uma boa escolha para o FastComments: o widget é uma tag script na página, e qualquer coisa que precise de um segredo, como Secure SSO ou verificação de webhook, pode ser executada no lado do servidor no mesmo val.

Este guia cobre a adição do widget de comentários a um val HTTP, a exibição de contagens de comentários em uma página de índice, o login de usuários com a conta Val Town que eles já possuem e o recebimento de webhooks de comentários.

Você não precisa de uma conta para experimentá-lo. Os exemplos usam tenantId: "demo", um sandbox compartilhado, e o Passo 2 cobre a troca para o seu próprio.

Contagem de Comentários em uma Página de Índice Internal Link

Em uma página de índice, não renderize um widget de contagem de comentários por linha. Isso gera uma solicitação por postagem. Use a contagem em massa, que faz uma única solicitação para a página inteira.

Marque cada linha com o urlId que seu thread usa e, em seguida, carregue o widget em massa uma única vez:

Contagens de comentários em massa em um índice
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

O script encontra cada elemento .fast-comments-count na página e preenche sua contagem.

data-fast-comments-url-id deve corresponder ao urlId que o widget de comentários da postagem usa. Se o widget usar o slug, o marcador usará o slug. Uma incompatibilidade exibirá zero em um thread que tem comentários.

O script verifica window.FastCommentsBulkCountConfig, portanto não importa se você definir a configuração antes ou depois da tag de script.

SSO Seguro com std/oauth Internal Link

Se o seu val já sabe quem é o visitante, o Secure SSO entrega essa identidade ao widget para que ele nunca veja um segundo login. Não há endpoints para construir e nada para chamar em tempo de execução: você calcula três valores no servidor e os passa na configuração do widget.

O Val Town oferece login sem configuração com std/oauth, então o visitante pode entrar com a conta Val Town que já possui. Troque isso por qualquer autenticação que seu app use; a parte do FastComments não muda.

Construa o payload no servidor

O segredo da API assina o payload e nunca deve chegar ao código do navegador. Instale o SDK do npm, que funciona no runtime Deno do Val Town como está:

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() retorna { userDataJSONBase64, verificationHash, timestamp }. Esses três valores são tudo o que chega ao navegador. O segredo os assina e é então descartado, então nada na página permite que um leitor falsifique um usuário diferente.

Passe-o ao widget

Configuração do widget com 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 adiciona GET /auth/login, GET /auth/callback e POST /auth/logout para você. Note que o logout é um POST, enquanto o widget navega para logoutURL com um GET, então aponte logoutURL para uma pequena rota sua que submeta o POST.

Quando o visitante está deslogado, passe sso com apenas um loginURL. O widget então exibirá um prompt de login em vez de uma caixa de comentários anônima.

Coisas que dão errado

timestamp está em milissegundos de época, não deve estar no futuro e não deve ter mais de dois dias. Gere-o no servidor na mesma requisição que calcula o hash. Gerá-lo no navegador é a falha clássica: o valor difere daquele que foi assinado e todo comentário é rejeitado.

Nunca defina isAdmin ou isModerator a partir do provedor de identidade. Entrar com uma conta Val Town não diz nada sobre quem deve moderar seu site.

Consulte o guia de SSO para a lista completa de campos, threads restritas a grupos e badges.

Recebendo Webhooks Internal Link

Um val é um receptor de webhook natural: ele tem uma URL estável, pode verificar uma assinatura e inclui SQLite e armazenamento de blobs embutidos.

FastComments assina ${timestamp}.${body} com o segredo da API da sua conta e envia dois cabeçalhos:

Cabeçalhos do webhook
Copy CopyRun External Link
1
2X-FastComments-Timestamp: 1789004710 unix seconds, not milliseconds
3X-FastComments-Signature: sha256=<hex>
4

O método transporta o evento: PUT para um comentário criado ou atualizado, DELETE para um comentário excluído.

Verificando uma entrega
Copy CopyRun External Link
1
2import { createHmac, timingSafeEqual } from "node:crypto";
3
4async function receive(c) {
5 // Os bytes exatos que chegaram. NÃO use c.req.json() e reserialize.
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 // Rejeitar entregas antigas para que uma solicitação capturada não possa ser reproduzida mais tarde.
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 // ...tratar JSON.parse(rawBody)
28 return Response.json({ received: true });
29}
30
31app.put("/", receive);
32app.delete("/", receive);
33

Duas coisas que dão dor

Verifique os bytes brutos. Analisar o JSON e reserializá‑lo altera a ordem das chaves e os espaços em branco, portanto o hash difere e cada entrega falha sem causa óbvia. Essa é a razão usual de um receptor de webhook "simplesmente não funciona".

Compare em tempo constante. Um simples === na assinatura vaza quantos bytes coincidem, o que é suficiente para forjar um byte de cada vez.

Manipulando eventos

Responda rapidamente. FastComments tenta novamente em caso de resposta não‑2xx, e um endpoint que continua falhando é eventualmente desativado automaticamente, portanto faça o trabalho real após responder, em vez de inline.

Torne esse trabalho idempotente com base no ID do comentário. Uma nova tentativa é assinada novamente com um timestamp novo, e o mesmo ID de comentário chega novamente em edição e exclusão, portanto não há nada estável para desduplicar.

Vals de Exemplo Internal Link

Quatro vals públicos que você pode remixar, cada um cobrindo uma parte deste guia.

Blog com comentários (live) é um blog em Markdown com um tópico sob cada post e contagens de comentários em massa no índice. Ele funciona no momento em que você o remixar, e uma variável de ambiente aponta para sua própria conta.

Demo SSO (live) autentica o visitante com a conta do Val Town dele e entrega essa identidade ao widget, de modo que não há um segundo login.

Receptor de Webhook (live) verifica a assinatura HMAC em cada entrega e armazena os eventos em SQLite. Ele possui um botão que assina uma carga de teste e a entrega a si mesmo, para que você possa observar a verificação ser bem‑sucedida antes de configurar um webhook real.

Habilidades de agente (live) é uma biblioteca de habilidades de agente FastComments que cobre o widget, SSO, a API REST, moderação e migração do Disqus. Remix‑a e o agente da Val Town, Townie, carrega as habilidades de skills/ automaticamente, de modo que seu agente saiba como integrar comentários sem que você precise colar a documentação no chat.

As mesmas habilidades podem ser instaladas em qualquer outro lugar com npx skills add fastcomments/skills.