FastComments.com

הוספת תגובות חיות לאפליקציות Val Town

Val Town מריץ TypeScript על Deno, ולכן val הוא שרת אמיתי. זה עושה אותו מתאים מאוד ל‑FastComments: הווידג׳ט הוא תג script בעמוד, וכל דבר שדורש סוד, כמו Secure SSO או אימות webhook, יכול לרוץ בצד השרת באותו val.

המדריך הזה מכסה הוספת ווידג׳ט התגובות ל‑HTTP val, הצגת ספירות תגובות בעמוד אינדקס, התחברות משתמשים עם חשבון Val Town שכבר יש להם, וקבלת webhook של תגובות.

אינכם צריכים חשבון כדי לנסות זאת. הדוגמאות משתמשות ב‑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, ולכן לא משנה אם אתה מגדיר את ההגדרה לפני או אחרי תג הסקריפט.

SSO מאובטח עם std/oauth Internal Link

אם ה‑val שלך כבר יודע מי המבקר, Secure SSO מעביר את הזהות לווידג'ט כך שהם לעולם לא יראו התחברות שנייה. אין צורך לבנות נקודות קצה ואין מה לקרוא בזמן ריצה: אתה מחשב שלושה ערכים בצד השרת ומעביר אותם בתצורת הווידג'ט.

Val Town מספק כניסה ללא צורך בתצורה עם std/oauth, כך שהמבקר יכול להתחבר עם חשבון Val Town שכבר יש לו. החלף זאת בכל מה שהאפליקציה שלך משתמשת בו; החלק של FastComments אינו משתנה.

Build the payload on the server

הסוד של ה‑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 }. שלושת הערכים האלה הם כל מה שמגיע לדפדפן. הסוד חותם עליהם ולאחר מכן נזרק, ולכן שום דבר בעמוד אינו מאפשר לקורא לזייף משתמש שונה.

Pass it to the widget

תצורת וידג'ט עם 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. הווידג'ט יציג אז תזכורת התחברות במקום תיבת תגובה אנונימית.

Things that go wrong

timestamp הוא זמן אפוק (epoch) במילישניות, אסור שיהיה בעתיד, וחייב להיות לא יותר משני ימים ישנים. צור אותו בצד השרת באותו בקשה שמחשבת את החתימה. יצירתו בדפדפן היא הכשל הקלאסי: הערך שונה מהערך שחולץ ולכן כל תגובה נדחית.

לעולם אל תגדיר isAdmin או isModerator מספק הזהות. התחברות עם חשבון Val Town לא אומרת דבר על מי צריך למודרציה של האתר שלך.

ראה את מדריך SSO לקבלת רשימת השדות המלאה, נושאים עם גישה קבוצתית, ותגים.

קבלת Webhooks 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 // ...טפל ב‑JSON.parse(rawBody)
28 return Response.json({ received: true });
29}
30
31app.put("/", receive);
32app.delete("/", receive);
33

שני דברים שמציקים

אמת את הבתים הגולמיים. ניתוח ה‑JSON וסיריאליזציה מחדש משנים את סדר המפתחות והרווחים, ולכן ההאש שונה וכל משלוח נכשל ללא סיבה ברורה. זהו הסיבה הרגילה שמקלט ווב‑הוק "פשוט לא עובד".

השווה בזמן קבוע. השוואה פשוטה === על החתימה חושפת כמה בתים תואמים, וזה מספיק כדי לזייף בת אחד בכל פעם.

טיפול באירועים

ענה במהירות. FastComments מנסה מחדש על תגובה שאינה 2xx, וקצה שממשיך להיכשל מושבת בסופו של דבר באופן אוטומטי, ולכן יש לבצע עבודה אמיתית אחרי שליחת התגובה ולא באופן מקומי.

הפוך את הפעולה הזאת לאידמפוטנטית על מזהה ההערה. ניסיון חוזר נחתם מחדש עם חותמת זמן חדשה, וה‑comment id עצמו מגיע שוב בעריכה ובמחיקה, ולכן אין דבר יציב שניתן להשתמש בו לדדופלקציה.


דוגמאות Vals Internal Link

ארבעה vals ציבוריים שאתה יכול לשנות, כל אחד מכסה חלק אחד של המדריך הזה.

בלוג עם תגובות (חי) הוא בלוג Markdown עם שרשור תחת כל פוסט וספירת תגובות מרוכזת באינדקס. הוא פועל ברגע שאתה משנה אותו, ומשתנה סביבתי אחד מצביע אותו לחשבון שלך.

הדגמת SSO (חי) מחבר את המבקר עם חשבון Val Town שלו ומעביר את הזהות לווידג'ט, כך שאין צורך בכניסה שנייה.

מקבל Webhook (חי) מאמת את חתימת ה‑HMAC בכל משלוח ושומר אירועים ב‑SQLite. יש לו כפתור שמחתום על מטען בדיקה ומספק אותו לעצמו, כך שאתה יכול לצפות באימות מוצלח לפני קביעת Webhook אמיתי.

כישורי סוכן (חי) היא ספרייה של כישורי סוכן FastComments המכסים את הווידג'ט, SSO, ה‑REST API, מודרציה והמעבר מ‑Disqus. שנה אותה והסוכן של Val Town, Townie, יטען את הכישורים מתוך skills/ אוטומטית, כך שהסוכן שלך יודע איך לחבר תגובות ללא צורך בהדבקת תיעוד בצ'אט.

הכישורים זהים מותקנים בכל מקום אחר עם npx skills add fastcomments/skills.