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 אינו משתנה.

בניית ה-payload בשרת

סוד ה-API חותם את ה-payload ואסור שיגיע לקוד בדפדפן. התקן את ה-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. שים לב שההתנתקות היא POST, בעוד שהווידג'ט מנווט אל logoutURL עם GET, ולכן כוון את logoutURL לנתיב קטן משלך ששולח את ה-POST.

כאשר המבקר מנותק, העבר sso עם loginURL בלבד. הווידג'ט יציג אז בקשת התחברות במקום תיבת תגובות אנונימית.

דברים שמשתבשים

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

לעולם אל תגדיר 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.