2 คะแนน โดย GN⁺ 5 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • หากสร้างเซิร์ฟเวอร์ ActivityPub เอง มักจะติดตั้งแต่คำขอ Follow แรกด้วย 401 Unauthorized ที่ไม่มีคำอธิบาย และ Fedify คือเฟรมเวิร์ก TypeScript ที่ย้ายภาระเรื่อง ลายเซ็น, JSON-LD, การส่งต่อ และความปลอดภัย ออกไปไว้นอกโค้ดแอปพลิเคชัน
  • การยืนยันตัวตนใน fediverse ใช้ทั้งร่างที่หมดอายุ draft-cavage-http-signatures-12 และมาตรฐาน RFC 9421 ควบคู่กันไป และหากรวมลายเซ็นระดับเอกสารด้วย ก็ต้องรับมือกับ กลไกลายเซ็นสี่แบบ รวมถึงคีย์ RSA และ Ed25519
  • แม้จะเป็นกิจกรรม ActivityPub เดียวกัน แต่ใน JSON-LD ก็อาจมาได้หลายรูปแบบ เช่น สตริง อาร์เรย์ อ็อบเจ็กต์แบบอินไลน์ หรือการอ้างอิง URI ทำให้ยิ่งเขียนเองมากเท่าไร โค้ดเชิงป้องกัน ก็ยิ่งกระจายไปทั่วทั้งโค้ดเบส
  • ในการส่งต่อแบบกระจาย อาจเกิดปัญหาอย่าง “โพสต์ซอมบี้” ที่ Delete มาถึงก่อน Create จึงต้องมีคิว การลองใหม่ ความเป็น idempotent การรับประกันลำดับ และ circuit breaker
  • Fedify มีการเชื่อมต่อกับเว็บเฟรมเวิร์ก 13 แบบ, อะแดปเตอร์ KV และ message queue, รวมถึง CLI, linter, debugger และ OpenTelemetry ทำให้เริ่มพัฒนาแอปแบบ federated ได้ โดยไม่ต้องมีความรู้เชิงลึกเรื่อง ActivityPub

ปัญหาที่เจอเมื่อทำ ActivityPub เองโดยตรง

  • หากจะส่งกิจกรรม Follow แรกไปยัง Mastodon ต้องจัดการทั้งการเขียน JSON, เซ็นคำขอ HTTP และ POST แต่ถ้าล้มเหลว อาจได้กลับมาเพียงบรรทัดเดียวว่า 401 Unauthorized
    • สาเหตุอาจเป็นความคลาดเคลื่อนของเวลาในเฮดเดอร์ Date, ความผิดพลาดของแฮช Digest, ตัวพิมพ์เล็กใหญ่ของ (request-target), วิธีแสดงคีย์สาธารณะ เป็นต้น
    • ถ้าเซิร์ฟเวอร์ปลายทางไม่บอกเหตุผล ก็อาจต้องไปอ่านโค้ดของเซิร์ฟเวอร์อื่นเพื่อดีบัก
  • Fedify เริ่มต้นมาจากกระบวนการสร้าง Hollo ซึ่งเป็นเซิร์ฟเวอร์ไมโครบล็อกแบบผู้ใช้เดี่ยว
    • เมื่อภาระในการทำ ActivityPub กลืนงานพัฒนาผลิตภัณฑ์ จึงถูกทำออกมาเป็นเฟรมเวิร์กที่จำเป็นก่อนตัวแอป
  • ความยากหลัก ๆ กระจุกอยู่ที่ มาตรฐานลายเซ็น, รูปแบบเอกสาร JSON-LD, การส่งต่อแบบกระจาย, ธรรมเนียมเฉพาะของแต่ละ implementation และการตั้งค่าความปลอดภัยพื้นฐาน

มาตรฐานลายเซ็นไม่ได้มีแค่อย่างเดียว

  • การยืนยันตัวตนระหว่างเซิร์ฟเวอร์ใช้ HTTP signature แต่ใน fediverse จริงมีทั้งร่างที่หมดอายุ draft-cavage-http-signatures-12 และมาตรฐาน RFC 9421 อยู่ร่วมกัน
  • ไม่สามารถรู้ล่วงหน้าได้ว่าเซิร์ฟเวอร์ไหนรับลายเซ็นแบบใด จึงต้องลองเซ็นด้วยวิธีหนึ่งก่อน ถ้าถูกปฏิเสธก็ต้องเซ็นใหม่อีกแบบ และจดจำวิธีที่สำเร็จแยกตามแต่ละเซิร์ฟเวอร์
  • HTTP signature พิสูจน์ได้เพียงผู้ส่งคำขอ ดังนั้นในกรณีอย่าง inbox forwarding ที่ต้องส่งต่อกิจกรรมที่ได้รับไปยังบุคคลที่สาม ก็จำเป็นต้องมีลายเซ็นที่ติดกับตัวเอกสารด้วย
    • กลไกที่ต้องใช้มีทั้งหมดสี่แบบ รวมถึง Linked Data Signatures และ Object Integrity Proofs
    • ต้องจัดการคีย์ทั้ง RSA และ Ed25519 ควบคู่กันไป

รูปแบบเอกสาร JSON-LD เปลี่ยนไปตลอด

  • รูปแบบการส่งของ ActivityPub คือ JSON-LD และแม้จะเป็นกิจกรรม Create ที่มีความหมายเดียวกัน ก็สามารถแสดงออกมาได้หลายแบบ
    • actor อาจเป็นสตริง URI หรือเป็นอ็อบเจ็กต์ Person แบบอินไลน์ก็ได้
    • to อาจเป็นสตริงตัวเดียว หรือเป็นอาร์เรย์ก็ได้
    • object อาจเป็นได้ทั้งอ็อบเจ็กต์แบบอินไลน์หรือ URI
  • ที่อยู่สำหรับระบุการเผยแพร่สาธารณะก็ใช้ได้ทั้งสามแบบคือ https://www.w3.org/ns/activitystreams#Public, as:Public, Public
  • หากจะประมวลผลให้ตรงตามสเปก ต้องทำ normalization โดยขยาย (expansion) แล้วบีบอัด (compaction) ผ่านตัวประมวลผล JSON-LD
    • หลาย implementation กลับปฏิบัติกับมันเหมือนเป็น “แค่ JSON” จนพังแบบเงียบ ๆ เมื่อเจอรูปแบบที่เซิร์ฟเวอร์บางตัวส่งออกมา
  • ถ้าทำเองโดยตรง จะมี โค้ดเชิงป้องกัน โผล่เต็มไปหมดเพื่อตรวจสอบว่าค่าเป็นสตริง อาร์เรย์ อ็อบเจ็กต์ หรือเป็น URI ที่ต้องไปดึงมา

การส่งต่อแบบกระจายและ “โพสต์ซอมบี้”

  • ถ้าผู้ใช้โพสต์ข้อความแล้วเห็นพิมพ์ผิดจึงลบทันที เซิร์ฟเวอร์จะส่ง Create ตามด้วย Delete แต่ด้วยสภาพเครือข่าย เซิร์ฟเวอร์ผู้รับอาจได้รับ Delete ก่อน
    • ถ้ามองข้ามการลบของโพสต์ที่ยังไม่มีอยู่ แล้วมาประมวลผล Create ทีหลัง โพสต์ที่ผู้เขียนเชื่อว่าลบไปแล้วก็อาจยังคงอยู่บนเซิร์ฟเวอร์นั้นต่อไป
  • หากมีผู้ติดตาม 5,000 คน โพสต์หนึ่งครั้งอาจสร้างการส่ง HTTP หลายพันรายการ และหากจัดการทั้งหมดในตัว request handler การตอบสนองของปุ่มโพสต์อาจช้าลงหรือถึงขั้นทำให้เซิร์ฟเวอร์ล่มได้
  • ต่อให้ใช้คิว ก็ยังต้องกำหนดตารางการลองใหม่ของการส่งที่ล้มเหลว, exponential backoff, จำนวนครั้งที่ลองใหม่, ความแตกต่างระหว่าง 500 Internal Server Error กับ 410 Gone, การล้างรายชื่อผู้ติดตามของเซิร์ฟเวอร์ที่หายไป และการจัดการโฮสต์ที่ขัดข้องยาวนาน
  • พื้นที่นี้ใกล้เคียงกับงาน วิศวกรรมระบบกระจาย มากกว่าการทำโปรโตคอลแบบตรงไปตรงมา

แค่ทำตามสเปกก็ยังไม่จบเรื่องการทำงานร่วมกัน

  • ต่อให้ทำตามสเปกอย่างสมบูรณ์ ปัญหาการทำงานร่วมกับ implementation จริงใน fediverse ก็ยังคงอยู่
  • secure mode ของ Mastodon ใช้ authorized fetch ที่บังคับ HTTP signature แม้กับคำขอ GET
    • หากเซิร์ฟเวอร์ทั้งสองฝั่งเปิด secure mode เหมือนกัน จะเกิดภาวะชะงักงันที่การดึงคีย์สาธารณะของอีกฝ่ายต้องใช้ลายเซ็น แต่การตรวจสอบลายเซ็นนั้นก็ต้องให้อีกฝ่ายดึงคีย์สาธารณะของเราก่อน
    • ชุมชนจึงหลบปัญหาด้วยการเซ็นผ่าน instance actor ที่แทนตัวเซิร์ฟเวอร์เอง แต่สิ่งนี้ไม่ได้อยู่ในสเปก
  • Threads ไม่สามารถพาร์สกิจกรรมที่ actor มาเป็นอ็อบเจ็กต์แบบอินไลน์ได้ ดังนั้นเวลาส่งไปยัง Threads ต้องส่ง actor เป็น URI
  • Lemmy จะปฏิเสธแบบเงียบ ๆ หากไม่มีฟิลด์ของ Group actor ที่ Mastodon ไม่ได้บังคับ
    • ตัวอย่างเช่น moderators collection และ featured collection ที่เชื่อมผ่าน attributedTo
  • Misskey มีการขยาย vocabulary ของตัวเอง และแค่เรื่อง quote post ก็มีการใช้ชื่อพร็อพเพอร์ตี้อยู่สามแบบตามแต่ละ implementation
  • การทำงานร่วมกันไม่ใช่งานที่ปรับครั้งเดียวแล้วจบ แต่เป็นเรื่องที่ต้องคอยดูแลต่อเนื่อง

สถานะตั้งต้นของการทำเองนั้นไม่ปลอดภัย

  • หากข้ามการตรวจสอบลายเซ็นของกิจกรรมขาเข้า ใครก็สามารถใส่ Follow หรือ Delete ปลอมเข้ามาได้
  • หากไม่จำกัด document loader กิจกรรมอันตรายอาจชี้ไปที่ http://169.254.169.254/ หรือเครือข่ายภายใน ทำให้เซิร์ฟเวอร์กลายเป็นพร็อกซี SSRF
  • หากละเลยการตรวจสอบแหล่งที่มาของ embedded object เซิร์ฟเวอร์ใด ๆ ก็สามารถปล่อยเอกสารที่ทำให้ดูเหมือนบุคคลใดบุคคลหนึ่งเป็นคนพูดได้
  • กับดักเหล่านี้มักไม่เห็นชัดในทันที และก่อนจะถูกนำไปใช้โจมตี ทุกอย่างอาจดูเหมือนทำงานได้ตามปกติ

สิ่งที่ Fedify จัดการแทน

  • Fedify เป็นไลบรารี TypeScript สำหรับสร้างแอปเซิร์ฟเวอร์แบบเฟเดอเรตด้วย ActivityPub และมาตรฐานที่เกี่ยวข้อง
  • ทำงานได้บน Deno, Node.js และ Bun รวมถึงรองรับ edge runtime อย่าง Cloudflare Workers
  • เป้าหมายของการออกแบบคือย้ายเรื่องลายเซ็น, JSON-LD, การส่งต่อ, ความต่างระหว่างอิมพลีเมนเทชัน และรายละเอียดด้านความปลอดภัย ออกจากโค้ดแอปพลิเคชัน
  • การจัดการลายเซ็น

    • เมื่อลงทะเบียน actor dispatcher และ key pair dispatcher ก็สามารถเผยแพร่ actor หนึ่งตัวขึ้นสู่ fediverse ได้
    • ทุกคำขอขาออกจะมีลายเซ็นแนบไปด้วย
    • สำหรับคีย์ RSA จะสร้างทั้ง HTTP Signatures และ Linked Data Signatures
    • หากเพิ่มคีย์ Ed25519 ก็จะมี Object Integrity Proofs ติดไปด้วย
    • กลไกทั้งสี่แบบสามารถอยู่ร่วมกันใน activity เดียว และผู้รับจะตรวจสอบด้วยวิธีที่แข็งแรงที่สุดที่ตนเข้าใจ
    • Fedify จัดการ double-knocking ให้โดยตรง
      • การติดต่อครั้งแรกจะส่งด้วย RFC 9421 และถ้าถูกปฏิเสธก็จะลองใหม่ด้วย draft-cavage
      • วิธีที่สำเร็จจะถูกแคชแยกตามแต่ละเซิร์ฟเวอร์
      • หากการตอบกลับแบบปฏิเสธมี Accept-Signature challenge ก็จะเซ็นใหม่ตามองค์ประกอบที่เซิร์ฟเวอร์ร้องขอ
    • ลายเซ็นขาเข้า จะถูกตรวจสอบก่อนที่โค้ดแอปพลิเคชันจะได้เห็น และ activity ที่ตรวจสอบไม่ผ่านจะไปไม่ถึง listener
    • แค่ลงทะเบียน actor dispatcher ก็จะได้เซิร์ฟเวอร์ WebFinger RFC 7033 มาด้วย ทำให้ค้นหา actor ในช่องค้นหาของ Mastodon ได้ด้วยรูปแบบ @alice@example.com
  • จัดการ type แทน JSON-LD

    • Fedify มี คลาสราว 80 คลาส ที่ครอบคลุม Activity Vocabulary ทั้งชุดและส่วนขยายหลักจากผู้ให้บริการต่าง ๆ
    • คลาสเหล่านี้มี type, immutable และ accessor จะช่วยซ่อนความต่างของรูปแบบเอกสารที่ JSON-LD อนุญาตไว้
    • lookupObject() รับ handle แล้วทำขั้นตอน lookup ทั้งหมด รวมถึง WebFinger discovery
    • accessor อย่างเช่น getFollowers() ทำงานได้เหมือนกันไม่ว่าค่าจะเป็น URI reference หรือ inline object และค่าที่ดึงมาแล้วจะถูกแคช
    • ความต่างเฉพาะของแต่ละผู้ให้บริการก็ถูกซ่อนไว้หลัง API เช่นกัน
      • คุณสมบัติ quote ทั้งสามแบบคือ quoteUri, _misskey_quote, quoteUrl ถูกหลอมรวมไว้หลัง API เดียวร่วมกับ quote ของ FEP-044f ที่เพิ่ง появขึ้นใหม่
      • คุณสมบัติ isCat ของ Misskey ก็มีเป็น type เช่นกัน จึงจัดการได้อย่าง type-safe
  • โครงสร้างพื้นฐานการส่งต่อและการรับประกันลำดับ

    • หากเชื่อม message queue เข้ากับ createFederation() การส่งต่อจะย้ายไปทำงานเบื้องหลัง และเมื่อเกิดความล้มเหลวจะมีการ retry อัตโนมัติด้วย exponential backoff โดยค่าปริยายสูงสุด 10 ครั้ง
    • เมื่อโพสต์หนึ่งชิ้นต้องส่งต่อถึงผู้ติดตามหลายพันคน ระบบ two-stage fan-out จะทำงาน
      • ข้อความรวมหนึ่งรายการจะถูกใส่เข้าคิว
      • worker เบื้องหลังจะแยกมันออกเป็นงานส่งต่อรายเซิร์ฟเวอร์
      • ปุ่มโพสต์จะตอบกลับได้ทันที
    • เพราะการ retry อาจทำให้ activity เดิมมาถึงสองครั้ง Fedify จึงข้ามรายการซ้ำก่อนถึง handler ด้วย idempotency cache ที่เก็บ activity ที่ประมวลผลแล้วไว้ 24 ชั่วโมง
    • หากระบุ { orderingKey: post.id } ในการเรียก sendActivity() activity ที่ใช้ orderingKey เดียวกันจะถูกส่งถึงแต่ละเซิร์ฟเวอร์ปลายทางตามลำดับที่ส่ง
      • Delete จะไม่แซง Create
      • activity ที่ใช้คีย์ต่างกันจะถูกส่งแบบขนานเพื่อคง throughput ไว้
    • เมื่อเจอ 404 Not Found หรือ 410 Gone ระบบจะหยุด retry และเรียก permanent delivery failure handler ที่ลงทะเบียนไว้
    • หากส่งไปยัง shared inbox ก็ยังสามารถรับรายชื่อผู้ติดตามที่อยู่เบื้องหลังนั้นมาใช้ทำความสะอาดบัญชีที่หายไปได้
    • สำหรับโฮสต์ที่ล้มเหลวซ้ำ ๆ circuit breaker ที่เปิดใช้งานโดยปริยายจะพักการส่งต่อไว้และตรวจสอบการฟื้นตัวเป็นระยะ

แนวปฏิบัติเฉพาะของแต่ละอิมพลีเมนเทชันและค่าเริ่มต้นด้านความปลอดภัย

  • Fedify รองรับ authorized fetch โดยเชื่อม .authorize() เข้ากับ dispatcher เพื่อส่งตัวตนของผู้ขอที่ตรวจสอบแล้วเข้า callback
    • การจัดการอย่างรายการบล็อกหรือคอลเล็กชันส่วนตัวสามารถเขียนเป็นตรรกะแอปพลิเคชันได้
    • มีแพตเทิร์นรองรับแม้กับปัญหา deadlock ของ instance actor
  • ปัญหา inline actor ของ Threads ถูกจัดการด้วย activity transformer ที่เปิดใช้โดยปริยาย ซึ่งจะแปลง inline actor ใน activity ขาออกให้เป็น URI
  • moderators collection ที่ Lemmy ต้องการสามารถเปิดเผยได้ในไม่กี่บรรทัดด้วย custom collection API และมี JSON-LD context ของ Lemmy รวมมาให้ล่วงหน้าแล้ว
  • เมื่อพบปัญหา interoperability ใหม่ ๆ การแก้ไขจะเข้าไปที่ Fedify ไม่ใช่ในแต่ละแอปพลิเคชัน
  • ค่าเริ่มต้นด้านความปลอดภัยเอนเอียงไปทางที่ปลอดภัยกว่า
    • การตรวจสอบลายเซ็นไม่ใช่ฟีเจอร์ที่ต้องเปิด แต่เป็นฟีเจอร์ที่ปิดได้เพื่อการทดสอบ
    • document loader จะบล็อก private address range และ loopback โดยปริยาย และยังคำนึงถึง DNS rebinding ด้วย
    • หากจะเปิดให้เสี่ยงต่อ SSRF ต้องเปิดตัวเลือกที่ตั้งชื่อชัดเจนว่าใช้เพื่อการทดสอบโดยเจตนาเท่านั้น
    • หากแหล่งที่มาของ embedded object ต่างจากเอกสารแม่ accessor จะไม่เชื่อถือและจะดึงใหม่จากต้นทาง
    • โมเดลความปลอดภัยตามแหล่งที่มานี้อิงตาม FEP-fe34

สแตกเดิมและเครื่องมือพัฒนา

  • Fedify ถูกออกแบบมาให้เข้ากับเว็บสแตกที่มีอยู่เดิม และมี การผสานรวมกับเว็บเฟรมเวิร์ก 13 รายการ
    • Express, Hono, Fastify, Koa, NestJS, Elysia
    • Next.js, Nuxt, SvelteKit, Astro, SolidStart, Fresh
  • มิดเดิลแวร์จะจัดการ content negotiation ทำให้ URL เดียวกันสามารถส่ง HTML ให้เบราว์เซอร์ และส่ง JSON-LD ให้เฟดิเวิร์สได้
  • สตอเรจของ Fedify เองต้องการเพียง อินเทอร์เฟซ key-value เดียว
    • มีอะแดปเตอร์สำหรับ Redis, PostgreSQL, MySQL/MariaDB, SQLite, Deno KV, Cloudflare Workers KV และ in-memory
  • message queue มีให้ 8 แบบ รวมถึง PostgreSQL, Redis, AMQP/RabbitMQ และหากไม่มีตัวที่เหมาะ ก็สามารถลงมือ implement อินเทอร์เฟซเองได้
  • ข้อมูลโดเมนสามารถคงไว้ในฐานข้อมูลและ ORM เดิมได้ตามเดิม
  • หากใช้งาน federation อยู่แล้วด้วยไลบรารีอื่น ก็สามารถย้ายมาจาก activitypub-express เป็นต้นได้โดยไม่สูญเสียผู้ติดตามเดิม ด้วย คู่มือย้ายระบบ และสคริปต์ migration ข้อมูล
  • มีแพ็กเกจระดับบนให้ด้วย
    • @fedify/relay ให้เซิร์ฟเวอร์ ActivityPub relay ที่สมบูรณ์ได้ด้วยการเรียกฟังก์ชันเพียงครั้งเดียว
    • @fedify/backfill จะไล่ตามเฟดิเวิร์สเพื่อกู้คืนเธรดบทสนทนาที่ไม่สมบูรณ์
  • เครื่องมือสำหรับ development loop

    • fedify init ใช้ scaffold โปรเจกต์ได้ในบรรทัดเดียว
    • fedify tunnel เปิดเผยเซิร์ฟเวอร์โลคัลผ่าน HTTPS เพื่อให้ทดสอบกับ Mastodon จริงได้
    • fedify inbox รันเซิร์ฟเวอร์ inbox ชั่วคราวไว้รับ activity ที่เซิร์ฟเวอร์ส่งมา
    • fedify lookup ใช้ตรวจสอบอ็อบเจ็กต์ที่โพสต์โดยเซิร์ฟเวอร์อื่นได้
    • fedify lookup --authorized-fetch จะสร้างคีย์คู่แบบใช้ครั้งเดียวและตั้งเซิร์ฟเวอร์ ActivityPub ชั่วคราว เพื่อส่งคำขอที่มีลายเซ็นไปยังอ็อบเจ็กต์ที่อยู่หลัง secure mode
    • @fedify/lint เป็นลินเตอร์เฉพาะ ActivityPub ที่จับบั๊กด้านการทำงานร่วมกันได้ 20 แบบ เช่น actor ที่ไม่มี inbox
    • สามารถรันทดสอบแบบไม่ใช้เครือข่ายได้ด้วย mock ของ @fedify/testing
    • @fedify/debugger เพิ่มแดชบอร์ดดีบักได้ในบรรทัดเดียว ทำให้ดู activity และผลการตรวจสอบลายเซ็นแบบเรียลไทม์ในเบราว์เซอร์ได้
    • ในสภาพแวดล้อม production มี OpenTelemetry instrumentation ในตัว พร้อม 28 span type และ 37 metric
    • ยังมี คู่มือมอนิเตอร์ริง และเครื่องมือทดสอบโหลดสำหรับ ActivityPub อย่าง fedify bench
    • เอกสารทางการประกอบด้วยคู่มือ 30 บทและทิวทอเรียล 5 ชุด ครอบคลุมแนวปฏิบัติจริง เช่น PromQL query และกฎแจ้งเตือนสำหรับดู queue backlog รวมถึงพร็อพเพอร์ตีที่ทำให้อวาตาร์แสดงผลใน Mastodon

กรณีใช้งานจริงและวิธีเริ่มต้น

  • Fedify ถูกใช้งานอยู่ในบริการจริง
    • บริการ ActivityPub ของ Ghost
    • Encyclia ที่เชื่อมบันทึกนักวิจัยของ ORCID เข้ากับเฟดิเวิร์ส
    • SiliconBeest ที่ทำงานแบบ serverless บน Cloudflare Workers
    • Typo Blue แพลตฟอร์มบล็อกของเกาหลี
    • Hollo แพลตฟอร์มไมโครบล็อกแบบผู้ใช้คนเดียว
    • Hackers' Pub ที่ขับเคลื่อนโดยชุมชน
  • ทิวทอเรียลมีตัวอย่างตามขนาดงาน
  • เป้าหมายของ Fedify ไม่ใช่การสร้างผู้เชี่ยวชาญด้าน ActivityPub เพิ่มขึ้น แต่คือการทำให้นักพัฒนาสร้างแอปแบบ federated ได้โดยไม่ต้องรู้รายละเอียดปลีกย่อยของ ActivityPub
  • คำสั่งเริ่มต้นคือ npm init @fedify
  • หากต้องการความช่วยเหลือ สามารถใช้ Matrix room หรือ GitHub Discussions ได้

1 ความคิดเห็น

 
GN⁺ 5 시간 전
ความคิดเห็นจาก Lobste.rs
  • นี่แหละคือเหตุผลที่โปรเจกต์ ActivityPub มี fork ของกันและกันเยอะมาก: การทำความเข้าใจแนวทางของคนอื่นง่ายกว่าการลงมือ implement ทั้งหมดด้วยตัวเอง
    สิ่งที่ผู้เขียนเสนอเองก็ดูไม่ได้ต่างจากพวก fork ของ Misskey หรือ Pleroma ที่เห็นกันบ่อยนักนัก ไลบรารีเองก็มีมุมมองและแนวทางของมันอยู่ และดูเหมือนจะไม่ได้ให้อำนาจควบคุมมากนัก แต่ข้อดีก็คืออย่างน้อยก็ไม่ได้บังคับไปถึง UI เหมือนตอน fork ทั้งเซิร์ฟเวอร์
    ในฐานะคนที่กำลัง implement AP อยู่ ส่วนที่ยากที่สุดคือ ไม่มีวิธีที่ดีในการใช้ JSON-LD ให้ถูกต้องจริง ๆ ถ้าสามารถแปลงอ็อบเจ็กต์เป็น representation มาตรฐานได้ง่าย การทำงานร่วมกันก็น่าจะตามมาเอง แต่ถ้าจะใช้มันเหมือน linked document จริง ๆ ก็ไม่มีประสิทธิภาพเกินไป และถ้าใช้เหมือนเอกสาร JSON ดิบ ๆ ก็จะตายกับเคสยกเว้นมหาศาล จนถึงตอนนี้ผมเลือกแนวทางหลังแล้วก็เจ๊งมาแล้ว

    • โดยเฉพาะถ้าคิดเรื่อง ลายเซ็น ปัญหาเรื่อง “representation มาตรฐานของอ็อบเจ็กต์” ยิ่งสำคัญขึ้นไปอีก การทำ normalization ของ XML สมัยก่อนก็มีไว้เพราะปัญหาลายเซ็นนี่แหละ คือเพื่อให้แน่ใจว่า byte serialization ฝั่งผู้รับตรงกับของฝั่งผู้ส่ง
      ถึงจะไม่ใช่ปัญหาเดียวกับโลกของ JSON-LD แบบเป๊ะ ๆ แต่ก็ไม่ได้ไม่เกี่ยวกันเลย
      อย่างไรก็ดี ผมคิดว่าเทคโนโลยีข้างเคียงของ JSON จำนวนมากก็เจอปัญหาคล้ายกัน มีวิธีทำ JSON Schema เยอะเกินไปสำหรับสคีมาเชิงตรรกะเดียวกัน และเพราะงั้นการทำงานกับเทคโนโลยีรอบ ๆ JSON Schema จึงน่ากลัวแบบขำไม่ออก โดยเฉพาะสคีมาของ OpenAPI ที่คล้ายแต่ไม่เหมือน เป็นหนังสยองขวัญอีกแบบหนึ่ง และต่อให้ยังไม่ต้องนับจำนวนเวอร์ชันร่างของสคีมา มันก็แย่มากพออยู่แล้ว
    • ผมคิดเรื่องการทำ AP server มาสักพักแล้วแต่ยังไม่ได้เริ่มจริง ๆ ดังนั้นควรฟังแบบเผื่อใจเยอะ ๆ สิ่งหนึ่งที่อาจช่วยได้คือแยกแอปพลิเคชันออกเป็นบริการย่อย ๆ และพึ่งพา actor model มากขึ้นเพื่อทำให้มันดูเหมือนอินเทอร์เฟซที่ “รวมเป็นหนึ่งเดียว” ตัวอย่างเช่น เราอาจเรียนรู้จากการแยก MTA กับ MUA ในอีเมลเซิร์ฟเวอร์ได้
      บริการ “MTA” ของ AP จะรับหน้าที่ส่งข้อความจาก outbox และรับข้อความเข้า inbox เอกสาร JSON-LD สำหรับบริการนี้แทบจะเป็นข้อมูลก้อน ๆ มากกว่า ต้อง parse นิดหน่อยเพื่อหาผู้ส่งกับผู้รับ แต่ไม่ได้ต้องทำมากกว่านั้นมากนัก storage ก็อาจเป็นแบบไฟล์ก็ได้ และถ้าจำไม่ผิด go-ap ก็ใช้แนวนี้
      ส่วน “MUA” ของ AP ก็คือตัวแอปพลิเคชันจริง เป็นฝั่งที่ต้องเข้าใจความหมายของ JSON-LD อาจใช้ PostgreSQL เก็บเอกสารเป็น jsonb แล้วใช้ generated columns กับ views เพื่อให้เข้าถึงในรูปแบบที่เป็นมิตรกับ SQL มากขึ้น แบบนั้นก็จะกำหนดวิธีแทนเอกสารที่เหมาะที่สุดตามประเภทของอ็อบเจ็กต์ได้
      อีกตัวอย่างหนึ่งคืออาจ model บริการค้นหาเป็น actor ด้วย แล้วให้มันส่งผลลัพธ์กลับมาทาง outbox ชั่วคราว
  • นี่เป็นรายการที่มีคุณค่ามาก เพราะรวบรวม พฤติกรรมประหลาด ของหลาย implementation และวิธีบรรเทาปัญหาไว้
    น่าเสียดายที่ใน GoActivityPub ยัง implement ได้ไม่ถึงครึ่งของพวกนั้นเลย

  • ตอนต้นบทความเริ่มด้วยเรื่องเทคนิค เลยรู้สึกขอบคุณ แต่พอกลางเรื่องกลับเหมือนหักไปเป็นการ โปรโมตเฟรมเวิร์ก ของตัวเอง ทำให้อรรถรสในการอ่านตกลงไป
    ก็ดีอยู่ที่อย่างน้อยในโลกบางส่วนที่ใช้ TypeScript อาจไม่ต้องมาเจอจุดพิสดารในการ implement พวกนี้ซ้ำอีก แต่ถ้ามีบันทึกเชิง mental model ว่า “ในเงื่อนไขและสถานการณ์นี้จะได้ผลลัพธ์แบบนี้ และต้องแก้แบบนี้” คนที่ไม่ได้ใช้ TypeScript เช่นผู้เขียนโปรเจกต์พี่น้องอย่าง GoActivityPub ก็จะได้อานิสงส์จากความลำบากนั้นด้วย ที่นี่มีพูดถึงบางอย่างอยู่บ้าง แต่บทความเป็นแค่ snapshot ของช่วงเวลาหนึ่ง ขณะที่ตัวโปรเจกต์ดูเหมือนจะค่อย ๆ สะสมบั๊กด้าน interoperability ทั้งหมดไปเรื่อย ๆ ตามกาลเวลา
    ตอนนี้ทางเลือกที่ผมเห็นก็คือไล่อ่าน commit message ที่ไม่ได้เขียนโดยมนุษย์ ทั้งหมด เพื่อแยกให้ออกว่าอันไหนเป็นบั๊กของ Fedify เอง และอันไหนเป็นบั๊กด้าน interoperability
    ยิ่งไปกว่านั้น ทั้งที่ repository นี้ดูเหมือนจะทุ่มสุดตัวให้ AI แต่กลับไม่ทำบัญชีแยกประเภทแบบนั้นก็ชวนให้รู้สึกย้อนแย้ง โฆษณาชวนเชื่อที่ได้ยินเกี่ยวกับ LLM คือมันช่วยทำงานจุกจิกซ้ำ ๆ ให้เป็นอัตโนมัติ ถ้าอย่างนั้นก็ให้ Claude สร้าง GitHub issue หรือดีกว่านั้นคือให้เขียนไฟล์ .md ใน repo เพื่อบันทึกสิ่งที่สังเกตพบและวิธีที่ Fedify ใช้แก้ก็ได้ ไหนจะมี debugger ของตัวเองอยู่แล้ว และยังมี “best practices” ที่ไม่รู้ว่าหมายถึงอะไร อีก แบบนี้ยิ่งเหมาะกับงานนี้มาก

    • เขากำลังขยายปัญหาเล็กน้อยมาก ๆ ให้ดูเหมือนเป็น ความล้มเหลวของ ActivityPub ตัวอย่างเช่น ถ้ามีผู้ติดตาม 5,000 คน โพสต์หนึ่งครั้งก็จะเกิดการส่ง HTTP หลายพันรายการ และถ้าไปทำตรงนั้นใน request handler เลย การตอบสนองปุ่มโพสต์จะใช้เวลา 30 วินาทีหรือไม่ก็ทำเซิร์ฟเวอร์ล้ม ดังนั้นให้ใช้คิว อะไรทำนองนั้น
      ทำไมถึงไปทำ request ไปยังบริการภายนอกแบบ inline ล่ะ? นี่มันพื้นฐานของเว็บแอปพลิเคชันเลย ถ้าต้องคุยกับบริการภายนอกก็ควรส่งไปเป็นงานเบื้องหลัง ถ้าเป็นข้อมูลที่ไม่จำเป็นต่อการตอบ request ก็ยิ่งควรส่งไปเป็นงานเบื้องหลัง ปัญหาที่เกิดจากการทำ request แบบนี้ใน request handler เป็นปัญหาที่ก่อขึ้นเองระดับเหยียบคราดแล้วด้ามเด้งฟาดหน้า ไม่เกี่ยวอะไรกับ ActivityPub เลย
      ถ้าการส่งล้มเหลวก็ต้อง retry จะจัดตารางยังไง ใช้ exponential backoff ไหม จะลองกี่ครั้ง จะนับ 500 Internal Server Error กับ 410 Gone เป็นความล้มเหลวแบบเดียวกันหรือเปล่า ทั้งหมดนี้ก็เป็นแค่ปัญหาทั่วไปของการพัฒนาเว็บแอปพลิเคชัน เป็นปัญหาที่เกิดเวลาให้ task queue ไปยิง request ไปยังบริการภายนอก และไม่เกี่ยวกับ ActivityPub เว็บเฟรมเวิร์กส่วนใหญ่มีค่าเริ่มต้นที่สมเหตุสมผลอยู่แล้ว จุดที่ต้องตัดสินจริง ๆ มีแค่ตอนที่ต้องเลือกว่าจะ retry หรือไม่ตามชนิดของ error การ retry 410 เป็นการสิ้นเปลือง แต่ก็ไม่ใช่ปัญหาเร่งด่วนอะไร มันอาจเพิ่มแรงกดดันด้านหน่วยความจำของ task queue แต่โอกาสที่จะทำให้แอปล่มภายในไม่กี่ชั่วโมงนั้นต่ำ
  • “ดูว่าถูกปฏิเสธไหม แล้วเซ็นใหม่ด้วยวิธีอื่น แล้วจำไว้ว่าต่อเซิร์ฟเวอร์วิธีไหนใช้ได้” นี่มันอ่านอะไรกันอยู่เนี่ย แบบนี้เองหรือที่ทำให้ การพัฒนา Mastodon ช้าขนาดนี้?
    “โพสต์หนึ่งครั้งเกิดการส่ง HTTP หลายพันรายการ” เนี่ยนะ ทั้งที่พูดถึง Ruby ซึ่งขึ้นชื่อว่าเป็นภาษาที่เก่งเรื่องการเขียนโปรแกรมระบบเครือข่ายและคิว
    ฟังดูไม่น่าเชื่อ และแม้จะดีที่เอาเรื่องนี้ไปห่อเป็นไลบรารีแล้ว แต่ก็ยังชวนอึ้งอยู่ดี

  • หลังจากเคย implement ActivityPub ด้วย Java ผมก็ได้ข้อสรุปว่า โปรโตคอลระหว่างเซิร์ฟเวอร์ แบบนี้ควรสร้างบน git ไปเลยดีกว่า
    ความซับซ้อนส่วนใหญ่มีอยู่เพราะกำลังพยายามแก้ปัญหาที่ git แก้ไว้ดีกว่าอยู่แล้ว ถ้า model มันเป็นเอกสาร JSON ภายใน git repository ก็ไม่ต้องมายุ่งกับ pagination โปรโตคอลก็รับประกันได้อยู่แล้วว่าจะส่งเฉพาะข้อมูลที่ยังไม่มี ได้ทั้ง commit signing ได้ทั้งการรับประกันลำดับเหตุการณ์ แก้ปัญหาที่บทความนี้พูดถึงได้ และยังได้ประวัติฟรี ๆ อีกด้วย น่าจะตั้งกฎข้อที่สิบของ Greenspun เวอร์ชันหนึ่งได้ว่าโปรโตคอลแบบนี้มักมี git ครึ่ง ๆ กลาง ๆ ที่ทั้งช้าและมีบั๊กซ่อนอยู่ข้างใน

    • git ไม่ใช่ตัวเลือกที่ยอดเยี่ยม เพราะประวัติของมันขึ้นกับ parent commit อย่างไรก็ตาม Merkle tree gossip protocol ที่ทำงานด้วยกลยุทธ์การเจรจาที่คล้ายกันอาจเหมาะมาก
  • บทความนี้อ่านแล้วเหมือนเป็น บทความคุณภาพต่ำที่ AI สร้างขึ้น
    ถ้าจะพูดให้เฉพาะกว่านั้นคือไม่เข้าใจว่าทำไมถึงเขียนเป็นรูปแบบเล่าเรื่อง ข้อเท็จจริงที่ต้องการสื่อสามารถเขียนได้กระชับกว่าและมีอคติน้อยกว่านี้มาก และตัวเรื่องเล่าก็ไม่น่าโน้มน้าวใจ โดยเฉพาะเพราะมีสำนวนแบบ AI อยู่เยอะ
    อ่านแล้วไม่สนุก ถึงอย่างนั้นก็ยังขอบคุณที่ชี้ปัญหาต่าง ๆ ออกมา และหวังว่าจะได้อ่านและแก้ปัญหาเหล่านั้นในรูปแบบอื่น