<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Jirawatee on Medium]]></title>
        <description><![CDATA[Stories by Jirawatee on Medium]]></description>
        <link>https://medium.com/@jirawatee?source=rss-8376e328ad37------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*9FcousiSNHt21AQ-oOY0tA.png</url>
            <title>Stories by Jirawatee on Medium</title>
            <link>https://medium.com/@jirawatee?source=rss-8376e328ad37------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 23:53:25 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@jirawatee/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Tech-Verse 2026 งาน Tech Conference ที่ใหญ่ที่สุดของ LY Corporation บริษัทแม่ของ LINE Thailand]]></title>
            <link>https://medium.com/linedevth/tech-verse-2026-f99e8daf8597?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/f99e8daf8597</guid>
            <category><![CDATA[tech-verse]]></category>
            <category><![CDATA[ly-corporation]]></category>
            <category><![CDATA[tech-verse-2026]]></category>
            <category><![CDATA[linedevth]]></category>
            <category><![CDATA[yahoo-japan]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Mon, 06 Jul 2026 02:01:44 GMT</pubDate>
            <atom:updated>2026-07-06T02:01:44.135Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OjVAAqA5T7DQf_ULDH14VA.png" /></figure><h3>Tech-Verse 2026 งาน Tech Conf ที่ใหญ่ที่สุดของ LY Corporation บริษัทแม่ของ LINE Thailand</h3><p>งาน Tech-Verse 2026 เป็นงาน Tech Conference ประจำปีของบริษัท LY Corporation (LINE และ Yahoo! JAPAN) โดยหลังจากทั้ง 2 บริษัทควบรวมกัน ก็ได้จัดงานนี้ครั้งแรกในปี 2022 และจัดต่อเนื่องมาเรื่อยๆ โดยวัตถุประสงค์ในการจัดงานก็เพื่อแสดงวิสัยทัศน์ทิศทางเรื่องเทคโนโลยีของบริษัท การแลกเปลี่ยนความรู้ Best practice ต่างๆระหว่างบริษัทในเครือทั้งฝั่ง ญี่ปุ่น เกาหลี ไต้หวัน เวียดนาม และ ไทย</p><p>ส่วนตัวผมเคยมาร่วมงานนี้ในปี 2024 ซึ่งตอนนั้นยังเป็นงานที่จัดภายในออฟฟิศ และให้พนักงานเข้าร่วมเท่านั้น แต่ปีนี้ทาง LY ได้เลือกไปจัดงานที่ Akasaka Intercity Conference ซึ่งเป็นฮอลจัดงานที่ใหญ่และอลังการมาก แถมมีการถ่ายทอดสดให้คนที่สนใจได้รับชมผ่านช่องทางออนไลน์ด้วย โดยในงานวันนั้นมีผู้เข้าร่วมกว่าพันคน ทั้งพนักงานในเครือ และแขกที่ทาง LY เชิญมา</p><p>สำหรับประเทศไทย ปีนี้ผมมีเพื่อนจากทีม Engineer ของ LINE Thailand อีก 13 คน ร่วมทริปมาทำภารกิจด้วยกัน โดยภารกิจหลักของเราคือ มาเรียนรู้สิ่งที่กำลังจะเกิดขึ้นในอนาคต นำความรู้ใหม่ๆกลับไปใช้กับงาน รู้จักกับเพื่อนต่างชาติต่างทีมให้มากที่สุด เนื่องจากทุกคนมีโอกาสที่จะได้ร่วมงานกับพนักงานจากประเทศอื่นๆในเครือ ในการพัฒนาบริการที่เอาไปใช้กับอีกหลายประเทศ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*enr82QcXgqfwvk3FvSKsFg.png" /></figure><p>โดยเมื่อเดินทางมาถึงงาน Check-in แล้วจะได้รับ ป้ายชื่อและถุงยังชีพ ภายในถุงจะมีสติ๊กเกอร์ของงาน, ช็อคโกแลต, สมุดสะสมสติ๊กเกอร์, และสติ๊กเกอร์ที่เอาไว้แลกกับคนอื่น โดยแต่ละคนจะได้สติ๊กเกอร์สำหรับแลกไม่เหมือนกัน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fkZpFehBUkx9bDBBGyg9fA.png" /></figure><p>ผมว่าไอเดียการสะสมสติ๊กเกอร์นี่เจ๋งดีนะ คือเราต้องสะสมสติ๊กเกอร์ที่แตกต่างกันให้ได้ครบ 12 ตัว จึงจะสามารถนำไปแลก Special swag ได้ แล้วคือของที่ให้แลกมันก็พรีเมี่ยมด้วยไง อย่างผมนี่เล็งร่มไว้เลย เพราะเป็นร่มกัน UV แถมเวลาเก็บก็มีปลอกกันน้ำด้วย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rieyfpb_Pke4V8YUB4yC3w.png" /></figure><p>โดยวิธีการได้มาของสติ๊กเกอร์ คือจะต้องไปทำความรู้จัก พูดคุยกับคนอื่น เพื่อให้ได้สติ๊กเกอร์แบบที่ไม่ซ้ำกันมา ซึ่งไม่ได้แปลว่า คุย 12 คน จะได้ 12 แบบนะ เพราะบางคนที่เราไปคุยก็คือมีสติ๊กเกอร์ที่เรามีแล้ว และสติ๊กเกอร์บางตัวต้องไปเยี่ยมชมบูธถึงจะได้มา ดังนั้นกว่าจะสะสมครบนี่คุยไปหลายสิบคนมาก</p><p>บริเวณรอบฮอลจะมีบูธที่แสดงเทคโนโลยีจากหลายๆทีมกว่า 20 บูธ โดยจะมีทั้งเทคโนโลยีที่เปิดให้ใช้งานแล้ว และยังอยู่ในขั้นทดลอง แต่ละบูธจะมี Engineer ที่พัฒนายืนให้ความรู้อยู่ประจำบูธ ใครสนใจเรื่องอะไร เข้าไปคุยได้เลย ทั้งความรู้และ Insight คืออย่างลึก ลึกระดับแกนโลกอะบอกเลย ถถถ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n23N-Zk8W7S3QkLp6RRGxw.jpeg" /></figure><p>มาถึงเนื้อหาของงานบ้าง หัวข้อที่เขาขึ้นมาแชร์ในปีนี้จะถูกแบ่งออกเป็น 2 Tracks คือ AI และ Core Technology โดยในบทความนี้ ผมจะขอหยิบหนึ่ง session ในช่วงเช้า และอีกหนึ่ง session ในช่วงบ่ายที่ผมชอบมาเล่าสู่กันฟัง โดยเริ่มกันที่ Keynote ในช่วงเช้า</p><h3>Keynote</h3><p>Speaker ของ session นี้จะมี 2 คน คือ Park Euivin (CTO) และ Namiki Ryouta (<br>AI CBU Lead) ขึ้นมาพูดถึงวิสัยทัศน์ของ LY Corporation ในการผสานเทคโนโลยี AI เข้ากับชีวิตประจำวันและการทำงานอย่างเต็มรูปแบบ โดยเนื้อหาจะแบ่งเป็น 3 เรื่อง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4Q72_u99FR5r6aIcqkzkaA.png" /></figure><h4>1. Agent I บริการ AI เพื่อผู้ใช้และธุรกิจ</h4><p>LY Corporation ตั้งเป้าที่จะนำจุดเด่นของบริการที่มีผู้ใช้อย่างกว้างขวางอย่าง LINE ที่มีผู้ใช้มากกว่า 100 ล้านคน รวมถึงบริการ Yahoo Search, สื่อ, อีคอมเมิร์ซ และการชำระเงิน มาเปลี่ยนให้เป็นระบบ AI Agent โดยจะแบ่งออกเป็น 2 ส่วน</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v8upWEboyK4SK6v0QyGoHQ.png" /></figure><p><strong>Agent I (สำหรับบุคคลทั่วไป):</strong> จะไม่ได้เป็นเพียงแค่การถามตอบ แต่เป็นการเชื่อมโยง AI Agent หลายตัวเข้าด้วยกันเพื่อเข้าใจความต้องการของผู้ใช้ เช่น หากคุณและเพื่อนเสนอวันที่สะดวกจัดงานปาร์ตี้ในห้องแชท แต่เพื่อนตอบช้า ตัวเอเจนต์จะทำการส่งแจ้งเตือน ปรับตารางเวลา และลงทะเบียนลงในปฏิทินให้โดยอัตโนมัติเมื่อได้ข้อสรุป</p><p><strong>Agent I for Business (สำหรับธุรกิจ):</strong> จะเปิดระบบให้กับ LINE Official Account ซึ่งมีอยู่กว่า 100 ล้านบัญชี โดยเพิ่ม AI Mode ที่ให้ธุรกิจต่างๆ สามารถพัฒนาฟังก์ชัน AI เข้ากับบริการของตัวเองได้ทันที เช่น ระบบ AI สำหรับจองโต๊ะในร้านอาหารผ่าน LINE ที่สามารถเสนอช่วงเวลาอื่นได้อย่างเหมาะสม หากเวลาที่ลูกค้าต้องการเลือกมีคนจองเต็ม</p><h4><strong>2. Platform เบื้องหลัง Agent I</strong></h4><p>เทคโนโลยีเบื้องหลัง <strong>Agent I</strong> ที่พัฒนาขึ้นเพื่อเปลี่ยนผ่าน AI สู่ระดับการใช้งานในองค์กร ว่าเขาโฟกัสเรื่องอะไร ใช้เครื่องมือตัวไหน และเทคนิคอะไรบ้าง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AKjxENs8W0aXxs59zEkbmQ.png" /></figure><p><strong>พัฒนาง่ายและเร็ว (Democratized Development):</strong> มี “Agent Builder” ให้พนักงานทุกคนสามารถสร้างและเปิดใช้งาน Agent ได้ทันที, มี “MCP Hub” ที่เชื่อมโยงระบบความปลอดภัยและการเรียกใช้ข้อมูลภายในบริษัท โดยที่ไม่ต้องเขียนโค้ด แถมมี “Agent Router” ระบบวิเคราะห์ความต้องการของผู้ใช้ เพื่อเลือกส่งต่องานไปยัง Agent ที่เหมาะสมที่สุด</p><p><strong>รู้ใจผู้ใช้งาน (High Precision Personalization): </strong>มี “Long-term Memory” สำหรับจดจำข้อมูลสำคัญของผู้ใช้ โดย AI จะประเมินเองว่าข้อมูลส่วนไหนควรจำ อัปเดต หรือลบทิ้ง และทำงานร่วมกับ “Memory Aggregator” ที่มีข้อมูลโปรไฟล์ของผู้ใช้ เพื่อสร้างเป็น Context ที่ส่งต่อให้โมเดลประมวลผลได้แม่นยำขึ้น</p><p><strong>ควบคุมและความปลอดภัย (Governance &amp; Safety):</strong> มี “Model Routing” ที่เลือกใช้โมเดล AI ให้เหมาะสมกับงาน เช่น ใช้โมเดลขนาดเล็กในเครื่องเพื่อความเป็นส่วนตัว หรือโมเดลขนาดใหญ่สำหรับงานซับซ้อน ทั้งนี้เพื่อช่วยจำกัดต้นทุนและเพิ่มความเร็วในการใช้งาน มี “Agent Catalog” ที่มาคุมการทำงานของ Agent ทุกตัวที่อยู่ในกระบวนการ และมี “5-Layer Guardrails” ในการคัดกรองเนื้อหาที่ไม่เหมาะสม</p><h4>3. AX (AI Transformation)</h4><p>หัวข้อนี้เขาเล่าถึงกลยุทธ์การเปลี่ยนผ่านองค์กรขนาดใหญ่ในยุค AI จากขั้นเตรียมการและเพิ่มทักษะ (Build &amp; Enable), ขั้นตอน Execution, และการวัดผล</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Jn0vTwftUOgeXGA2JrHMUA.png" /></figure><p><strong>การเตรียมระบบและบุคลากร:</strong> มีการปรับปรุงระบบข้อมูลและเชื่อมต่อผ่าน API และ MCP พร้อมทั้งการ Training พนักงานให้เป็น “AI-Native Human Workforce”</p><p><strong>การจัดการ Legacy Services:</strong> มีการปรับปรุงระบบเก่าให้อยู่ในรูปแบบที่ AI เข้าใจได้ง่ายขึ้น ส่งผลให้โค้ดใหม่ที่สร้างโดย AI ในระบบเดิมเพิ่มเป็น 20% แล้ว</p><p><strong>การวัดผล:</strong> ใช้ “Production Lead Time” (เวลาในกระบวนการทำงาน) เป็นตัวชี้วัดหลัก และขยายการใช้ AI ไปยังส่วนงานต่างๆ ซึ่งช่วยลดเวลาทำงานโดยรวมลงไปประมาณ 13% แล้ว</p><p><strong>Cloud และ Security:</strong> มีการเปิดใช้งาน Private Cloud อย่าง “Flava” ภายในองค์กร และเริ่มใช้ AI Agent ในการควบคุมความปลอดภัย</p><p>ตัดภาพมาช่วงพักเที่ยง ทางผู้จัดงานก็มีข้าวกล่องที่มีเมนูให้เลือกทานได้ และหยิบไปทานได้แบบฟรีๆเลย พร้อมน้ำดื่มที่สกรีนชื่องานบนตัวขวดสวยงาม</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xT4X8Di_mrzA9JP6Qh0PpQ.png" /></figure><p>หลังจากนั้น ด้วยความโลภเข้าครอบงำ กลัวจะมีคน Redeem ร่มไปจนหมด ผมเลยรีบพุ่งไปหาคนคุย เพื่อแลกสติ๊กเกอร์ให้ครบในเที่ยงวันให้ได้ ความพยายามอยู่ที่ไหน ความพยายามก็อยู่ที่นั่นแหละ ^^ ในที่สุดผมก็ได้มันมาสมใจปองก่อนไปเข้า session ช่วงบ่าย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aFUH-L1PsDN2sqIENnc_Vw.png" /><figcaption>นั่งพักแพร๊บ วิ่งหาสติ๊กเกอร์อย่างเหนื่อย</figcaption></figure><h3>Good Design Pays Dividends: The Evolution of LINE API Gateway Handling 30B Daily Requests</h3><p>Session นี้อยู่ใน Core Technology Track ที่ผมเล็งไว้ตั้งแต่ก่อนบินมาญี่ปุ่น เพราะเกี่ยวข้องกับ LINE API โดย Speaker ที่ขึ้นพูดมาจากบริษัท LINE Plus (บริษัท LINE ที่เกาหลีใต้) ซึ่งมีคุณ Gihwan Kim (Developer Platform Dev) และคุณ Yejin Shin (Developer Platform Dev)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zeaPABdFuKtEW9Urc2Tv9Q.png" /></figure><p>เนื้อหาจะพูดถึง <strong>LINE API Gateway</strong> ที่ต้องรับ traffic มหาศาลกว่า <strong>30,000 ล้าน requests ต่อวัน</strong>! ทีมที่พัฒนา LINE Channel Platform ได้แชร์เรื่องการปรับจูนระบบจากที่เคยมีปัญหาคอขวดและระบบล่มบ่อยๆ จนกลายมาเป็นระบบที่เสถียรได้ โดยปัญหาหลักๆที่เจอจะมีอยู่ 2 เรื่อง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wZNsZmEId5Id_I6G5fJ2DA.png" /></figure><p><strong>Cascading Failures:</strong> แต่ก่อนระบบเขารันบน Tomcat และ Spring Web ซึ่งเป็นแบบ synchronous (1 request ต่อ 1 thread) เวลาบริการปลายทาง (upstream) ทำงานช้า ตัว thread ก็จะค้างจนเต็ม ส่งผลให้เซิร์ฟเวอร์ไม่สามารถรับ request ใหม่ได้และล่มในท้ายที่สุด</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sjmNZncF3gJ8ycDRfdgf7w.png" /></figure><p><strong>The Bottleneck:</strong> เวลาทีมอื่นต้องการเพิ่ม API แม้จะเป็นเพียงโค้ดง่ายๆ ทีม Gateway ก็ต้องมาคอยเขียนโค้ด Java เอง ต้องนัดประชุมหลายรอบเพื่อเข้าใจรายละเอียดงาน ทำการรีวิว PR และต้องมาเขียน Wiki เองด้วยมือ ซึ่งใช้เวลาทำงานเป็นสัปดาห์ แถมบางครั้งโค้ดที่อัปเดตไปแล้วก็ไม่ตรงกับ Wiki</p><p>เพื่อแก้ปัญหาทั้งหมด ทีมเขาจึงตัดสินใจสร้าง <strong>Moon</strong> ซึ่งเป็น API Gateway Framework ของตัวเองขึ้นมา โดยตัวมันจะมีความสามารถหลัก 4 เรื่อง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kjNLXIzXyaJJefcQwG43cA.png" /></figure><ul><li><strong>Declarative:</strong> ใช้ YAML กำหนด API Gateway แทนการเขียนโค้ด Java</li><li><strong>Dynamic Reconfiguration:</strong> สามารถปรับเปลี่ยนโครงสร้างเส้นทางหรือตั้งค่าระบบได้ทันทีในแบบ real-time โดยไม่ต้อง restart server</li><li><strong>Extensible:</strong> ออกแบบให้สามารถเพิ่มฟังก์ชันหรือฟีเจอร์ใหม่ๆ ได้ง่าย</li><li><strong>High Performance:</strong> รันบน <strong>Armeria</strong> (asynchronous framework ที่ LINE พัฒนาขึ้นเอง) ช่วยตัดปัญหาเรื่อง thread ค้าง และป้องกันการล่มแบบโดมิโนได้</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*epxBw_WVjhEpPk6_Ci5low.png" /></figure><p>ช่วงท้ายเขาได้โชว์ผลลัพธ์จากการใช้ Moon Framework ที่พัฒนาขึ้นมา ว่ารองรับ traffic ได้มากกว่าเดิมถึง 3 เท่า และหาก upstream ทำงานช้าลง ระบบก็จะไม่ล่ม แถมข้อมูลของ API ทั้งหมดยังถูกดึงจาก YAML ไปอัปเดท API Docs ให้อัตโนมัติ และเนื่องจากข้อมูลทุกอย่างอยู่ในรูปแบบ YAML ที่มีโครงสร้างชัดเจน ทีมเขาจึงสามารถแปลงข้อมูลนี้ไปเป็น Markdown เพื่อป้อนให้ <strong>Gemini</strong> ช่วยตอบคำถามของนักพัฒนาเกี่ยวกับการใช้ API ได้อย่างรวดเร็ว นอกจากนี้ยังสร้าง <strong>MCP server</strong> เพื่อรองรับการเขียนโค้ดร่วมกับ AI อย่าง <strong>Claude Code</strong> ให้อ่านข้อมูล API specs และเขียนโค้ดได้อย่างแม่นยำอีกด้วย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ysrYrdTPgxhDrNw3m-d2zg.png" /></figure><p>สรุปได้ว่า การออกแบบระบบที่ดีด้วยการจัดการข้อมูลให้มีโครงสร้างที่ชัดเจน (Structured Specification) ไม่เพียงแต่ช่วยลดขั้นตอนการทำงานและเพิ่มความเสถียรให้กับระบบระดับ 3 หมื่นล้าน requests/day เท่านั้น แต่ยังช่วยสร้างโอกาสใหม่ ๆ ในการนำ AI เข้ามาเพิ่มประสิทธิภาพการทำงานได้อย่างมหาศาลครับ</p><p>ช่วงท้ายมีพิธีมอบรางวัล Best Speaker / Best Author ให้กับ Speaker ที่ได้รับคะแนนโหวตมากที่สุด ซึ่งพี่บอล CTO จากไลน์ประเทศไทยก็เป็นหนึ่งในผู้ที่ได้รับเชิญขึ้นไปมอบรางวัลนี้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nlkyZpCm4jY5o5JM5sTXLw.png" /></figure><p>ตามด้วย After Party ที่เสิร์ฟอาหารแบบจัดเต็ม โดย Gimmick ปีนี้คือมีเบียร์จากทุกประเทศในกลุ่ม LY ให้เลือกดื่ม ซึ่งก็มีเบียร์สิงห์จากไทยด้วย งานนี้กินกันแบบอิ่มจังตังค์อยู่ครบ(เสียงในหัว)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DAJo3Xci6cmqzkz8eE2kCQ.png" /></figure><p>ความประทับใจปิดท้ายของการร่วมงาน Tech-Verse ในปีนี้ คือการได้เจอกลุ่ม Engineer ชาวไทยที่ทำงานที่ LY ทั้ง ติ๊ที่ทำงานที่ Yahoo! JAPAN มากว่า 6 ปี และโมกกับแคนที่เป็น New grad เพิ่งเข้ามาจอย และระหว่างที่พวกเรากำลังบันเทิงชนแก้วกับเพื่อนต่างชาติทั้ง คัมปาย, คอนแบ, คัมเปย, อู่กานลา, และ โมไหบ๋า โย่! กันอยู่นั้น…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TbGXqV95EsjAO1e3pb9Imw.jpeg" /></figure><p>Park Euivin (CTO) ที่ขึ้นพูด Keynote ก็เดินเข้ามาฉลองกับพวกเราด้วย แกเป็นกันเองมาก เข้ามาถามว่างานเป็นไงบ้าง สนุกกันไหม ที่สำคัญแกเรียกผมว่า “ที่” ซึ่งผมก็โมเมไปก่อนละว่าแกจำผมได้ ถถถ โดยหลังจากแกเดินจากไป ผมก็ถาม Engineer ชาวไทยที่ทำงานที่นี่ว่า ปกติทุกคนใกล้ชิดกับแกได้แบบนี้ไหม ทั้ง 3 คนก็มองกันไปมาแล้วยิ้ม 555+</p><p>การมาร่วมงาน Tech-Verse 2026 ในปีนี้มันตอบโจทย์ตัวผมมาก ทั้งการได้มาเรียนรู้สิ่งใหม่ๆ ได้มาดูการจัดงานระดับ Global ได้มาเจอเพื่อนๆ DevRel จากแต่ละประเทศ ได้เจอทั้งเพื่อนเก่าเพื่อนใหม่ โดยเฉพาะได้เจอคนไทยที่ทำงานที่นี่ ได้ Swag เกร๋ๆกลับบ้าน และที่สำคัญได้กระทบไหล่กับ CTO ด้วย(จากภาพน่าจะเรียกแบบนั้นได้ ^^) ทั้งหมดเป็นอีกหนึ่งประสบการณ์ที่ผมจะจดจำไว้ และแน่นอนว่าผมจะรอโอกาสที่จะได้มางานนี้อีกในอนาคตครับ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f99e8daf8597" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/tech-verse-2026-f99e8daf8597">Tech-Verse 2026 งาน Tech Conference ที่ใหญ่ที่สุดของ LY Corporation บริษัทแม่ของ LINE Thailand</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ดึงข้อมูลโปรไฟล์ผู้ใช้งาน LINE จาก User ID ใน Google Sheets เรื่องง่ายๆที่คุณไม่รู้]]></title>
            <link>https://medium.com/linedevth/line-user-profile-on-google-sheets-bf99ce65984e?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/bf99ce65984e</guid>
            <category><![CDATA[line-bot]]></category>
            <category><![CDATA[linemessagingapi]]></category>
            <category><![CDATA[google-sheets]]></category>
            <category><![CDATA[line-chatbot]]></category>
            <category><![CDATA[line-oa]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Mon, 25 May 2026 03:59:15 GMT</pubDate>
            <atom:updated>2026-05-25T10:15:47.730Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0itgtVe4ixemhJq_nmt0rw.png" /></figure><p>เชื่อว่านักพัฒนาส่วนใหญ่ที่พัฒนาบริการร่วมกับ Messaging API, LINE MINI App, LIFF app, หรือ LINE Login จะมีการเก็บข้อมูลโปรไฟล์ผู้ใช้งาน LINE เพื่อนำไปอ้างอิง และ Google Sheets ก็เป็นเครื่องมือยอดนิยม ที่หลายคนเลือกใช้ในการเก็บข้อมูล</p><p>กรณีของ LINE OA หากที่ผู้ใช้งานได้มีการเพิ่มเพื่อนแล้ว ระบบจะสามารถดึงข้อมูลโปรไฟล์ของผู้ใช้งานจาก userId ผ่าน API ออกมาได้ ซึ่งจะมีข้อมูลตามนี้</p><pre>{<br>  &quot;displayName&quot;: &quot;LINE taro&quot;,<br>  &quot;userId&quot;: &quot;U4af4980629...&quot;,<br>  &quot;language&quot;: &quot;en&quot;,<br>  &quot;pictureUrl&quot;: &quot;https://profile.line-scdn.net/ch/v2/p/uf9da5ee2b...&quot;,<br>  &quot;statusMessage&quot;: &quot;Hello, LINE!&quot;<br>}</pre><p>ตามรายละเอียดในข้อ 2.1 ของบทความด้านล่างนี้</p><p><a href="https://medium.com/linedevth/%E0%B8%A3%E0%B8%B9%E0%B9%89%E0%B8%84%E0%B8%A3%E0%B8%9A%E0%B8%88%E0%B8%9A%E0%B9%83%E0%B8%99%E0%B8%97%E0%B8%B5%E0%B9%88%E0%B9%80%E0%B8%94%E0%B8%B5%E0%B8%A2%E0%B8%A7%E0%B8%81%E0%B8%B1%E0%B8%9A%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%94%E0%B8%B6%E0%B8%87-user-profile-%E0%B8%9C%E0%B9%88%E0%B8%B2%E0%B8%99-api-%E0%B8%95%E0%B9%88%E0%B8%B2%E0%B8%87%E0%B9%86%E0%B9%83%E0%B8%99-line-dafb17e5864a">รู้ครบจบในที่เดียวกับการดึง User Profile ผ่าน API ต่างๆใน LINE</a></p><p>โดยนักพัฒนาหลายคนเลือกที่จะเก็บข้อมูลข้างต้นลงใน Google Sheets ทั้งหมด โดยที่ไม่รู้ว่าข้อมูลทั้งหมดที่ไม่ใช่ userId จะสามารถเปลี่ยนแปลงได้ตลอดเวลา จึงทำให้การแสดงผลข้อมูลใน Google Sheets นั้นไม่อัปเดท</p><p>จริงอยู่ว่า userId สามารถนำไปใช้ดึงข้อมูลโปรไฟล์ของผู้ใช้ LINE ที่เป็นปัจจุบันออกมาได้ แต่ใน Google Sheets ก็ไม่มีสูตร(formula) ที่จะเอา userId ไปเรียก API ได้โดยตรง และนั่นก็คือที่มาของบทความนี้ ที่ผมจะพาทุกคนไปรู้จักเคล็ดลับในการสร้าง formula ของตัวเอง แบบที่ง่ายมากๆ และไม่มีค่าใช้จ่ายใดๆ…ถ้าพร้อมแล้ว เราไปเริ่มกันเลย!</p><h3>1. สร้าง Spreadsheet ใน Google Sheets</h3><p>เริ่มต้นให้ไปสร้าง spreadsheet ใหม่ขึ้นมา โดยสามารถพิมพ์หรือคลิก <a href="https://sheets.new/">sheets.new</a> ที่เว็บเบราเซอร์ของคุณ จากนั้นกำหนด column เพื่อแสดงผลข้อมูลโปรไฟล์ต่างๆของผู้ใช้งาน LINE โดยที่ column แรกให้ระบุค่า userId ของผู้ติดตาม LINE OA ของคุณลงไป</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lRRDzQ8Br696sNwPY5le8g.png" /></figure><h3>2. เพิ่มโค้ด Apps Script</h3><p>ในหน้าของ Google Sheets ให้ไปเมนูด้านบน จากนั้นคลิก <strong>Extensions</strong> &gt; <strong>Apps Script</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7yO2rI_VoIxD84TkL14ekA.png" /></figure><p>เมื่อเข้าสู่หน้า Apps Script ให้ลบโค้ดทุกอย่างที่อยู่ใน Editor ออก แล้วให้คัดลอกโค้ดของฟังก์ชัน <strong>LINEPROFILE</strong> ด้านล่างนี้ไปใส่แทนที่</p><p><strong>ห้ามลืม</strong>: คุณจะต้องเอาค่า <strong>Channel Access Token</strong> จาก Messaging API Channel ของ LINE OA ของคุณไปแทนที่ <strong>YOUR-ใน CHANNEL-ACCESS-TOKEN</strong> ในโค้ด</p><pre>function LINEPROFILE(userId, path) {<br>  try {<br>    // 1. กำหนด headers สำหรับการใช้งาน Messaging API<br>    var options = {<br>      &quot;method&quot; : &quot;get&quot;,<br>      &quot;headers&quot; : {<br>        &quot;Authorization&quot;: &quot;Bearer YOUR-CHANNEL-ACCESS-TOKEN&quot;,<br>        &quot;Accept&quot;: &quot;application/json&quot;<br>      },<br>      &quot;muteHttpExceptions&quot;: true<br>    };<br><br>    // 2. ดึงข้อมูลโปรไฟล์จาก userId ผ่าน API<br>    var response = UrlFetchApp.fetch(`https://api.line.me/v2/bot/profile/${userId}`, options);<br><br>    if (response.getResponseCode() !== 200) {<br>      return &quot;Error: HTTP &quot; + response.getResponseCode() + &quot; - &quot; + response.getContentText();<br>    }<br><br>    var json = response.getContentText();<br>    var data = JSON.parse(json);<br>    <br>    // 2. ดึงค่าของ JSON object ตาม path ที่ต้องการ<br>    var pathArray = path.split(&#39;.&#39;);<br>    for (var i = 0; i &lt; pathArray.length; i++) {<br>      data = data[pathArray[i]];<br>    }<br>    <br>    return data;<br>  } catch (error) {<br>    return &quot;Error: &quot; + error.toString();<br>  }<br>}</pre><p>เสร็จแล้วให้กด Save ผ่านไอคอน floppy disk แล้วปิดแท็บ Apps Script ไปได้เลย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L66W4_twmtb4oW7RRNNSBA.jpeg" /><figcaption>Save ด้วยการกด Ctrl + S บน Windows หรือ Cmd + S บน Mac ก็ได้ด้วยนะ</figcaption></figure><h3>3. เรียกใช้งานฟังก์ชัน LINEPROFILE</h3><p>กลับมาที่หน้า Google Sheets เราจะมาเรียกใช้ฟังก์ชัน <strong>LINEPROFILE </strong>ที่เราสร้างขึ้นมา โดยอ้างอิงโค้ดในขั้นตอนที่ 2 ฟังก์ชันนี้จะรับตัวแปร 2 ตัว</p><ul><li><strong>userId</strong> ข้อมูลที่เตรียมไว้ใน column A ของ spreadsheet แล้ว</li><li><strong>path</strong> ชื่อ path ของ response ที่ได้มาจาก API ในขั้นตอนที่ 2</li></ul><pre>// ตัวอย่าง formula สำหรับดึงค่า displayName<br>=LINEPROFILE(A2, &quot;displayName&quot;)</pre><p><strong>โปรดทราบ</strong>: เมื่อมีการเรียก formula ที่ต้องเชื่อมต่อกับ API ภายนอก Google Sheets จะแจ้งเตือนขอสิทธิ์จากเราก่อน ตรงนี้ก็ให้เรากด <strong>Allow access</strong> ไปครับ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OlKXAcQKG7oKfxFYiH8dRQ.png" /></figure><p>เมื่อนำ formula นี้มาใช้กับทั้ง 4 path ตาม column ข้อมูลก็จะโผล่มาแล้วจ้า</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gCz9GjkqIGXtgDNOufYwsQ.png" /></figure><h3>Bonus: เทคนิคการแสดงรูป และ ลิงก์ไปเปิดภาพใหญ่</h3><p>ใครที่ทำตามมาถึงขั้นตอนที่ 3 แล้ว ก็คงคิดว่าถ้า <strong>pictureUrl</strong> สามารถแสดงเป็นรูปออกมาได้เลยก็น่าจะดีใช่ไหมครับ เพราะบางครั้งเห็นรูปปุ๊บก็รู้ได้เลยว่าเป็นใคร หรือตัวตนเขาเป็นอย่างไร ดังนั้น ขั้นตอนนี้ผมจะมาบอกเทคนิคพิเศษที่ทำให้มันเป็นจริงได้</p><h4>การแสดงรูปจาก URL</h4><p>จากขั้นตอนที่ 3 เราจะเพิ่มฟังก์ชัน <strong>IMAGE</strong> โดยมันจะรับ URL ของรูป โดยเราจะเอาไปครอบฟังก์ชัน LINEPROFILE เดิมแบบนี้</p><pre>=IMAGE(LINEPROFILE(A2, &quot;pictureUrl&quot;))</pre><p>เพียงเท่านี้ รูปก็จะปรากฏขึ้นมา บอกเลยว่า easy มั๊กมาก</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PIKCTUjITc1W6oe2kDI7Gw.png" /></figure><h4>โหลดรูปไวกว่าด้วย Thumbnail</h4><p>รูปที่เราดึงมาจาก pictureUrl นั้นจะเป็นรูปต้นฉบับที่ผู้ใช้อัปโหลดเข้ามา ซึ่งบางรูปคือใหญ่มาก ดังนั้นลองคิดดูว่าถ้า spreadsheet นี้มีข้อมูลผู้ใช้สัก 1,000 แถว มันก็น่าจะใช้เวลาแสดงรูปนานเลยใช่ไหมครับ แต่โชคเข้าข้างเรา เพราะ LINE เขาเตรียมรูปที่เป็น thumbnail มาไว้ให้เราเรียกใช้แล้ว</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FnbWaKqhpINw%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DnbWaKqhpINw&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FnbWaKqhpINw%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/f7726f9005fceea822afbad8dcef639a/href">https://medium.com/media/f7726f9005fceea822afbad8dcef639a/href</a></iframe><p>ดังนั้นเพื่อให้ตัว Google Sheets โหลดรูปได้เร็วขึ้นเราจะมาปรับ formula เป็นตามนี้</p><pre>=IMAGE(LINEPROFILE(A2, &quot;pictureUrl&quot;) &amp; &quot;/large&quot;)</pre><p>โดยหน้าตาผลลัพธ์ที่ได้จะเหมือนเดิม แต่เวลาโหลดรูปจะเร็วกว่าเดิม โดยเฉพาะกรณีที่มีจำนวนแถวเยอะๆนี่เห็นชัดเลยว่าโหลดรูปเร็วขึ้นมากๆครับ</p><h4>สร้างลิงก์ไปเปิดภาพใหญ่</h4><p>รูปเล็ก โหลดเร็ว ก็จริง แต่บางรูปก็อยากดูชัดๆ ใหญ่ๆ จะได้ไหม? ได้สิครับ เพราะเทคนิคนี้เราจะใช้ฟังก์ชัน <strong>HYPERLINK</strong> สร้างลิงก์ให้กดไปเปิดรูปต้นฉบับได้ โดยตัวมันจะรับตัวแปร 2 ตัวคือ URL และ Object ตามตัวอย่างด้านล่างนี้</p><pre>=HYPERLINK(LINEPROFILE(A2, &quot;pictureUrl&quot;), IMAGE(LINEPROFILE(A2, &quot;pictureUrl&quot;) &amp; &quot;/large&quot;))</pre><p>คราวนี้หากเราไปคลิกที่รูปก็จะมี URL ต้นฉบับแสดงออกมาพร้อมให้เราคลิกได้แล้ว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Mckm5hJQCM9g-sT_rFCRdg.png" /></figure><h4>เพอร์ฟอร์แมนซ์</h4><p>ยั๊ง ยังไม่หมดอีกเหรอ 555 เทคนิคสุดท้ายละ สำหรับผู้อ่านที่เป็นนักพัฒนาทุกท่าน จะสังเกตว่า การทำลิงก์ก่อนหน้านี้ มันมีอะไรขัดใจอยู่ใช่ไหม…ใช่ครับ มันเรียก LINEPROFILE เพื่อไปเอา pictureUrl เดิม ออกมาถึง 2 ครั้ง ในตัวอย่างมี 4 คน ก็โหลด 8 ครั้ง แล้วถ้ามีแถวเป็นหลักพันหลักหมื่น นี่โหลดกระจาย กลายเป็นช้าไปอีก</p><p>ดังนั้นเราจะใช้ฟังก์ชัน <strong>LET</strong> ที่ใช้ประกาศตัวแปร และสามารถเรียกใช้ตัวแปรภายหลังได้ (โปรแกรมมิ่งใน formula มีอยู่จริง) โดยตัวมันจะรับ name, value, และ formula expression ตามนี้</p><pre>=LET(pictureUrl, LINEPROFILE(A3, &quot;pictureUrl&quot;), HYPERLINK(pictureUrl, IMAGE(pictureUrl &amp; &quot;/large&quot;)))</pre><p>เท่านี้ เราก็จะเรียก LINEPROFILE เพื่อดึงเอา pictureUrl เพียงครั้งแล้ว</p><h3>Last but not least</h3><p>ถึงแม้ Apps Script จะให้ใช้งานฟรี(ไม่มีเติมเงิน) แต่มันก็มีข้อจำกัดด้านจำนวนการ fetch URL อยู่ ซึ่งโควต้าการ fetch จะขึ้นอยู่กับประเภทของบัญชีผู้ใช้ที่เป็นเจ้าของ Apps Script นั้น โดยหากเป็น</p><ul><li>บัญชี <strong>Gmail</strong> จะ fetch ได้ <strong>20,000 ครั้ง/วัน</strong></li><li>บัญชี <strong>Google Workspace</strong> จะ fetch ได้ <strong>100,000 ครั้ง/วัน</strong></li></ul><p>ดังนั้นใครที่คิดจะนำไปใช้กับ spreadsheet ที่มีจำนวนแถวเยอะๆ ลองประเมินจำนวนครั้งที่ต้อง fetch ก่อนนะครับ รายละเอียดเกี่ยวกับโควต้าดูเพิ่มเติมได้ที่ลิงก์ด้านล่าง</p><p><a href="https://developers.google.com/apps-script/guides/services/quotas">Quotas for Google Services | Apps Script | Google for Developers</a></p><h3>ปิดท้าย</h3><p>จากการได้ศึกษาและเขียนบทความนี้ ผมพบว่า Google Sheets มันทรงพลังมาก ทำอะไรได้มากกว่าที่ผมคิด และเชื่อเหลือเกินว่าบทความนี้จะตอบโจทย์นักพัฒนา LINE ในบ้านเรา เพราะดูจากโพสใน <a href="https://www.facebook.com/groups/LINEDEVTH">Community</a> จะเห็นได้เลยว่ามีคนใช้ Google Sheets ในการเก็บข้อมูลเยอะ หวังว่าเทคนิคที่ผมมาแบ่งปันในบทความนี้ จะเป็นประโยชน์กับทุกท่าน แล้วพบกันใหม่บทความหน้าครับ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bf99ce65984e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/line-user-profile-on-google-sheets-bf99ce65984e">ดึงข้อมูลโปรไฟล์ผู้ใช้งาน LINE จาก User ID ใน Google Sheets เรื่องง่ายๆที่คุณไม่รู้</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ปลดล็อค LINE MINI App ให้ใช้งานผ่าน “เว็บเบราว์เซอร์” บน “คอมพิวเตอร์”]]></title>
            <link>https://medium.com/linedevth/open-line-mini-app-on-computer-fe395bdd5875?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/fe395bdd5875</guid>
            <category><![CDATA[line-api]]></category>
            <category><![CDATA[miniapp]]></category>
            <category><![CDATA[liff-app]]></category>
            <category><![CDATA[line-mini-app]]></category>
            <category><![CDATA[linedevth]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Wed, 29 Oct 2025 03:12:48 GMT</pubDate>
            <atom:updated>2025-10-29T03:49:31.157Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HI09o4_4d0_K_g44wIpg8g.png" /></figure><p>หนึ่งในปัจจัยสำคัญที่ทำให้นักพัฒนา LIFF app หลายคนยังไม่อัปเกรดมาใช้ LINE MINI App ที่มีฟีเจอร์มากกว่า ก็คือ ความต้องการที่จะรองรับลูกค้าหรือผู้ใช้งานบนคอมพิวเตอร์ เพราะปัจจุบันหากเรานำ URL ของ LINE MINI App ที่หน้าตาแบบนี้</p><pre>https://miniapp.line.me/1655694964-pkPXD1Ng</pre><p>ไปเปิดผ่านเว็บเบราว์เซอร์บนคอมพิวเตอร์ ก็จะต้องพบกับหน้าจอที่แจ้งว่าให้เปิดผ่านสมาร์ทโฟนเท่านั้น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z_nyIJj3HS3fiu6c8Ky-XQ.png" /></figure><p>สำหรับนักพัฒนาบางท่านที่ตาดีไปเห็นข้อความด้านล่างนี้ใน Channel ก็คงคิดว่า LIFF URL จะเป็นแสงสว่างที่ปลายอุโมงค์ให้เราสามารถเปิด LINE MINI App กับเว็บเบราว์เซอร์บนคอมพิวเตอร์ได้แน่ๆ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LwDgKC7dDBVQGMMycUb0Fw.jpeg" /></figure><p>แต่…เสียใจด้วย มันยังไม่ใช่ทางออกของเรื่องนี้ เพราะผลลัพธ์จากการเอา ID ของ LINE MINI App ไปเปิดด้วย LIFF URL แบบด้านล่างนี้ กับเบราว์เซอร์บนคอมพิวเตอร์นั้น ก็จะพบกับข้อความที่แจ้งว่าให้เปิดผ่านสมาร์ทโฟนเช่นกัน</p><pre>https://liff.line.me/1655694964-pkPXD1Ng</pre><p>เจอแบบนี้เข้าไป คิดหนักเลย นี่เราต้องทำเว็บแอปขึ้นมา 2 ตัว ตัวหนึ่งใช้กับ LINE MINI App อีกตัวหนึ่งใช้กับเว็บเบราว์เซอร์บนคอมพิวเตอร์เหรอเนี่ย ? ตอนนั้นก็มืดแปดด้านเลย เอาไงดี…จู่ๆเสียงในหัวก็มีเพลงพี่ตูนดังขึ้นมา</p><blockquote>“ในค่ำคืนที่ฟ้านั้นไม่มีดาวอยู่ตรงนี้ ฉันยังคงก้าวไป ยังคงมีรักแท้เป็นแสงนำไปในคืนที่หลงทาง”</blockquote><p>เท่านั้นหละ ไอเดียมา! โดยบทความนี้ผมจะมาเฉลยวิธีการสร้าง Universal URL สำหรับ LINE MINI App ใน 5 ขั้นตอน ซึ่งผลลัพธ์เราจะได้<strong> URL เดียว ที่เปิดได้ทั้งสมาร์ทโฟน และ คอมพิวเตอร์</strong> ถ้าพร้อมแล้ว ไปลุยกันเลย 🚀</p><h3>1. เตรียม HTML</h3><p>นอกจากการที่เราจะทำให้ LINE MINI App เปิดใช้งานได้กับเบราว์เซอร์บนคอมพิวเตอร์แล้ว สิ่งที่หนึ่งที่ช่วยยืนยันว่าเราได้เข้าถึงฟังก์ชันของ LINE MINI App ได้จริง ก็คือการเข้าถึงโปรไฟล์ของลูกค้าหรือผู้ใช้งาน ดังนั้นผมจะเตรียม HTML ไว้เพื่อแสดงผลข้อมูลผู้ใช้งาน</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html&gt;<br>&lt;head&gt;<br>  &lt;meta charset=&quot;UTF-8&quot;&gt;<br>  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,initial-scale=1,viewport-fit=cover&quot;&gt;<br>  &lt;title&gt;LINE MINI App&lt;/title&gt;<br>  &lt;meta name=&quot;description&quot; content=&quot;A web application which you can deliver services on LINE&quot;&gt;<br>  &lt;link rel=&quot;stylesheet&quot; href=&quot;https://mokmoon.com/mini/css/mini.css&quot;&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>  &lt;img id=&quot;pictureUrl&quot; src=&quot;https://vos.line-scdn.net/imgs/apis/ic_mini.png&quot;&gt;<br>  &lt;p id=&quot;userId&quot;&gt;&lt;b&gt;userId:&lt;/b&gt; &lt;/p&gt;<br>  &lt;p id=&quot;displayName&quot;&gt;&lt;b&gt;displayName:&lt;/b&gt; &lt;/p&gt;<br>  &lt;p id=&quot;statusMessage&quot;&gt;&lt;b&gt;statusMessage:&lt;/b&gt; &lt;/p&gt;<br>  &lt;p id=&quot;decodedIDToken&quot;&gt;&lt;b&gt;email:&lt;/b&gt; &lt;/p&gt;<br>  <br>  &lt;!-- button จะถูกซ่อนไว้ด้วย CSS เป็น default --&gt;<br>  &lt;button id=&quot;btnLogIn&quot; onclick=&quot;liff.login();&quot;&gt;Log In&lt;/button&gt;<br>  &lt;button id=&quot;btnLogOut&quot; onclick=&quot;liff.logout(); window.location.reload();&quot;&gt;Log Out&lt;/button&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;</pre><h3>2. Bind ตัว HTML Element</h3><p>ขั้นตอนนี้เราจะทำการ bind ตัว element ต่างๆจาก HTML ในข้อ 1 เพื่อเตรียมใช้งานใน JavaScript</p><pre>&lt;script&gt;<br>  const pictureUrl = document.querySelector(&quot;#pictureUrl&quot;);<br>  const userId = document.querySelector(&quot;#userId&quot;);<br>  const displayName = document.querySelector(&quot;#displayName&quot;);<br>  const statusMessage = document.querySelector(&quot;#statusMessage&quot;);<br>  const email = document.querySelector(&quot;#decodedIDToken&quot;);<br>  const btnLogIn = document.querySelector(&quot;#btnLogIn&quot;);<br>  const btnLogOut = document.querySelector(&quot;#btnLogOut&quot;);<br>&lt;/script&gt;</pre><h3>3. Import ตัว LIFF SDK เข้ามา</h3><p>ด้วยความสามารถต่างๆบน LINE MINI App ในฝั่งโปรแกรมมิ่งนั้นมี LIFF SDK เป็นผู้อยู่เบื้องหลังทั้งหมด และ workaround ที่เราจะทำในวันนี้ ก็จะใช้ความสามารถของฟังก์ชันจาก LIFF SDK ด้วย ดังนั้นให้เราทำการ import ตัว SDK เข้ามา</p><pre>&lt;script src=&quot;https://static.line-scdn.net/liff/edge/2/sdk.js&quot;&gt;&lt;/script&gt;</pre><h3>4. เตรียมฟังก์ชัน mobileCheck() และ getUserProfile()</h3><p>ขั้นตอนนี้เราจะเตรียมฟังก์ของ JavaScript 2 ฟังก์ชันด้วยกัน โดย</p><ul><li><strong><em>mobileCheck()</em></strong> ใช้สำหรับตรวจสอบว่าผู้ใช้งานเปิดเว็บจากสมาร์ทโฟนหรือไม่ ?</li><li><strong><em>getUserProfile()</em></strong> ใช้สำหรับแสดงผลข้อมูลผู้ใช้งาน</li></ul><pre>&lt;script&gt;<br>  function mobileCheck() {<br>    let check = false;<br>    (function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor || window.opera);<br>    return check;<br>  }<br>  <br>  async function getUserProfile() {<br>    const profile = await liff.getProfile();<br>    pictureUrl.src = profile.pictureUrl;<br>    userId.append(profile.userId);<br>    statusMessage.append(profile.statusMessage);<br>    displayName.append(profile.displayName);<br>    email.append(liff.getDecodedIDToken().email);<br>  }<br>&lt;/script&gt;</pre><h3>5. สร้างฟังก์ชัน main() เพื่อกำหนดทิศทางลม</h3><p>ฟังก์ชัน <strong><em>main()</em></strong> ถือเป็นหัวใจของ workaround นี้เลย โดยการทำงานจะแบ่งออกมาเป็น 3 ขั้นตอนย่อย</p><pre>&lt;script&gt;<br>  async function main() {<br>    const liffId = &quot;YOUR-LIFF-ID&quot;;<br><br>    // Step 5.1<br>    if (liff.isInClient()) {<br>      await liff.init({ liffId });<br>      getUserProfile();<br>      return;<br>    }<br><br>    // Step 5.2<br>    if (mobileCheck()) {<br>      window.location.replace(`line://app/${liffId}`);<br>      setTimeout(() =&gt; { window.close(); }, &quot;5000&quot;);<br>      return;<br>    }<br><br>    // Step 5.3<br>    await liff.init({ liffId, withLoginOnExternalBrowser: true });<br>    if (liff.isLoggedIn()) {<br>      getUserProfile();<br>      btnLogOut.style.display = &quot;block&quot;;<br>      return;<br>    }<br>    btnLogIn.style.display = &quot;block&quot;;<br>  }<br><br>  main();<br>&lt;/script&gt;</pre><h4>ขั้นตอนที่ 5.1</h4><p>ขั้นตอนนี้ จะตรวจสอบว่าผู้ใช้งาน<strong>เปิด Universal URL ในแอป LINE หรือไม่ ?</strong> หาก “ใช่” ตัว LINE MINI App จะทำการ Log in ให้อัตโนมัติ และเราจะดึงข้อมูลโปรไฟล์ผู้ใช้ออกมาแสดง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hYtJnSlesyxTy0YrwTRmgA.png" /></figure><h4>ขั้นตอนที่ 5.2</h4><p>หากไม่เข้าเงื่อนไขที่ 5.1 จะมาเจอขั้นตอนนี้ โดยขั้นตอนนี้จะตรวจสอบว่าผู้ใช้งานได้<strong>เปิด Universal URL บนสมาร์ทโฟนหรือไม่ ?</strong> หาก “ใช่” เราจะเปิดตัว LINE MINI App ด้วย <a href="https://developers.line.biz/en/docs/line-login/using-line-url-scheme/#opening-a-liff-app">LINE URL scheme</a> และสั่งปิดเว็บที่เปิดจาก Universal URL ใน 5 วินาที</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2ZJ4unJ_unPKa8kl5mVMzA.gif" /></figure><h4>ขั้นตอนที่ 5.3</h4><p>หากไม่เข้าเงื่อนไขทั้ง 5.1 และ 5.2 จะเจอมาขั้นตอนนี้ (ขั้นตอนนี้บ่งบอกว่าผู้ใช้เปิด Universal URL กับเบราว์เซอร์บนคอมพิวเตอร์) โดยขั้นตอนนี้จะตรวจสอบว่าผู้ใช้งาน <strong>Log in มาแล้วหรือไม่ ?</strong> หาก</p><ul><li>“ใช่” เราจะดึงข้อมูลโปรไฟล์ผู้ใช้ออกมาแสดง พร้อมแสดงปุ่ม Log out</li><li>“ไม่ใช่” เราจะแสดงปุ่ม Log in</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3n0fsJuzWd2vD1gEiPGtLA.png" /></figure><h4>หมายเหตุ</h4><ul><li>LINE URL scheme ใน<strong>ขั้นตอนที่ 5.2</strong> จะช่วยพาผู้ใช้งานกระโดดออกจาก In-App browser ของ LINE, Facebook, หรือแอปอื่นๆ แล้วพาไปเปิด LINE MINI App โดยตรง</li><li>การระบุ <em>withLoginOnExternalBrowser: true</em> ใน liff.init() <strong>ขั้นตอนที่ 5.3</strong> จะช่วยให้ตัวเว็บเรียกคำสั่ง Log in ให้อัตโนมัติ โดยหากเป็น External browser บนสมาร์ทโฟนจะทำการ Log in กับแอป LINE ให้อัตโนมัติด้วยนาจา</li></ul><h3>สรุป</h3><p>มาถึงตรงนี้หากใครต้องการทดสอบ workaround ที่ผมได้แนะนำไป สามารถเอา <a href="https://mokmoon.com/mini">https://mokmoon.com/mini</a> ซึ่งเป็น Universal URL ไปทดลองเปิดใช้งาน LINE MINI App ตามช่องทางต่างๆ หรือจะดูโค้ดฉบับเต็มก็ view-source ดูได้เลย</p><p>สุดท้ายนี้ผมหวังเป็นอย่างยิ่งว่าบทความนี้จะช่วยทลายข้อจำกัดการใช้งาน LINE MINI App กับเบราว์เซอร์บนคอมพิวเตอร์ได้ และช่วยให้นักพัฒนาตัดสินใจย้าย LIFF app มาเป็น LINE MINI App ได้อย่างมั่นใจขึ้น</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fe395bdd5875" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/open-line-mini-app-on-computer-fe395bdd5875">ปลดล็อค LINE MINI App ให้ใช้งานผ่าน “เว็บเบราว์เซอร์” บน “คอมพิวเตอร์”</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[มัดรวม API สำหรับดึงสถิติการส่งข้อความใน LINE OA ด้วย Messaging API]]></title>
            <link>https://medium.com/linedevth/line-messaging-api-stat-849ec3094093?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/849ec3094093</guid>
            <category><![CDATA[linemessagingapi]]></category>
            <category><![CDATA[line-official-account]]></category>
            <category><![CDATA[line-bot]]></category>
            <category><![CDATA[line-oa]]></category>
            <category><![CDATA[line-chatbot]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Mon, 07 Jul 2025 03:01:45 GMT</pubDate>
            <atom:updated>2025-07-07T03:01:45.347Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e9cOl41ZoqcoBFWtuyI2iQ.png" /></figure><p>ขอเกริ่นสักนิดนึงก่อนว่าที่มาของบทความนี้ นั้นมาจากสมาชิกใน Community และคนที่รู้จัก ได้สอบถามวิธีการเก็บสถิติการส่งข้อความใน LINE OA ว่ามันมี API ให้ดึงข้อมูลเดียวกันกับเมนู <strong>Insights</strong> ใน <a href="https://manager.line.biz/">OA Manager</a> บ้างไหม ตามตัวอย่างด้านล่างนี้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_QN-xAG4O7EnXsWwOV4lMQ.jpeg" /></figure><p>บางคนต้องการจะเอาข้อมูลตรงนี้ไปทำหน้า Dashboard บางคนจะเอาไปทำระบบ Alert แจ้งเตือน หรือบางคนก็จะเอาไปทำ Report แบบ Programmatic (ไม่อยากไป copy แบบ manual) โดยสิ่งที่ผมทำก็คือ จะต้องไปค้นหา API ตามโจทย์ของแต่ละคนใน Document ซึ่งจะต้องใช้เวลา เนื่องจาก API แต่ละตัวจะอยู่กระจัดกระจายตามหมวดที่ต่างกัน ดังนั้นบทความนี้ จะมารวบตึง API ที่เกี่ยวข้องกับสถิติการส่งข้อความทั้งหมด พร้อมคำอธิบายไว้ในที่เดียว ถ้าพร้อมกันแล้วไปดูกันเลย!</p><h3>1. Get จำนวนข้อความที่สามารถส่งได้ตามแพ็กเกจ</h3><p>API เส้นนี้จะให้ข้อมูลจำนวนข้อความที่สามารถส่งได้ด้วยการ Push, Multicast, Narrowcast, และ Broadcast ในเดือนปัจจุบันตามแพ็กเกจที่ใช้งาน</p><pre>// HTTP Request<strong><br>GET</strong> https://api.line.me/v2/bot/message/quota<br><br>// Request Headers<br><strong>Authorization</strong>: Bearer CHANNEL-ACCESS-TOKEN</pre><p>โดยหาก Request สำเร็จ 200 จะได้ Response เป็น JSON object ที่ประกอบไปด้วย</p><ul><li><strong>type</strong>: limited กรณีมีข้อความที่จำกัด หรือ none กรณียิงได้ไม่อั้น</li><li><strong>value</strong>: จำนวนข้อความที่สามารถส่งได้ โดย object ตัวนี้จะมาในกรณีที่ type ด้านบนเป็น limited</li></ul><pre>// Example Response<br>{<br>  &quot;type&quot;: &quot;limited&quot;,<br>  &quot;value&quot;: 15000<br>}</pre><p><strong>หมายเหตุ</strong>: Rate limit ของ API เส้นนี้คือ 2,000 requests/วินาที</p><h3>2. Get จำนวนข้อความที่ใช้ไปในเดือนแบบ Real-time</h3><p>API เส้นนี้จะให้ข้อมูลจำนวนข้อความรวมที่ถูกส่งผ่าน <a href="https://manager.line.biz/">OA Manager</a> และ Messaging API จากการ Push, Multicast, Narrrowcast, และ Broadcast ในเดือนปัจจุบัน ซึ่งเป็น API ที่ให้ข้อมูลตามการใช้งานจริงแบบ <strong>Real-time</strong></p><pre>// HTTP Request<br><strong>GET</strong> https://api.line.me/v2/bot/message/quota/consumption<br><br>// Request Headers<br><strong>Authorization</strong>: Bearer CHANNEL-ACCESS-TOKEN</pre><p>โดยหาก Request สำเร็จ 200 จะได้ Response เป็น JSON object ที่ประกอบไปด้วย</p><ul><li><strong>totalUsage</strong>: ตัวเลขจำนวนข้อความที่ใช้ไป</li></ul><pre>// Example Response<br>{<br>  &quot;totalUsage&quot;: 4096<br>}</pre><p><strong>หมายเหตุ</strong>: Rate limit ของ API เส้นนี้คือ 2,000 requests/วินาที</p><h3>3. Get จำนวนข้อความที่ส่งด้วยการ Reply</h3><p>API เส้นนี้จะให้ข้อมูลจำนวนข้อความที่ส่งผ่าน Endpoint <a href="https://developers.line.biz/en/reference/messaging-api/#send-reply-message">/bot/message/reply</a></p><pre>// HTTP Request<br><strong>GET</strong> https://api.line.me/v2/bot/message/delivery/reply<br><br>// Request Headers<br><strong>Authorization</strong>: Bearer CHANNEL-ACCESS-TOKEN<br><br>//Query Parameters<br><strong>date</strong>: วันที่ส่งข้อความในรูปแบบ yyyyMMdd ต.ย 20250615 [Required]</pre><p>โดยหาก Request สำเร็จ 200 จะได้ Response เป็น JSON object ที่ประกอบไปด้วย</p><ul><li><strong>status</strong>: สถานะของการประมวลผลจำนวนข้อความ โดยจะแบ่งเป็น<br>+ ready: ประผลเสร็จแล้ว พร้อมให้ดึงข้อมูล<br>+ unready: ยังประมวลผลไม่เสร็จ<br>+ out_of_service: วันที่เลือกเก่ากว่า 31 มี.ค 2018</li><li><strong>success</strong>: ตัวเลขจำนวนข้อความที่ส่งไปตามวันที่กำหนด โดย object ตัวนี้จะมาในกรณีที่ status ด้านบนเป็น ready</li></ul><pre>// Example Response<br>{<br>  &quot;status&quot;: &quot;ready&quot;,<br>  &quot;success&quot;: 2048<br>}</pre><p><strong>หมายเหตุ</strong>: Rate limit ของ API เส้นนี้คือ 2,000 requests/วินาที</p><h3>4. Get จำนวนข้อความที่ส่งด้วยการ Push</h3><p>API เส้นนี้จะให้ข้อมูลจำนวนข้อความที่ส่งผ่าน Endpoint <a href="https://developers.line.biz/en/reference/messaging-api/#send-push-message">/bot/message/push</a></p><pre>// HTTP Request<br><strong>GET</strong> https://api.line.me/v2/bot/message/delivery/push<br><br>// Request Headers<br><strong>Authorization</strong>: Bearer CHANNEL-ACCESS-TOKEN<br><br>//Query Parameters<br><strong>date</strong>: วันที่ส่งข้อความในรูปแบบ yyyyMMdd ต.ย 20250615 [Required]</pre><p>โดยหาก Request สำเร็จ 200 จะได้ Response เป็น JSON object ที่ประกอบไปด้วย</p><ul><li><strong>status</strong>: สถานะของการประมวลผลจำนวนข้อความ โดยจะแบ่งเป็น<br>+ ready: ประผลเสร็จแล้ว พร้อมให้ดึงข้อมูล<br>+ unready: ยังประมวลผลไม่เสร็จ<br>+ out_of_service: วันที่เลือกเก่ากว่า 31 มี.ค 2018</li><li><strong>success</strong>: ตัวเลขจำนวนข้อความที่ส่งไปตามวันที่กำหนด โดย object ตัวนี้จะมาในกรณีที่ status ด้านบนเป็น ready</li></ul><pre>// Example Response<br>{<br>  &quot;status&quot;: &quot;ready&quot;,<br>  &quot;success&quot;: 1024<br>}</pre><p><strong>หมายเหตุ</strong>: Rate limit ของ API เส้นนี้คือ 2,000 requests/วินาที</p><h3>5. Get จำนวนข้อความที่ส่งด้วยการ Multicast</h3><p>API เส้นนี้จะให้ข้อมูลจำนวนข้อความที่ส่งผ่าน Endpoint <a href="https://developers.line.biz/en/reference/messaging-api/#send-multicast-message">/bot/message/multicast</a></p><pre>// HTTP Request<br><strong>GET</strong> https://api.line.me/v2/bot/message/delivery/multicast<br><br>// Request Headers<br><strong>Authorization</strong>: Bearer CHANNEL-ACCESS-TOKEN<br><br>//Query Parameters<br><strong>date</strong>: วันที่ส่งข้อความในรูปแบบ yyyyMMdd ต.ย 20250615 [Required]</pre><p>โดยหาก Request สำเร็จ 200 จะได้ Response เป็น JSON object ที่ประกอบไปด้วย</p><ul><li><strong>status</strong>: สถานะของการประมวลผลจำนวนข้อความ โดยจะแบ่งเป็น<br>+ ready: ประผลเสร็จแล้ว พร้อมให้ดึงข้อมูล<br>+ unready: ยังประมวลผลไม่เสร็จ<br>+ out_of_service: วันที่เลือกเก่ากว่า 31 มี.ค 2018</li><li><strong>success</strong>: ตัวเลขจำนวนข้อความที่ส่งไปตามวันที่กำหนด โดย object ตัวนี้จะมาในกรณีที่ status ด้านบนเป็น ready</li></ul><pre>// Example Response<br>{<br>  &quot;status&quot;: &quot;ready&quot;,<br>  &quot;success&quot;: 512<br>}</pre><p><strong>หมายเหตุ</strong>: Rate limit ของ API เส้นนี้คือ 2,000 requests/วินาที</p><h3>6. Get จำนวนข้อความที่ส่งด้วยการ Broadcast</h3><p>API เส้นนี้จะให้ข้อมูลจำนวนข้อความที่ส่งผ่าน Endpoint <a href="https://developers.line.biz/en/reference/messaging-api/#send-broadcast-message">/bot/message/broadcast</a></p><pre>// HTTP Request<br><strong>GET</strong> https://api.line.me/v2/bot/message/delivery/broadcast<br><br>// Request Headers<br><strong>Authorization</strong>: Bearer CHANNEL-ACCESS-TOKEN<br><br>//Query Parameters<br><strong>date</strong>: วันที่ส่งข้อความในรูปแบบ yyyyMMdd ต.ย 20250615 [Required]</pre><p>โดยหาก Request สำเร็จ 200 จะได้ Response เป็น JSON object ที่ประกอบไปด้วย</p><ul><li><strong>status</strong>: สถานะของการประมวลผลจำนวนข้อความ โดยจะแบ่งเป็น<br>+ ready: ประผลเสร็จแล้ว พร้อมให้ดึงข้อมูล<br>+ unready: ยังประมวลผลไม่เสร็จ<br>+ out_of_service: วันที่เลือกเก่ากว่า 31 มี.ค 2018</li><li><strong>success</strong>: ตัวเลขจำนวนข้อความที่ส่งไปตามวันที่กำหนด โดย object ตัวนี้จะมาในกรณีที่ status ด้านบนเป็น ready</li></ul><pre>// Example Response<br>{<br>  &quot;status&quot;: &quot;ready&quot;,<br>  &quot;success&quot;: 256<br>}</pre><p><strong>หมายเหตุ</strong>: Rate limit ของ API เส้นนี้คือ 2,000 requests/วินาที</p><h3>7. Get จำนวนข้อความที่ส่งจากทุกกระบวนท่า</h3><p>ใครอ่านมาถึงตรงนี้ ถือว่าโชคดีมาก 55 เพราะผมมั่นใจว่าหลายคนไม่รู้จัก API เส้นนี้ โดยความพิเศษมันคือ จะให้ข้อมูลจำนวนข้อความที่ส่งจากทุกประบวนท่าแบบแยกส่วนกัน ทั้งจากฝั่ง <a href="https://manager.line.biz/">OA Manager</a> และ Messaging API ครบจบใน Request เดียวเลย</p><pre>// HTTP Request<br><strong>GET</strong> https://api.line.me/v2/bot/insight/message/delivery<br><br>// Request Headers<br><strong>Authorization</strong>: Bearer CHANNEL-ACCESS-TOKEN<br><br>//Query Parameters<br><strong>date</strong>: วันที่ส่งข้อความในรูปแบบ yyyyMMdd ต.ย 20250615 [Required]</pre><p>โดยหาก Request สำเร็จ 200 จะได้ Response เป็น JSON object ที่ประกอบไปด้วย</p><ul><li><strong>status</strong>: สถานะของการประมวลผลจำนวนข้อความ โดยจะแบ่งเป็น<br>+ ready: ประผลเสร็จแล้ว พร้อมให้ดึงข้อมูล<br>+ unready: ยังประมวลผลไม่เสร็จ<br>+ out_of_service: วันที่เลือกเก่ากว่า 31 มี.ค 2018</li><li><strong>broadcast</strong>: ตัวเลขจำนวนข้อความที่ส่งผ่าน OA Manager แบบที่เลือกผู้รับเป็น <em>All Friends </em>(จะมีในกรณีที่มีการใช้งาน)</li><li><strong>targeting</strong>: ตัวเลขจำนวนข้อความที่ส่งผ่าน OA Manager แบบที่เลือกผู้รับเป็น <em>Targeting </em>(จะมีในกรณีที่มีการใช้งาน)</li><li><strong>apiBroadcast</strong>: ตัวเลขจำนวนข้อความที่ส่งผ่าน Endpoint <a href="https://developers.line.biz/en/reference/messaging-api/#send-broadcast-message">/bot/message/broadcast</a> (จะมีในกรณีที่มีการใช้งาน)</li><li><strong>apiPush</strong>: ตัวเลขจำนวนข้อความที่ส่งผ่าน Endpoint <a href="https://developers.line.biz/en/reference/messaging-api/#send-push-message">/bot/message/push</a> (จะมีในกรณีที่มีการใช้งาน)</li><li><strong>apiMulticast</strong>: ตัวเลขจำนวนข้อความที่ส่งผ่าน Endpoint <a href="https://developers.line.biz/en/reference/messaging-api/#send-multicast-message">/bot/message/multicast</a> (จะมีในกรณีที่มีการใช้งาน)</li><li><strong>apiNarrowcast</strong>: ตัวเลขจำนวนข้อความที่ส่งผ่าน Endpoint <a href="https://developers.line.biz/en/reference/messaging-api/#send-narrowcast-message">/bot/message/narrowcast</a> (จะมีในกรณีที่มีการใช้งาน)</li><li><strong>apiReply</strong>: ตัวเลขจำนวนข้อความที่ส่งผ่าน Endpoint <a href="https://developers.line.biz/en/reference/messaging-api/#send-reply-message">/bot/message/reply</a> (จะมีในกรณีที่มีการใช้งาน)</li><li><strong>pnpNoticeMessage</strong>: ตัวเลขจำนวนข้อความที่ส่งผ่าน <a href="https://medium.com/linedevth/c92487e0dee1">LON</a> (จะมีในกรณีที่มีการใช้งาน)</li></ul><pre>// Example Response<br>{<br>  &quot;status&quot;: &quot;ready&quot;,<br>  &quot;broadcast&quot;: 4096,<br>  &quot;targeting&quot;: 2048<br>}</pre><p><strong>หมายเหตุ</strong>: Rate limit ของ API เส้นนี้คือ 60 requests/ชั่วโมง</p><h3>สรุป</h3><p>เอาเข้าจริงตอนมารวบรวม API เกี่ยวกับสถิติข้อความนี้ก็ทำให้ผมได้รู้ว่า API ตัวทีเด็ดตัวสุดท้ายมัน return ข้อมูลมามากกว่าเมื่อก่อน ซึ่งผมหวังว่าบทความนี้จะถูกใจนักพัฒนาที่กำลังหาวิธีเก็บสถิติการใช้งานข้อความ รวมถึงตัวผมเองที่มั่นใจว่าจะสามารถตอบคำถามเกี่ยวกับเรื่องนี้ได้เร็วขึ้นกว่าเดิมแน่นอน สำหรับวันนี้ขอตัวลาไปก่อน แล้วพบกันใหม่บทความหน้าครับ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=849ec3094093" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/line-messaging-api-stat-849ec3094093">มัดรวม API สำหรับดึงสถิติการส่งข้อความใน LINE OA ด้วย Messaging API</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[สร้าง LINE Chatbot ให้เข้าใจ PDF, Image, Video และ Audio ที่ผู้ใช้ส่งเข้ามา ด้วย Gemini + Firebase]]></title>
            <link>https://medium.com/linedevth/linebot-gemini-multimodal-b4e4f31fa9bc?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/b4e4f31fa9bc</guid>
            <category><![CDATA[gemini]]></category>
            <category><![CDATA[line-bot]]></category>
            <category><![CDATA[firebase-cloud-functions]]></category>
            <category><![CDATA[multimodal]]></category>
            <category><![CDATA[chatbots]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Wed, 04 Jun 2025 05:06:00 GMT</pubDate>
            <atom:updated>2025-06-18T02:31:02.225Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DrdLr2RL4oyKz29T-xeVEw.png" /></figure><h3>สร้าง LINE Chatbot ให้เข้าใจ PDF, Image, Video และ Audio ที่ผู้ใช้ส่งเข้ามา ด้วย Gemini และ Firebase</h3><p>วันนี้หลายๆคนน่าจะคุ้นเคยกับการถามเรื่องต่างๆกับ AI แต่สิ่งที่ตัวมันสามารถตอบเราได้นั้นก็จะเป็น ข้อมูล ความรู้ ทั่วๆไป ซึ่งในบางครั้งเราก็อยากให้มันตอบกลับในสิ่งที่เฉพาะเจาะจงกลับมา เช่น</p><ul><li>ตอบข้อมูลสิทธิประโยชน์ของพนักงานในองค์กรจากไฟล์ PDF</li><li>วิเคราะห์เกรดในใบ Transcript จากไฟล์ Image</li><li>สรุปเนื้อหาที่ได้จากไฟล์ Video</li><li>ถอดบทสนทนาในการประชุมจากไฟล์ Audio</li></ul><p>ตอนนี้บางคนก็อาจจะยังไม่รู้ว่า AI มันสามารถเข้าใจบริบท และโต้ตอบเนื้อหาแบบเฉพาะเรื่อง ผ่านการที่เราให้มันกินไฟล์เนื้อหาเข้าไปก่อนได้ และด้วยที่ผู้ใช้งานส่วนใหญ่คุ้นเคยกับการส่งไฟล์อย่าง PDF, Image, Video และ Audio ผ่าน LINE กันอยู่แล้ว ดังนั้น บทความนี้ผมจะพาทุกคนไปสร้าง LINE Chatbot ที่เข้าใจ File หรือ URL ที่ส่งเข้ามาในห้องแชท พร้อมควบคุมให้การตอบกลับมีเฉพาะเนื้อหาที่อยู่ในไฟล์กัน</p><h3>1. สร้าง Provider และ Channel</h3><p>ใครที่ยังไม่เคยสร้าง Provider และ Channel สำหรับ LINE Chatbot มาก่อน ก็ให้อ่านบทความปฐมบทด้านล่างนี้ แต่ใครที่คุ้นเคยอยู่แล้วให้ข้ามขั้นตอนนี้ไปได้เลย</p><p><a href="https://medium.com/linedevth/%E0%B8%9B%E0%B8%90%E0%B8%A1%E0%B8%9A%E0%B8%97%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%AA%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%87-line-bot-b2cb90643901">ปฐมบทการสร้าง LINE Chatbot (ฉบับอัพเดท 2024)</a></p><h3>2. ติดตั้ง Cloud Functions for Firebase</h3><p>ใครที่ยังไม่เคยพัฒนาโปรแกรมด้วย Cloud Functions for Firebase มาก่อน ก็ให้อ่านบทความด้านล่างนี้(โดยเฉพาะข้อ 2) แต่สำหรับคนที่คุ้นเคยแล้วให้ข้ามโลด</p><p><a href="https://medium.com/firebasethailand/cloud-functions-for-firebase-2nd-gen-cdda33bbd7dd">รู้จัก Cloud Functions for Firebase 2nd Gen ตั้งแต่ Zero จนเป็น Hero</a></p><h3>3. สร้าง Webhook สำหรับ LINE Chatbot</h3><h4>3.1 สร้าง Module สำหรับการทำ HTTP Request</h4><p>ในตัวอย่างนี้ผมจะใช้ <a href="https://www.npmjs.com/package/axios"><strong>axios</strong></a> มาเป็นตัวช่วยในการสร้าง request ซึ่งจะต้องทำการติดตั้ง dependency ตัวนี้ก่อน โดยให้เปิด command line แล้วเข้าไปที่ <strong>/functions</strong> จากนั้นใช้คำสั่ง</p><pre>npm install axios --save</pre><p>และผมจะใช้ <a href="https://www.npmjs.com/package/node-cache"><strong>node-cache</strong></a> มาเก็บข้อมูลไฟล์ที่ผู้ใช้ส่งมาใน memory ซึ่งสำหรับ serverless มันจะหายไปเองหาก Instance ไม่มีคนใช้งาน หรือมีการ deploy โค้ดขึ้นไปสร้าง Instance ใหม่ โดยให้ทำการติดตั้งผ่านคำสั่ง</p><pre>npm install node-cache --save</pre><p>ถัดไปให้สร้างไฟล์ <strong>/functions/.env</strong> แล้วให้ไป copy ค่า <strong>Channel Access Token</strong> ของ Messaging API Channel ใน <a href="https://developers.line.biz/console/">LINE Developers console</a> จากข้อ 1 มาระบุ</p><pre>CHANNEL_ACCESS_TOKEN=YOUR-CHANNEL-ACCESS-TOKEN</pre><p>และเพื่อโค้ดที่สั้น เป็นระเบียบ เราจะแยกฟังก์ชันสำหรับการทำ HTTP Request ออกมาเป็น module โดยภายในจะมีฟังก์ชัน</p><ul><li><strong>getBinary()</strong> สำหรับดึง binary ของไฟล์ที่ส่งผ่าน LINE</li><li><strong>reply()</strong> สำหรับตอบข้อความกลับหาผู้ใช้</li><li><strong>loading()</strong> สำหรับแสดง animation ระหว่างที่ AI กำลังประมวลผล</li><li><strong>curl()</strong> สำหรับดึง binary ของไฟล์ที่ส่งมาเป็น URL</li></ul><p>ขั้นตอนนี้ให้สร้างไฟล์ <a href="https://github.com/jirawatee/LINE-Chatbot-x-Gemini-Multimodal/blob/main/functions/utils/request.js"><strong>/functions/utils/request.js</strong></a> แล้ว copy โค้ดด้านล่างนี้ไปวาง</p><pre>const axios = require(&quot;axios&quot;);<br>const LINE_HEADER = {<br>  &quot;Content-Type&quot;: &quot;application/json&quot;,<br>  Authorization: `Bearer ${process.env.CHANNEL_ACCESS_TOKEN}`<br>};<br><br>class Request {<br>  getBinary(messageId) {<br>    return axios({<br>      method: &quot;get&quot;,<br>      headers: LINE_HEADER,<br>      url: `https://api-data.line.me/v2/bot/message/${messageId}/content`,<br>      responseType: &quot;arraybuffer&quot;<br>    });<br>  }<br>  reply(replyToken, payload) {<br>    return axios({<br>      method: &quot;post&quot;,<br>      url: &quot;https://api.line.me/v2/bot/message/reply&quot;,<br>      headers: LINE_HEADER,<br>      data: { replyToken, messages: payload }<br>    });<br>  }<br>  loading(userId) {<br>    return axios({<br>      method: &quot;post&quot;,<br>      url: &quot;https://api.line.me/v2/bot/chat/loading/start&quot;,<br>      headers: LINE_HEADER,<br>      data: { chatId: userId }<br>    });<br>  }<br>  async c/service/https://medium.com/url(url) {<br>    try {<br>      return axios({ method: &quot;get&quot;, url, responseType: &quot;arraybuffer&quot; });<br>    } catch (error) {<br>      throw error;<br>    }<br>  }<br>}<br><br>module.exports = new Request()</pre><h4>3.2 สร้างและเชื่อมต่อ Webhook กับ Messaging API Channel</h4><p>ให้เปิดไฟล์ <a href="https://github.com/jirawatee/LINE-Chatbot-x-Gemini-Multimodal/blob/main/functions/index.js"><strong>/functions/index.js</strong></a> แล้วให้ copy โค้ดด้านล่างนี้ไปแทนที่โค้ดเดิม เพื่อเตรียมรับ <a href="https://medium.com/linedevth/4cb7da653274">Webhook event</a> ประเภท <strong>image</strong> , <strong>video</strong> , <strong>audio</strong> และ <strong>text</strong> จาก LINE</p><pre>const { onRequest } = require(&quot;firebase-functions/v2/https&quot;);<br>const request = require(&quot;./utils/request&quot;);<br>const NodeCache = require(&quot;node-cache&quot;);<br>const cache = new NodeCache();<br><br>exports.webhook = onRequest(async (req, res) =&gt; {<br>  if (req.method !== &quot;POST&quot;) { return res.send(req.method); }<br><br>  const events = req.body.events;<br>  if (events.length === 0) { return res.end(); }<br><br>  for (const event of events) {<br>    const userId = event.source.userId;<br>    switch (event.type) {<br>      case &quot;postback&quot;:<br>        break;<br>      case &quot;message&quot;:<br>        const messageType = event.message.type;<br>        if ([&quot;image&quot;, &quot;video&quot;, &quot;audio&quot;].includes(messageType)) {<br>        }<br>        if (messageType === &quot;text&quot;) {<br>        }<br>        break;<br>    }<br>  }<br>  res.end();<br>});</pre><p>แล้วก็ให้เปิด command line ขึ้นมาอีกครั้ง และให้แน่ใจว่าเราอยู่ที่ <strong>functions/</strong> จากนั้นใช้คำสั่งด้านล่างนี้เพื่อ deploy ตัว webhook ของเราขึ้น production</p><pre>firebase deploy --only functions</pre><p>เมื่อ deploy เสร็จเรียบร้อยเราจะเห็น Webhook URL โผล่อยู่ใน command line เลย ก็ให้เรา copy มา หรือกรณีที่ไม่พบก็ให้เข้าไปที่ <a href="https://console.firebase.google.com/project/_/functions">Firebase console</a> เลือกโปรเจคที่เราสร้างไว้ แล้วเมนู <strong>Functions</strong> ทางด้านซ้าย ก็จะเจอ ชื่อฟังก์ชัน และ Webhook URL ที่นั่น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lO6i_4IDZeu9-9pzN0fc3w.png" /></figure><p>เมื่อได้ Webhook URL มาอย่างสมใจปองแล้ว ก็เอา URL นี้ไปอัพเดทที่ <strong>Messaging API Channel</strong> ใน <a href="https://developers.line.biz/console/">LINE Developers console</a> ตามที่ได้สร้างไว้ในข้อที่ 1 และให้มั่นใจว่าได้เปิดใช้ <strong>Use webhook</strong> ด้วยแล้ว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*K91PR0OW_rrdkGi5BqBPnw.png" /></figure><h3>4. สร้าง API Key ใน Google AI Studio</h3><p>ขั้นตอนนี้เราจะพาทุกคนไปสร้าง API key สำหรับการใช้งาน Gemini API กัน โดยเริ่มจากคลิกลิงค์ <a href="https://aistudio.google.com/app/apikey">https://aistudio.google.com/app/apikey</a> แล้วกดปุ่ม <strong>Create API key</strong> ที่มุมขวาบน จากนั้นจะเจอ dialog ที่ให้เราเลือก Google Cloud projects โดยให้เราเลือกโปรเจคเดียวกับที่เราได้สร้าง Firebase project ไว้ในข้อที่ 2 ซึ่งเมื่อกดปุ่ม <strong>Create API key in existing project</strong> เราก็จะได้ API key มา</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7ul9gtjF7ifLY3AiAoaBYQ.png" /></figure><p>จากนั้นให้เราเพิ่มตัวแปร <strong>API_KEY</strong> ในไฟล์ <strong>/functions/.env</strong> แล้วให้เอา API key ที่ได้จาก Google AI Studio ระบุลงไปแบบนี้</p><pre>CHANNEL_ACCESS_TOKEN=YOUR-CHANNEL-ACCESS-TOKEN<br>API_KEY=YOUR-API-KEY</pre><h3>5. เชื่อมต่อ LINE Chatbot เข้ากับ Gemini API</h3><h4>5.1 สร้าง Module สำหรับการเรียกใช้ Gemini API</h4><p>ขั้นตอนนี้เราจะมาติดตั้ง <strong>GoogleGenAI</strong> ซึ่งเป็น dependency สำหรับใช้งาน Gemini โดยให้เปิด command line แล้วเข้าไปที่ <strong>/functions</strong> จากนั้นใช้คำสั่ง</p><pre>npm install @google/genai --save</pre><p>และเพื่อให้โค้ดไม่ยาวจนเกินไป เราจะแยกฟังก์ชันทั้งหมดออกมาเป็น module โดยภายในจะมีฟังก์ชัน</p><ul><li><strong>isUrl()</strong> สำหรับตรวจสอบว่าข้อความที่เข้ามาเป็น URL หรือไม่</li><li><strong>getMimeType()</strong> สำหรับดึง Content-Type ของไฟล์ออกมา</li><li><strong>isAllowedMimes()</strong> สำหรับตรวจสอบว่าไฟล์นำไปใช้กับ Gemini ได้หรือไม่</li><li><strong>multimodal()</strong> สำหรับส่ง prompt ที่รวมทั้งข้อความ Text และ File ไปให้ Gemini สร้างเนื้อหากลับมา</li></ul><p>โดยให้สร้างไฟล์ <a href="https://github.com/jirawatee/LINE-Chatbot-x-Gemini-Multimodal/blob/main/functions/utils/gemini.js"><strong>/functions/utils/gemini.js</strong></a> แล้วให้ copy โค้ดด้านล่างนี้ไปวาง</p><pre>const { GoogleGenAI } = require(&quot;@google/genai&quot;);<br>const ai = new GoogleGenAI({ apiKey: `${process.env.API_KEY}` });<br><br>class Gemini {<br>  isUrl(str) {<br>    return /^(http(s)?:\/\/)?(www\.)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(str);<br>  }<br>  getMimeType(response) {<br>    const contentType = response.headers[&quot;content-type&quot;];<br>    return contentType ? contentType.split(&#39;;&#39;)[0] : &#39;application/octet-stream&#39;;<br>  }<br>  isAllowedMimes(mimeType) {<br>    return [<br>      &quot;application/pdf&quot;,<br>      &quot;image/jpeg&quot;,<br>      &quot;image/png&quot;,<br>      &quot;audio/wav&quot;,<br>      &quot;audio/mp3&quot;,<br>      &quot;audio/x-m4a&quot;,<br>      &quot;video/mp4&quot;,<br>      &quot;video/mov&quot;,<br>    ].includes(mimeType);<br>  }<br>  async multimodal(promptArray) {<br>    const response = await ai.models.generateContent({<br>      model: &quot;gemini-2.5-flash&quot;,<br>      contents: promptArray,<br>    });<br>    return response;<br>  }<br>}<br><br>module.exports = new Gemini();</pre><p>เสร็จแล้วก็ให้เราไปอัพเดทโค้ดในไฟล์ <a href="https://github.com/jirawatee/LINE-Chatbot-x-Gemini-Multimodal/blob/main/functions/index.js"><strong>/functions/index.js</strong></a> โดยดึงเอา module ของ Gemini มาใช้งาน</p><pre>const { onRequest } = require(&quot;firebase-functions/v2/https&quot;);<br>const request = require(&quot;./utils/request&quot;);<br>const NodeCache = require(&quot;node-cache&quot;);<br>const cache = new NodeCache();<br>const gemini = require(&quot;./utils/gemini&quot;);</pre><h4>5.2 รับและบันทึกไฟล์ Image, Video, และ Audio ลงใน Cache</h4><p>ที่ไฟล์ <strong>/functions/index.js</strong> เราจะไปดึงเอา <strong>event.message.id</strong> จาก Webhook events มาแล้วเรียกใช้ฟังก์ชัน <strong>getBinary()</strong> เพื่อดึงเอา binary ของไฟล์ออกมาจาก LINE</p><pre>// ...<br>if ([&quot;image&quot;, &quot;video&quot;, &quot;audio&quot;].includes(messageType)) {<br>  const response = await request.getBinary(event.message.id);<br>  if (response.status === 202) {<br>    await request.reply(event.replyToken, [{<br>      type: &quot;text&quot;,<br>      text: `ระบบกำลังประมวลผลไฟล์ กรุณากดปุ่ม &quot;ตรวจสอบสถานะ&quot; ด้านล่างอีกครั้งครับ?`,<br>      quickReply: {<br>        items: [{<br>          type: &quot;action&quot;,<br>          action: { type: &quot;postback&quot;, label: &quot;ตรวจสอบสถานะ&quot;, data: `${event.message.id}` }<br>        }]<br>      }<br>    }]);<br>    break;<br>  }<br>  if (response.status === 200) {<br>    await getReady(response, userId, event.replyToken);<br>    break;<br>  }<br>}<br>// ...</pre><p>ขั้นตอนนี้จะสำคัญตรงที่ เราต้องตรวจสอบ HTTP status ของ response จาก API ด้วย เนื่องจากเวลาในการประมวลผลไฟล์ที่ LINE sever จะขึ้นอยู่กับขนาดของไฟล์โดย <strong>response.status</strong> จะแบ่งออกเป็น 2 แบบ</p><ul><li>202 : กรณี LINE ยังประมวลผลไม่เสร็จ (ไฟล์ขนาดใหญ่) เราจึงยังไม่ได้ binary กลับมา ดังนั้นผมจึงจะ reply() ข้อความกลับไปว่า “<em>ระบบกำลังประมวลผลไฟล์…</em>” พร้อมแนะนำ <a href="https://medium.com/linedevth/9f4a3fe4bfa">Quick Reply</a> ที่แนบ <strong>event.message.id</strong> ไปกับ Postback action เพื่อจะไปจัดการในลำดับถัดไป</li><li>200: กรณี LINE ประมวลผลเสร็จแล้ว (ไฟล์ขนาดเล็ก) เราจะไปเรียกฟังก์ชัน <strong>getReady()</strong></li></ul><p><strong>รอรับ Postback event<br></strong>กรณีที่ได้ HTTP status เป็น 202 และผู้ใช้งานได้กด “ตรวจสอบสถานะ” จาก Quick Reply มา เราจะต้องเขียนโค้ดเพื่อรอรับ Webhook event ประเภท Postback โดยในตัวอย่างนี้ ผมจะเอา <strong>event.message.id</strong> ที่แนบมากับ Postback ไปยิง<strong> </strong><strong>getBinary() </strong>ซ้ำ โดยหาก HTTP status เป็น 200 แล้วก็จะเรียกฟังก์ชัน <strong>getReady()</strong></p><pre>// ...<br>case &quot;postback&quot;:<br>  const response = await request.getBinary(event.postback.data);<br>  if (response.status === 200) {<br>    await getReady(response, userId, event.replyToken);<br>  }<br>  break;<br>// ...</pre><p><strong>ฟังก์ชัน </strong><strong>getReady()</strong> <br>ฟังก์ชันนี้ผมได้สร้างไว้ที่จุดล่างสุดของ <a href="https://github.com/jirawatee/LINE-Chatbot-x-Gemini-Multimodal/blob/main/functions/index.js"><strong>/functions/index.js</strong></a><strong> </strong>เลย โดยหน้าที่ของมันจะมี 3 อย่างด้วยกัน</p><ul><li>ตรวจสอบ Content-Type ของไฟล์ว่ารองรับไหม</li><li>เก็บข้อมูล binary ไฟล์ที่แปลงเป็น base64 และ mimeType ลงใน cache โดยใช้ userId เป็น key ของ cache (หากมีไฟล์ใหม่จะทับไฟล์เดิม) เพื่อให้ cache ในระบบไม่ซ้ำของใครของมัน และไม่มีการกำหนดอายุของ cache เพื่อให้สามารถแชทถามได้ต่อเนื่อง</li><li>Reply หาผู้ใช้งานว่าระบบพร้อมให้คุยกับ AI แล้ว หรือกรณีที่ไฟล์ไม่รองรับก็จะมีการแจ้งว่าระบบรองรับไฟล์ประเภทไหนบ้าง</li></ul><pre>// ...<br>const getReady = async (response, userId, replyToken) =&gt; {<br>  let message = &quot;ปัจจุบันฉันสามารถเข้าใจไฟล์ PDF, JPEG, PNG, WAV, MP3, MP4, และ MOV ได้เท่านั้น&quot;;<br>  const mimeType = gemini.getMimeType(response);<br>  if (gemini.isAllowedMimes(mimeType)) {<br>    const base64 = Buffer.from(response.data).toString(&#39;base64&#39;);<br>    cache.set(`${userId}`, { data: base64, mimeType });<br>    message = &quot;คุณอยากรุ้เรื่องอะไรจากไฟล์ที่คุณส่งมาครับ?&quot;;<br>  }<br>  await request.reply(replyToken, [{ type: &quot;text&quot;, text: message }]);<br>}</pre><h4>5.3 จัดการ File URL และปั้น Prompt ส่ง Gemini</h4><p>นอกจากระบบเราจะรองรับไฟล์ที่ผู้ใช้ส่งมาแล้ว เพื่อเพิ่มความสะดวกในการใช้งาน เราจะเขียนโค้ดเพิ่มอีกหน่อย เพื่อรองรับการส่งไฟล์แบบ URL ไปด้วยเลย ซึ่งกรณีนี้จะทำให้ LINE Chatbot ของเรารับไฟล์ประเภท PDF ได้ (ปัจจุบันผู้ใช้งานไม่สามารถ browse หรือ drag ไฟล์ PDF เข้ามาใน LINE OA ได้)</p><p>โดยเราจะสร้างเงื่อนไขในการตรวจสอบว่าข้อความ Text ที่ส่งเข้ามาว่าเป็น URL หรือไม่ โดยหากเป็น URL จะไปเรียกฟังก์ชัน <strong>curl() </strong>เพื่อดึง binary ออกมา และเอา binary นั้นส่งไปยังฟังก์ชัน <strong>getReady() </strong>ตามโค้ดด้านล่างนี้</p><pre>// ...<br>if (messageType === &quot;text&quot;) {<br>  const prompt = event.message.text.trim();<br><br>  if (gemini.isUrl(prompt)) {<br>    try {<br>      const response = await request.curl(https://medium.com/prompt);<br>      await getReady(response, userId, event.replyToken);<br>    } catch (error) {<br>      await request.reply(event.replyToken, [{ type: &quot;text&quot;, text: &quot;ขออภัย ฉันไม่สามารถเปิด URL นี้ได้ครับ&quot; }]);<br>    }<br>    break;<br>  }<br>}<br>// ...</pre><p>กรณีที่ข้อความ Text ที่ส่งเข้ามาไม่ใช่ URL เราก็ตรวจสอบ cache ก่อนโดยเอา userId ของผู้ใช้ไปดึงมาว่าตอนนี้มีข้อมูลไฟล์อยู่ในนั้นไหม โดยหากมีข้อมูลไฟล์อยู่ใน cache เราจะเอามันมาปั้น prompt ซึ่ง prompt ของเราจะแบ่งออกเป็น 3 ส่วน</p><ul><li>การใส่คำสั่งให้ AI ตอบเฉพาะเนื้อหาในไฟล์</li><li>ข้อมูลไฟล์จาก cache ที่ประกอบไปด้วย base64 และ mimeType</li><li>คำสั่งหรือคำถามที่ผู้ใช้ส่งมา</li></ul><p>ให้เราเอา prompt ที่ปั้น ส่งไปให้ฟังก์ชัน <strong>multimodal() </strong>เพื่อให้ AI ประมวลผล ซึ่งเมื่อเราได้ผลลัพธ์มาก็จะ reply คำตอบกลับไปหาผู้ใช้งาน ตามโค้ดด้านล่าง</p><pre>// ...<br>if (messageType === &quot;text&quot;) {<br>  // ...<br>  const data = cache.get(userId);<br>  if (data) {<br>    await request.loading(userId);<br>    const response = await gemini.multimodal([<br>      &quot;ตอบคำถามผู้ใช้เฉพาะเนื้อหาที่อยู่ในไฟล์เท่านั้น&quot;,<br>      { inlineData: data },<br>      prompt<br>    ]);<br>    await request.reply(event.replyToken, [{ type: &quot;text&quot;, text: `${response.text}` }]);<br>  }<br>}</pre><p>มาถึงตรงนี้โค้ดเราก็พร้อม deploy แล้ว จะรออะไรเปิด command line ขึ้นมา และให้แน่ใจว่าเรา cd ไปอยู่ที่ <strong>functions/</strong> แล้ว จากนั้นเรียกคำสั่งด้านล่างได้เลย</p><pre>firebase deploy --only functions</pre><p>ผมได้เตรียม ไฟล์ + Prompt ไว้สำหรับ Demo ตามนี้ครับ</p><ul><li><a href="https://vos.line-scdn.net/blogs/documents/LINE-Business-Guide.pdf">PDF</a> สรุปเรื่อง LINE Promotion Sticker เป็นภาษาไทย</li><li><a href="https://vos.line-scdn.net/blogs/videos/line-dev-summary-q12025.mp4">Video</a> สรุปเนื้อหาจากไฟล์วิดีโอที่ส่งไป</li><li><a href="https://vos.line-scdn.net/blogs/audios/GoodToSeeYouAgain.mp3">Audio</a> ถอดเนื้อเพลงจากไฟล์ที่ส่งไป</li><li><a href="https://vos.line-scdn.net/blogs/images/yellow.jpg">Image</a> ร้านนี้ชื่ออะไร ? + กระจกในภาพมีทั้งหมดกี่บาน ?</li></ul><p>โดยผลลัพธ์ที่ได้คือมันใช้ได้จริง แม่นมากๆ และที่สำคัญคือช่วยลดเวลาที่ผมจะต้องใช้ในการไปอ่าน ไปแปล ไปดู ไปฟัง ไปศึกษา ได้เยอะมหาศาลเลยครับ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-i-mcGTA6ajUGekJkNKStQ.png" /></figure><h3>Tips</h3><ul><li>Gemini API มีทั้งแพลน Free และ Paid (Pay as you go)</li><li>กรณีที่ใช้ Paid plan จะมีการคิดค่าใช้จ่ายจาก Token โดยรวมทั้ง input และ output แต่ราคาจะขึ้นอยู่กับแต่ละ <a href="https://ai.google.dev/gemini-api/docs/pricing">model ที่เลือกใช้</a></li><li>วิธีตรวจสอบ token รวมทั้ง input และ output ที่เราใช้ไปในแต่ละ request ให้เอา response จาก Gemini ไปเรียกดูได้จาก <a href="https://github.com/jirawatee/LINE-Chatbot-x-Gemini-Multimodal/blob/main/functions/index.js#L70"><strong>response.usageMetadata.totalTokenCount</strong></a></li></ul><h3>สรุป</h3><p>หวังว่าเนื้อหาในบทความนี้จะทำให้ผู้อ่านทุกคนได้ไอเดียไปพัฒนา LINE Chatbot ที่เปรียบเสมือนผู้ช่วยที่คอยตอบคำถามต่างๆให้งานอีเวนท์ต่างๆ องค์กร แผนก ทีม หรือ ตัวคุณเอง ได้อย่างรอบรู้จากไฟล์ที่เราให้มันกินเข้าไป</p><p>และสำหรับใครที่อยากเอาโค้ดไปใช้เลย ผมได้เอา source code ทั้งหมดขึ้น GitHub ให้แล้ว ไป clone กันได้ตามลิงค์ด้านล่างนี้เลยครับ</p><p><a href="https://github.com/jirawatee/LINE-Chatbot-x-Gemini-Multimodal">GitHub - jirawatee/LINE-Chatbot-x-Gemini-Multimodal: Building a LINE Chatbot for user chatting with a PDF, Image, Video, and Audio files by Gemini and Cloud Functions</a></p><p>แล้วพบกันใหม่ในบทความถัดไป สำหรับวันนี้ลาไปก่อน…สวัสดีค้าบ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b4e4f31fa9bc" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/linebot-gemini-multimodal-b4e4f31fa9bc">สร้าง LINE Chatbot ให้เข้าใจ PDF, Image, Video และ Audio ที่ผู้ใช้ส่งเข้ามา ด้วย Gemini + Firebase</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Verify Signature ให้ครอบคลุม Emojis ทั้งจักรวาลกับ LINE Chatbot ที่พัฒนาด้วย Cloud Functions for…]]></title>
            <link>https://medium.com/linedevth/x-line-signature-d13c439ae613?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/d13c439ae613</guid>
            <category><![CDATA[line-chatbot]]></category>
            <category><![CDATA[chatbots]]></category>
            <category><![CDATA[line-bot]]></category>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[cloud-functions]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Mon, 10 Feb 2025 02:45:59 GMT</pubDate>
            <atom:updated>2025-02-10T02:47:53.643Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H4aHXISRsdbXaYlhI6TgdQ.png" /></figure><h3>Verify Signature ให้ครอบคลุม Emoji ทั้งจักรวาลกับ LINE Chatbot ที่พัฒนาด้วย Cloud Functions for Firebase</h3><p>ในปี <strong>2019</strong> ผมได้เคยแนะนำวิธีการเพิ่มความปลอดภัย ที่สำคัญมากในการพัฒนา LINE Chatbot นั่นก็คือการตรวจสอบว่า Webhook Events ที่วิ่งเข้ามายัง Webhook API ของเรานั้นถูกดัดแปลงข้อมูลมาหรือไม่ ผ่านบทความด้านล่างนี้</p><p><a href="https://medium.com/linedevth/x-line-signature-%E0%B8%95%E0%B8%B1%E0%B8%A7%E0%B8%8A%E0%B9%88%E0%B8%A7%E0%B8%A2%E0%B8%88%E0%B8%B2%E0%B8%81-line-%E0%B8%97%E0%B8%B5%E0%B9%88%E0%B8%88%E0%B8%B0%E0%B8%8A%E0%B9%88%E0%B8%A7%E0%B8%A2%E0%B8%A2%E0%B8%81%E0%B8%A3%E0%B8%B0%E0%B8%94%E0%B8%B1%E0%B8%9A%E0%B8%84%E0%B8%A7%E0%B8%B2%E0%B8%A1%E0%B8%9B%E0%B8%A5%E0%B8%AD%E0%B8%94%E0%B8%A0%E0%B8%B1%E0%B8%A2%E0%B9%83%E0%B8%AB%E0%B9%89%E0%B8%81%E0%B8%B1%E0%B8%9A-webhook-%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B8%84%E0%B8%B8%E0%B8%93-7a94d9548f34">X-Line-Signature ตัวช่วยจาก LINE ที่จะช่วยยกระดับความปลอดภัยให้กับ Webhook ของคุณ</a></p><p>บทความดังกล่าวมีนักพัฒนาในบ้านเราให้ความสนใจเข้ามาอ่านเป็นจำนวนมาก หลายๆคนก็นำมันไปใช้กับ LINE Chatbot ของตัวเอง แต่หลังจากนั้นผมก็ได้ feedback กลับมา ว่าโค้ดที่ใช้ในบทความดังกล่าวนั้นไม่ครอบคลุมการมี emoji ในข้อความ และโชคดีที่มีนักพัฒนาใน <a href="https://www.facebook.com/groups/LINEDEVTH/posts/661855664478343/?comment_id=662300387767204">Community</a> มาแชร์วิธีการที่จะเขียนโค้ดให้รองรับ emoji ในข้อความ จึงเป็นที่มาของวิดีโอ Secret Tips EP.18 ที่ปล่อยออกมาในปี <strong>2021</strong></p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FMOv5yI3WmUk%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DMOv5yI3WmUk&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FMOv5yI3WmUk%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/e1a9f46f8e250569b1facb3cee8fd7a0/href">https://medium.com/media/e1a9f46f8e250569b1facb3cee8fd7a0/href</a></iframe><p>แต่ในท้ายคลิปดังกล่าว ผมได้ทิ้งท้ายไว้ว่าโค้ดที่ใช้จะรองรับ emoji ที่มีในปัจจุบันเท่านั้น โดยสาเหตุที่ผมพูดเช่นนั้นก็เพราะ emoji มันเกิดขึ้นใหม่ได้ทุกปี ซึ่งโค้ดที่ใช้ในคลิปจะต้องคอยอัพเดทมันอยู่เรื่อยๆนั่นเอง</p><p>หลังจากปล่อยคลิปไป นักพัฒนาหลายๆคนก็เอาเทคนิคนี้ไปปรับใช้ และเหมือนมันจะใช้งานได้ดีเลย จนกระทั่งปีที่แล้ว <strong>2024</strong> ก็มีนักพัฒนาชาวญี่ปุ่นใน GitHub ชื่อ Yang-33 มาคอมเม้นท์ใต้โค้ดของผมใจความว่า “<em>การแปลง request.body แล้วนำไปสร้าง signature จะมีโอกาสที่ข้อมูลจะเปลี่ยนแปลงไป และจะ verify ไม่ผ่านได้</em>”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Mm6phmz6fEKPHCJseKLs-g.png" /><figcaption><a href="https://gist.github.com/jirawatee/366d6bef98b137131ab53dfa079bd0a4?permalink_comment_id=5152828">https://gist.github.com/jirawatee/366d6bef98b137131ab53dfa079bd0a4?permalink_comment_id=5152828</a></figcaption></figure><p>ผมขอยอมรับว่าช่วงนั้นผมค่อนข้างยุ่ง เลยไม่ได้ไปพิสูจน์ ทดลอง และหาคำตอบในทันที ซึ่งหลังจากนั้นผมก็ลืมมันไป แล้วเวลาก็ผ่านไปอย่างรวดเร็ว Fast forward มาที่ปี <strong>2025</strong> <a href="https://medium.com/u/afe99d129654">Supakarn Laorattanakul</a> ซึ่งเป็น LINE API Expert ของประเทศไทย ทักผมมาว่า</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lJHXwyACBhdf7wBSZLVewA.png" /></figure><p>จากข้อความนี้ ทำให้ผมรู้ว่าโค้ดที่ผมเคยแนะนำไว้ มีคนนำไปใช้ และกำลังเจอกับปัญหาอยู่(มีอีกหลายคนแน่ๆ) ทันใดนั้นเอง ผมก็คิดขึ้นมาได้ว่าเรื่องนี้ต้องมีคนรับผิดชอบ! ซึ่งจะเป็นใครหละ ถ้าไม่ใช้ Jirawatee นางตัวดี 555+</p><p>เอาหละบทความนี้ ผมจะพาทุกคนไปเข้าใจเหตุของปัญหา หนทางสู่การดับทุกข์ ที่รับรองได้ว่าเป็นทางสว่างที่ยั่งยืน คลอบคลุม emoji ทั้ง อดีต ปัจจุบัน และ อนาคต</p><h3>[แบบเดิม] วิธีการสร้าง Signature จาก Webhook event ใน Cloud Functions for Firebase</h3><pre>const crypto = require(&#39;crypto&#39;);<br><br>let text = JSON.stringify(request.body);<br><br>text = text.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, (e) =&gt; {<br>  return &quot;\\u&quot; + e.charCodeAt(0).toString(16).toUpperCase() + &quot;\\u&quot; + e.charCodeAt(1).toString(16).toUpperCase();<br>});<br><br>const signature = crypto.createHmac(&#39;SHA256&#39;, CHANNEL_SECRET).update(text).digest(&#39;base64&#39;).toString();<br>if (request.headers[&#39;x-line-signature&#39;] !== signature) {<br>  return res.status(401).send(&#39;Unauthorized&#39;);<br>}</pre><p>จากโค้ดด้านบนจะเป็นโค้ดเดิมที่ผมใช้มาตั้งแต่ปี 2021 โดยมันจะทำการ verify ตัว <strong>x-line-signature</strong> ที่ส่งมากับ Webhook event กับ <strong>signature</strong> ที่เราสร้างขึ้นมาจาก request.body และจุดสำคัญจะอยู่ที่ <strong>update()</strong> ซึ่งเป็น method ที่จะรับ argument ประเภท <strong>String</strong>, <strong>Buffer</strong>, <strong>TypedArray</strong>, และ <strong>DataView</strong> เท่านั้น</p><p>แต่ประเภทของ <strong>request.body</strong> ที่เราได้จาก Cloud Functions for Firebase นั้นจะเป็น <strong>JSON</strong> <strong>Object</strong> จึงทำให้เราต้องทำการแปลงมันก่อน และวิธีที่โค้ดด้านบนใช้คือการแปลงเป็น String ด้วยคำสั่ง <strong>JSON.stringify();</strong></p><p>โดยการแปลง <strong>request.body</strong> จะไม่พบปัญหากับข้อความที่เป็น <strong>text</strong> เพียวๆ แต่เมื่อไรก็ตามที่มี <strong>emoji</strong> อยู่ในข้อความจะทำให้ผลของการสร้าง <strong>signature</strong> นั้นจะไม่ตรงกับ <strong>x-line-signature </strong>จึงมีคนนำเสนอเทคนิคที่จะหา emoji ในข้อความและแปลงมันกลับมาให้เหมือนเดิมด้วย <strong>text.replace()</strong> ตามโค้ดด้านบน แต่ก็จะสังเกตได้ว่าการกำหนดตัว emoji จะอยู่ในรูปแบบของ <strong>regex</strong> ซึ่งมีการระบุรูปแบบ emoji ในวงจำกัดเท่าที่มีอยู่ ณ เวลานั้น</p><h4>ตัวอย่าง emoji แบบที่ ผ่าน และ ไม่ผ่าน</h4><p>สำหรับนักพัฒนาที่อ่านมาถึงตรงนี้ และใช้โค้ดแบบเดียวกับที่ผมแนะนำไปก่อนหน้า มาลองพิสูจน์กันว่า เรื่องที่ผมเล่า มันเป็นจริงหรือไม่ โดยการพิมพ์ 2 ข้อความด้านล่างนี้ทีละข้อความ</p><pre>ข้อความที่ 1 🔥 Fire<br><br>ข้อความที่ 2 ❌ Cross mark</pre><p>ถ้าคุณได้ทดลองแล้ว คุณจะพบว่า ข้อความที่ 2 จะไม่ผ่านการ verify signaure โดยตัวอย่างค่า signature ที่ผม log ออกมาจะเห็นชัดว่า <strong>x-line-signature</strong> กับ<strong> signature</strong> ที่ผมสร้างไม่เหมือนกัน</p><pre>// x-line-signature<br>4dO6jgHXCr7MQ8AcJP1Hf3WVzEEhlHUEuajRG8LzLII=<br><br>// signature<br>jw4bm/FoOtmA5iE6A5kaA5lh2uX30JNchT1ke3kHZVg=<br></pre><h3>[แบบยั่งยืน] วิธีการสร้าง Signature จาก Webhook event ใน Cloud Functions for Firebase</h3><p>ต่อจากนี้คือหนทางของการดับทุกข์ของผมนะครับ เอ้า ไปเริ่มกันเลย</p><p>สิ่งแรกที่ผมคิดคือ หากเราแปลง request.body จาก JSON Object มาเป็น Buffer แล้วโยนมันเข้าไปใน update() มันจะได้ไหมนะ</p><pre>const jsonString = JSON.stringify(request.body);<br>const buffer = Buffer.from(jsonString, &#39;utf8&#39;);</pre><p>จากผลการทดลองบอกผมว่าค่า signature ที่ได้ <strong>จะไม่เท่ากับ</strong> x-line-signature<strong> </strong>ดังนั้น <strong>อย่าหาทำ</strong>ท่านี้กันนะครับทุกคน</p><p>ถัดมาผมได้ลองค้นหาใน Google อีก 2–3 ท่า แต่ทั้งหมดก็ยังไม่เวิร์ค จนกระทั่งผมมาเจอโพสนี้ใน reddit</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HhmtIWMAlgDxBsfazY1K6g.png" /></figure><p>ทำให้ผมได้ keyword ที่สำคัญมานั่นคือ raw body และสิ่งที่ผมคิดต่อมา คือคนที่น่าจะรู้เรื่องนี้ดีที่สุดคนหนึ่งก็ควรเป็น…Gemini เพราะถ้ามันไม่รู้บริการในบริษัทตัวเอง ก็ไม่ต้องไปหวังพึ่ง AI ตัวอื่นแล้ว ผมจึงได้ถามมันไปว่า “<em>Gemini เอ๋ย บอกข้าเถิดใครงามเลิศในปฐพี</em>” หยอกๆ จริงๆแล้วผมถามว่า</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sZcpTnae6-jctbXUPxLdhg.png" /></figure><p>เท่านั้นหละ ผมรีบ console.log(request); ดูทันทีว่ามี <strong>rawBody</strong> พระเอกของผมอยู่ในนั้นไหม และนี่คือผลที่ได้…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9PC29t664gOjTXfrCrpdZA.jpeg" /></figure><p>มันมี rawBody อยู่จริง และประเภทของมันก็เป็น Buffer ซะด้วย ผมถึงกับต้องอุทานขึ้นมาว่า…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/240/1*0Gq8eMSsqbb_-WS-vbbysA.png" /></figure><p>และเมื่อผมได้มาปรับแต่งโค้ดใหม่โดยเปลี่ยนการส่ง request.body ไปเป็น <strong>request.rawBody</strong> ไปกับ <strong>update()</strong> ผมก็พบว่าไม่ว่าจะส่งข้อความที่มี emoji รูปแบบไหนมา <strong>signature ที่ผมสร้างจะเท่ากับ x-line-signature </strong>แล้ว</p><pre>const crypto = require(&#39;crypto&#39;);<br><br>const signature = crypto.createHmac(&#39;SHA256&#39;, CHANNEL_SECRET).update(request.rawBody).digest(&#39;base64&#39;).toString();<br>if (request.headers[&#39;x-line-signature&#39;] !== signature) {<br>  return res.status(401).send(&#39;Unauthorized&#39;);<br>}</pre><h4>หมายเหตุ</h4><ul><li><strong>rawBody</strong> จะมาเมื่อมี <strong>body</strong> ส่งมากับ request <strong>เสมอ</strong></li><li>วิธีการในบทความนี้นั้นรองรับการสร้าง Signature แบบถูกหลักอนามัย ทั้งใน Cloud Functions for Firebase <strong>1st</strong> และ <strong>2nd Gen</strong> ที่ใช้ <strong>Node.js</strong></li></ul><h3>สรุป</h3><p>บทความนี้ทำให้ผมรู้สึกได้ปลดปล่อยตัวเองจากความทุกข์ ที่โค้ดไม่ครอบคลุม emoji ใหม่ๆ และเกิด issue กับคนที่นำไปใช้ โดยเมื่อผมได้พบวิถีของการดับทุกข์นี้แล้ว ผมก็บอกกับตัวเองทันทีว่า “<em>กรูจะไม่เก็บเรื่องนี้ไว้คนเดียวแน่ๆ 555</em>” หวังว่าโค้ดชุดใหม่นี้จะช่วยให้ LINE Chatbot ของผู้อ่านตอบสนองผู้ใช้งานได้คลอบคลุม 100% จากนี้ไปนะครับ</p><p>สุดท้ายนี้ผมขอขอบคุณทุกท่านที่ได้ feedback ผมมา รวมถึงผู้ที่ช่วยผมหาคำตอบทุกคน โดยเฉพาะอย่างยิ่ง คุณ Siraphoom Sri-Art, Yang-33 และ คุณพร็อมท์ LAE ครับ ผมจะเป็นคนที่เก่งขึ้นไม่ได้ถ้าไม่มี…<strong>Community</strong> ขอบคุณค้าบ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d13c439ae613" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/x-line-signature-d13c439ae613">Verify Signature ให้ครอบคลุม Emojis ทั้งจักรวาลกับ LINE Chatbot ที่พัฒนาด้วย Cloud Functions for…</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[รู้หมดใครเพิ่มเพื่อน LINE OA จากช่องทางไหน ด้วยระบบ Friend Path ที่คุณก็สร้างเองได้]]></title>
            <link>https://medium.com/linedevth/friend-path-of-line-oa-213dd994b9a3?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/213dd994b9a3</guid>
            <category><![CDATA[line-oa]]></category>
            <category><![CDATA[chatbots]]></category>
            <category><![CDATA[liff]]></category>
            <category><![CDATA[utm-parameters]]></category>
            <category><![CDATA[firebase]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Wed, 18 Dec 2024 04:01:38 GMT</pubDate>
            <atom:updated>2025-07-10T03:06:39.378Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*J042f6eP0zZZgbzDbXbESw.png" /></figure><p>หนึ่งในคำถามที่ผมมักจะได้รับ จากคนที่ให้บริการต่างๆบน LINE OA ไม่ว่าจะเป็น ผู้ประกอบการ, นักพัฒนา โดยเฉพาะอย่างยิ่ง ทีมการตลาด ทั้งมือใหม่และมืออาชีพ ในปีนี้ ก็คือ…</p><blockquote>“เราจะรู้ได้อย่างไรว่า ลูกค้าคนนั้นๆ add ตัว LINE OA มาจากช่องทางไหน ที่เราไปโปรโมทไว้ ?”</blockquote><p>แน่นอนว่าถ้าเรามีข้อมูลตรงนี้ ก็จะสามารถวัดผลได้ว่า Marketing Campaign ที่ไปโปรโมท LINE OA ตามช่องทางต่างๆ เช่น LINE, Facebook, Google, X, Instagram, รวมถึง QR code ที่เราติดไว้บนป้ายโฆษณา หรือที่ร้านค้านั้นๆ ช่องทางไหนทำให้เรามี followers เพิ่มขึ้นมากที่สุด</p><p>ดังนั้น เพื่อให้ตอบโจทย์สิ่งที่หลายคนต้องการ บทความนี้ผมจะชวนทุกคนไปสร้างระบบ Friend Path ฉบับ DIY ที่จะช่วยให้คุณได้รู้ว่า followers ของคุณมาจากช่องทางไหนที่คุณไปโปรโมทไว้บ้าง โดยขั้นตอนจะมีดังนี้</p><ol><li>สร้าง LINE OA และเปิดใช้งาน Messaging API</li><li>สร้าง API เพื่อรับและเก็บข้อมูล Marketing Campaign</li><li>สร้าง LIFF app เพื่อเก็บข้อมูลและพาผู้ใช้ไปหา LINE OA</li><li>สร้าง Webhook API เพื่อรอรับสัญญาณการ Add / Unblock</li><li>Deploy และ Demo</li></ol><h3>1. สร้าง LINE OA และเปิดใช้งาน Messaging API</h3><p>ขั้นตอนแรก เราจะต้องมี LINE Chabot (LINE OA + Messaging API Channel) ซะก่อน โดยถ้าใครมีแล้วให้ข้ามขั้นตอนนี้ไป แต่หากใครยังไม่มีให้สร้างตามข้อ 1–3 ของบทความด้านล่างนี้ได้เลยครับ</p><p><a href="https://medium.com/linedevth/%E0%B8%9B%E0%B8%90%E0%B8%A1%E0%B8%9A%E0%B8%97%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%AA%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%87-line-bot-b2cb90643901">ปฐมบทการสร้าง LINE Chatbot (ฉบับอัพเดท 2024)</a></p><h3>2. สร้าง API เพื่อรับและเก็บข้อมูล Marketing Campaign</h3><p>ขั้นตอนนี้เราจะมาเตรียม API ไว้สำหรับรอรับค่า Marketing Campaign ที่จะส่งจาก LIFF app กัน โดยผมจะเลือกสร้างตัว API ด้วย Callable Function จาก Cloud Functions for Firebase เพราะผมชอบที่จะไม่ต้องระบุตัว Endpoint URL ลงไปฝั่ง client side</p><h4>2.1 สร้าง Callable Function และ ลงทะเบียน Web app</h4><p>ขั้นตอนนี้ เราจะมาสร้างโปรเจคใน Firebase และ setup ตัว Callable Function ขึ้นมาไว้ให้พร้อมเขียนโปรแกรม และไปลงทะเบียน Web app ของเราให้เรียบร้อยตามข้อ 1–2.1 ของบทความด้านล่างนี้เลยครับ</p><p><a href="https://medium.com/firebasethailand/callable-function-c423e75f6ece">รู้จัก Callable Function ใน Cloud Functions for Firebase ตั้งแต่ Zero จนเป็น Hero</a></p><h4>2.2 เปิดใช้งาน Cloud Firestore</h4><p>ด้วยระบบ Friend Path ฉบับ DIY ที่เรากำลังจะสร้างขึ้น จำเป็นต้องมีการเก็บและเรียกใช้ข้อมูล ผมจึงขอเลือกใช้ Firestore มาเป็น database ในงานนี้ ดังนั้นให้เราไปเปิดใช้งาน Cloud Firestore กันก่อน โดยใน <a href="https://console.firebase.google.com/">Firebase console</a> ให้เลือกเมนู <strong>Firestore Database</strong> ทางซ้าย แล้วกด <strong>Create database</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P69ONq4XmeBdL8HVj-ejpg.png" /></figure><p>จากนั้นเลือก location ของ database ตามต้องการ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6oWZmrKNbwFyTg-akGgM9A.png" /></figure><p>ถัดไปให้เลือก <strong>Start in production mode</strong> เพื่อที่จะป้องกันไม่ให้เขียนและอ่านข้อมูลจาก client script ได้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Iw63gmG3jyWffEvRcmpBQA.png" /></figure><p>จากนั้นกดปุ่ม Create ก็เป็นการเปิดใช้งาน Firestore เรียบร้อยครับ</p><h4>2.3 สร้าง API จาก Callable Function ไว้เพื่อรับและเก็บข้อมูล</h4><p>เมื่อเราเตรียม Callable Function ไว้เรียบร้อย ขั้นตอนนี้เราจะมาเขียนโค้ด เพื่อสร้าง API กัน โดยเราจะใช้ axios มาเป็นตัวช่วยในการสร้าง request ซึ่งเราจะต้องทำการติดตั้ง dependency ตัวนี้ก่อน</p><p>เริ่มต้นให้เปิด command line แล้วเข้าไปที่ <strong>/functions</strong> จากนั้นใช้คำสั่งเพื่อติดตั้ง axios ก่อน เพราะเราจะใช้ dependency ตัวนี้ในการเรียกใช้ LINE API</p><pre>npm install axios --save</pre><p>เพื่อโค้ดที่สั้น เป็นระเบียบ เราจะสร้างฟังก์ชันสำหรับการเรียกใช้ LINE API และ Firebase ออกมาเป็น module โดยมีรายละเอียดดังนี้</p><p>LINE module (<strong>/functions/utils/line.js</strong>) โดยภายในจะมีฟังก์ชัน <strong>getUserProfile()</strong> ที่จะทำหน้าที่ในการดึงข้อมูลผู้ใช้ด้วย <strong>accessToken</strong> ที่ได้จาก LIFF app</p><pre>const axios = require(&quot;axios&quot;);<br><br>class LINE {<br>  async getUserProfile(accessToken) {<br>    try {<br>      const profile = await axios({<br>        method: &#39;get&#39;,<br>        url: &quot;https://api.line.me/v2/profile&quot;,<br>        headers: { Authorization: `Bearer ${accessToken}` }<br>      });<br>      return profile.data;<br>    } catch (error) { return error.response.status; }<br>  }<br>}<br>module.exports = new LINE();</pre><p>Firebase module (<strong>/functions/utils/firebase.js</strong>) โดยภายในจะมี 3 ฟังก์ชันดังนี้</p><pre>const { getFirestore } = require(&quot;firebase-admin/firestore&quot;);<br>const { initializeApp } = require(&quot;firebase-admin/app&quot;);<br>initializeApp();<br>const followersRef = getFirestore().collection(&quot;followers&quot;);<br><br>class Firebase {<br>  // สำหรับดึงข้อมูลการ add/unblock ตัว LINE OA ของผู้ใช้แต่ละคนจาก Firestore<br>  getFollower(userId) {<br>    return followersRef.doc(userId).get();<br>  }<br><br>  // สำหรับ เพิ่มหรืออัพเดท ข้อมูลการ add/unblock ตัว LINE OA ของผู้ใช้แต่ละคน<br>  upSertFollower(userId, data) {<br>    return followersRef.doc(userId).set(data, { merge: true });<br>  }<br><br>  // สำหรับนับจำนวน follower ที่ add/unblock ตัว LINE OA จากแต่ละช่องทาง<br>  countUtmResource(utmSource) {<br>    return followersRef.where(&quot;utmSource&quot;, &quot;==&quot;, utmSource).where(&quot;isAddedOrUnblockedFromAd&quot;, &quot;==&quot;, true).count().get();<br>  }<br>}<br>module.exports = new Firebase()</pre><p>เสร็จแล้วให้ไปที่ไฟล์ <strong>/functions/index.js</strong> แล้ว copy โค้ดด้านล่างนี้ไปแทนที่โค้ดเดิมทั้งหมดในไฟล์</p><pre>const { onCall, onRequest, HttpsError } = require(&quot;firebase-functions/v2/https&quot;);<br><br>// Import LINE และ Firebase modules<br>const firebase = require(&quot;./utils/firebase&quot;);<br>const line = require(&quot;./utils/line&quot;);<br><br>exports.myCallable = onCall(async (request) =&gt; {<br>  // ดัึง user profile ด้วย accessToken จาก LIFF app<br>  const profile = await line.getUserProfile(request.data.accessToken);<br>  if (profile &gt;= 400) {<br>    throw new HttpsError(&quot;failed-precondition&quot;, &quot;The function must be called with a valid accessToken&quot;);<br>  }<br><br>  // Insert ข้อมูลของผู้ใช้ที่มาคลิก ad ไปเก็บที่ Firestore<br>  await firebase.upSertFollower(profile.userId, {<br>    utmSource: request.data.utmSource,<br>    timestamp: Date.now(),<br>    isAddedOrUnblockedFromAd: false<br>  });<br>  <br>  return request.data.utmSource;<br>});</pre><p>การทำงานของ myCallable คือจะเอา <strong>accessToken</strong> จาก LIFF app ไปยิง LINE API เพื่อดึงข้อมูลผู้ใช้ออกมา โดยสิ่งที่เราอยากได้จากข้อมูลผู้ใช้คือ <strong>userId</strong> เพื่อมาเป็น identity ของแต่ละ record และจะมีการเก็บค่าอีก 3 ค่าใน Firestore ดังนี้</p><ul><li><strong>utmSource</strong>: <strong>ชื่อช่องทาง</strong>ที่คุณไปโปรโมทซึ่งได้มาจาก LIFF app</li><li><strong>timestamp</strong>: เวลา ณ จังหวะที่ผู้ใช้เปิด LIFF app ขึ้นมา</li><li><strong>isAddedOrUnblockedFromAd</strong>: สิ่งที่จะยืนยันว่าผู้ใช้คนนั้นๆได้ add/unblock ตัว LINE OA จากช่องทางที่คุณไปโปรโมท โดยค่าเริ่มต้นจะเป็น <strong>false</strong></li></ul><h3>3. สร้าง LIFF app เพื่อรับข้อมูลและพาผู้ใช้ไปหา LINE OA</h3><p>ขั้นตอนนี้เราจะมาสร้าง LIFF app เพื่อรับและเก็บข้อมูล ว่าใครมาจากช่องทางไหน ก่อนที่จะมา add/unblock ตัว LINE OA โดยผลลัพธ์เมื่อเราพัฒนาเสร็จ เราจะได้ LIFF URL และ Endpoint URL ไปไว้เป็นลิงค์ติด ad ต่างๆ หรือ QR code เพื่อโปรโมท LINE OA ต่อไปได้</p><h4>3.1 สร้าง LINE Login Channel และ LIFF App</h4><p>ขั้นตอนนี้เราจะมาสร้าง LINE Login Channel และ LIFF app ตามบทความด้านล่างนี้ตั้งแต่ <strong>ข้อ 2–3 (ย้ำว่าไม่ทำข้อ 1)</strong></p><p><a href="https://medium.com/linedevth/%E0%B9%80%E0%B8%89%E0%B8%A5%E0%B8%A2%E0%B8%A7%E0%B8%B4%E0%B8%98%E0%B8%B5%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%94%E0%B8%B6%E0%B8%87-email-%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B8%9C%E0%B8%B9%E0%B9%89%E0%B9%83%E0%B8%8A%E0%B9%89%E0%B9%83%E0%B8%99-liff-v2-93ce9b944b57">สร้างและพัฒนา LIFF v2 ให้ดึง Email ของผู้ใช้ออกมาได้</a></p><p>โดยสิ่งที่คุณจำเป็นต้อง <strong>ทำเพิ่ม</strong> นอกเหนือจากบทความด้านบนนี้จะมีอยู่<strong> สองเรื่อง</strong> คือ</p><ul><li><strong>เรื่องแรก</strong>: จะต้องไปทำการ <strong>Linked LINE Official Account</strong> ที่แท็บ <strong>Basic settings</strong> ของ LINE Login Channel ใน <a href="https://developers.line.biz/console/">LINE Developers Console</a> โดยให้เราเลือกเชื่อมต่อ LINE OA ที่เราสร้างไว้ ซึ่งจะมีผลทำให้ LIFF app สามารถรู้ได้ว่าผู้ใช้งานที่เปิด LIFF app คนนั้นๆเป็นเพื่อนกับ LINE OA แล้วหรือยัง</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CfqJu78xlT5muwtwaFBueg.png" /></figure><ul><li><strong>เรื่องที่สอง</strong>: จะต้องตั้งค่า <strong>Add friend option</strong> ของ LIFF app ใน <a href="https://developers.line.biz/console/">LINE Developers Console</a> ให้เป็น <strong>Off</strong> ซะก่อน เนื่องจากหากผู้ใช้งานมีการ add/unblock ตัว LINE OA ที่หน้า consent ก่อนที่จะมาเจอหน้า LIFF app จะทำให้เรา track ไม่ได้ว่า followers คนนั้นๆมาจากช่องทางใด</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XD1oKRD3O3vhDgoFi_wy7Q.png" /></figure><h4><strong>*** คำเตือน ***</strong></h4><p>ระบบ Friend Path ฉบับ DIY ที่เรากำลังสร้างขึ้นจะ<strong>ไม่สามารถ</strong>ทำงานได้ถูกต้อง หากคุณไปสร้าง <strong>LINE Login Channel</strong> ภายใต้ Provider ใหม่ ซึ่งต่างกันกับ Provider ที่ <strong>Messaging API Channel</strong> อาศัยอยู่ เนื่องจากเราจำเป็นต้องเชื่อมโยงผู้ใช้จาก LIFF app และ LINE Chatbot ด้วย <strong>userId</strong> ที่เหมือนกัน</p><h4>3.2 พัฒนา LIFF app และเชื่อมต่อ Callable Function</h4><p>ขั้นตอนนี้เราจะมาเขียน JavaScript เพื่อที่จะดักเก็บข้อมูล marketing campaign และพาผู้ใช้ไปหน้า add/unblock ตัว LINE OA</p><p>เริ่มต้นให้สร้างไฟล์ <strong>main.js</strong> ขึ้นมาก่อน แล้ว copy ฟังก์ชัน <strong>isMobile()</strong> ด้านล่างนี้ไปใส่ไว้ โดยเราจะใช้ฟังก์ชันนี้ในลำดับถัดไป</p><pre>function isMobile() {<br>  let check = false;<br>  (function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor || window.opera);<br>  return check;<br>}</pre><p>จากนั้นให้สร้างไฟล์ <strong>index.html</strong> ขึ้นมา แล้วให้ import ตัว LIFF SDK และ <strong>main.js</strong> ที่สร้างไว้เข้ามา</p><pre>&lt;script src=&quot;https://static.line-scdn.net/liff/edge/2/sdk.js&quot;&gt;&lt;/script&gt;<br>&lt;script src=&quot;main.js&quot;&gt;&lt;/script&gt;</pre><p>ถัดไปให้เรา import ตัว Firebase SDK และ initialize ให้พร้อมใช้งาน</p><pre>&lt;script type=&quot;module&quot;&gt;<br>  import { initializeApp } from &quot;https://www.gstatic.com/firebasejs/11.1.0/firebase-app.js&quot;;<br>  import { getFunctions, httpsCallable, connectFunctionsEmulator } from &quot;https://www.gstatic.com/firebasejs/11.1.0/firebase-functions.js&quot;;<br><br>  const app = initializeApp({<br>    apiKey: &quot;### FIREBASE API KEY ###&quot;,<br>    authDomain: &quot;### FIREBASE AUTH DOMAIN ###&quot;,<br>    projectId: &quot;### CLOUD FUNCTIONS PROJECT ID ###&quot;<br>  });<br>  const functions = getFunctions(app);<br>  async function main() {<br>    // ...<br>  }<br>  main();<br>&lt;/script&gt;</pre><p><strong>หมายเหตุ</strong>: ค่า apiKey, authDomain, และ projectId หาได้จาก <strong>ขั้นตอนที่ 2.1</strong></p><p>ภายในฟังก์ชัน <strong>main()</strong> ให้เราเพิ่มโค้ดตามด้านล่างนี้</p><pre>// URL ของ LINE OA โดยให้ระบุ Basic ID จาก ขั้นตอนที่ 1<br>const LINE_OA_URL = &quot;https://line.me/R/ti/p/### BASIC ID ###&quot;;<br><br>// ดึง query parameters ทั้งหมดที่ติดมากับ URL<br>const params = new URLSearchParams(window.location.search);<br><br>// Initial ตัว LIFF app ด้วย LIFF ID ที่ได้จาก ขั้นตอนที่ 3.1<br>await liff.init({ liffId: &quot;### LIFF ID ###&quot; });</pre><p>ถัดลงมาในฟังก์ชัน main() เหมือนเดิม ให้เพิ่มโค้ดด้านล่างนี้เพื่อ survive ตัว <strong>In-App Browser</strong> และ รองรับ <strong>External Browser</strong></p><pre>if (!liff.isInClient()) {<br>  if (isMobile()) {<br>    window.location = `line://app/${liff.id}?${params}`;<br>    setTimeout(() =&gt; { liff.closeWindow(); }, &quot;3000&quot;);<br>    return;<br>  }<br>  if (!liff.isLoggedIn()) {<br>    liff.login({ redirectUri: window.location.href });<br>    return;<br>  }<br>}</pre><p>โดยโค้ดด้านบนจะตรวจสอบว่าผู้ใช้งานได้เปิด LIFF app ใน <strong>LIFF Browser</strong> หรือไม่ <strong>หากไม่</strong> จะเจอกับเงื่อนไขสองข้อ</p><ul><li><strong>isMobile()</strong> จะตรวจสอบว่าผู้ใช้งานเปิดหน้าเว็บนี้บนสมาร์ทโฟนหรือไม่ โดย <strong>หากใช่</strong> เราจะผลักผู้ใช้งานกระโดดไปหน้าเว็บนี้บน LIFF Browser เนื่องจากเราจะ <strong>ดึงโปรไฟล์ผู้ใช้งานได้โดยไม่ต้อง login</strong> <em>(กรณีนี้มักเกิดกับผู้ใช้งานที่คลิกเปิดลิงค์จาก ad หรือโพสในแอปต่างๆ ซึ่งจะการเปิดลักษณะนี้ จะเป็นการเปิดแบบ In-App Browser</em>)</li><li><strong>liff.isLoggedIn() </strong>จะตรวจสอบว่าผู้ใช้งานได้ login แล้วหรือยัง <strong>หากยัง</strong> เราก็จะพาเขาไป login ก่อน เพื่อที่เราจะระบุตัวตนคนที่คลิก ad ได้ (<em>กรณีนี้มักเกิดกับผู้ใช้งานที่เปิดเว็บนี้บน PC หรือ MacBook</em>)</li></ul><p>ยัง ยัง ยังไม่หมด ในฟังก์ชัน main() ให้เพิ่มโค้ดด้านล่างนี้ เพื่อตรวจสอบว่าผู้ใช้งานที่เปิดเว็บนี้เขาเป็นเพื่อนกับ LINE OA แล้วหรือยัง</p><pre>try {<br>  const data = await liff.getFriendship();<br>  if (data.friendFlag) {<br>    // หากเขาเป็นเพื่อนกับ LINE OA อยู่แล้ว ระบบจะพาผู้ใช้งานไปเปิด LINE OA ทันที<br>    window.location = LINE_OA_URL;<br>    setTimeout(() =&gt; { liff.closeWindow(); }, &quot;3000&quot;);<br>    return;<br>  }<br>} catch (error) {<br>  // กรณนี้ที่นักพัฒนายังไม่ได้ Linked LINE Official Account ใน ขั้นตอนที่ 3.1 มันจะฟ้อง<br>  alert(error.message);<br>  liff.closeWindow();<br>  return;<br>}</pre><p>และแล้วเราก็มาถึงส่วนสุดท้ายของการพัฒนา LIFF app ในฟังก์ชัน <strong>main()</strong> ละ ให้เพิ่มโค้ดด้านล่างนี้</p><pre>// เชื่อมต่อกับ Callable Function ที่เราได้เตรียมไว้ใน ขั้นตอนที่ 2.3<br>// โดยส่งค่า accessToken ของ LIFF app และ utmSource ซึ่งเป็น query param ที่มากับ URL<br>const myCallable = httpsCallable(functions, &#39;myCallable&#39;);<br>const result = await myCallable({<br>  accessToken: liff.getAccessToken(),<br>  utmSource: params.get(&quot;utm_source&quot;) || &quot;none&quot;<br>});<br><br>// พาผู้ใช้กระโดดต่อไปหน้า LINE OA<br>window.location = LINE_OA_URL;<br>setTimeout(() =&gt; { liff.closeWindow(); }, &quot;3000&quot;);</pre><h4>หมายเหตุ</h4><ul><li>เหตุผลที่ผมเลือกตั้งชื่อตัว query param เป็น <strong>utm_source</strong> เนื่องจาก หากต้องการจะติด <strong>Google Analytics</strong> เพื่อ track การเปิดหน้าเว็บนี้ ก็จะรองรับไปในคราวเดียวกัน</li><li>เหตุผลที่ผมใช้เวลา <strong>setTimeout()</strong> เป็น <strong>3,000</strong> (3 วินาที) เนื่องจากผมทดลองแล้วว่าเป็นช่วงเวลาที่ผู้ใช้จะกระโดดไปยังจุดต่อไป ได้โดยที่ต้นทางจะไม่ <strong>ปิด</strong> ตัวไปก่อน หรือจะ <strong>ไม่เปิดหน้านั้นค้าง </strong>นานจนเกินไป</li></ul><h3>4. สร้าง Webhook API เพื่อรอรับสัญญาณการ Add / Unblock</h3><p>ขั้นตอนนี้เราจะมาพัฒนา Webhook API สำหรับรอรับข้อมูลของผู้ใช้งานที่กด add/unblock ตัว LINE OA มา และสร้างเงื่อนไขเพื่อยืนยันว่าเขามาจากช่องทางที่เราไปโปรโมทหรือไม่</p><p>กลับมาที่โฟลเดอร์ <strong>/functions</strong> กัน โดยให้เปิดไฟล์ <strong>index.js</strong> ขึ้นมา แล้วเพิ่มโค้ดด้านล่างนี้ไปที่จุดล่างสุดของไฟล์ เพื่อที่จะรอรับ <strong>follow</strong> event จาก LINE</p><pre>exports.webhook = onRequest(async (req, res) =&gt; {<br>  for (const event of req.body.events) {<br>    switch (event.type) {<br>      case &quot;follow&quot;: {<br>        // ...<br>      }<br>    }<br>  }<br>  res.send(&quot;ok&quot;);<br>});</pre><p>ถัดไปเราจะเพิ่มโค้ดด้านล่างนี้ภายใน <strong>follow</strong> เพื่อดักเก็บ <strong>userId</strong> ที่มากับสัญญาณ แล้วนำไปใช้เรียก <strong>firebase.getFollower()</strong> เพื่อดึงประวิตการคลิก ad ออกมาจาก Firestore ที่เราได้เก็บไว้ใน <strong>ขั้นตอนที่ 2.3</strong></p><p>แต่ในกรณีที่ผู้ใช้งานกด add/unblock ตัว LINE OA เองโดยไม่ผ่าน ad ก็จะไม่มีข้อมูลใน Firestore เราจึงต้องกำหนดค่า default ไปด้วย</p><pre>const doc = await firebase.getFollower(event.source.userId);<br>let isAddedOrUnblockedFromAd = (doc.exists) ? doc.data().isAddedOrUnblockedFromAd : false;<br>const utmSource = (doc.exists) ? doc.data().utmSource : &quot;none&quot;;<br>const timestamp = (doc.exists) ? doc.data().timestamp : Date.now();</pre><p>ถัดไปให้เราเพิ่มโค้ด เพื่อสร้างเงื่อนไขก่อนที่จะยืนยันว่าผู้ติดตามมาจาก ad หรือไม่</p><pre>// ถ้ายืนยันแล้วว่าผู้ใช้คนนั้นๆ add/unblock มาจากช่องทางไหน เราก็จะไม่ไปอัพเดท database<br>if (isAddedOrUnblockedFromAd) { break; }<br><br>// ถ้าระยะเวลาระหว่าง &quot;การกด ad&quot; กับ &quot;การ add/unblock&quot; ต่างกันมากกว่า 1 นาที<br>// เราจะถือว่า follower คนดังกล่าวไม่ได้มาจาก ad เราก็จะไม่ไปอัพเดท database<br>if (doc.exists) {<br>  const expectedTimeToAdd = 1000 * 60 * 1; // 1 minute<br>  const gapTimeOfAdding = Date.now() - doc.data().timestamp;<br>  if (gapTimeOfAdding &gt; expectedTimeToAdd) { break; }<br>}<br><br>// กรณีที่ follower คนนั้นกด add/unblock มาจาก ad ค่าตรงนี้จะเป็น true<br>isAddedOrUnblockedFromAd = (utmSource !== &quot;none&quot;);</pre><p>ส่วนสุดท้ายในฟังก์ชัน follow ให้เราเพิ่มโค้ดด้านล่างนี้ เพื่อเพิ่มหรืออัพเดท ข้อมูลการ add/unblock ตัว LINE OA ของผู้ใช้แต่ละคน</p><pre>await firebase.upSertFollower(event.source.userId, {<br>  isAddedOrUnblockedFromAd,<br>  utmSource,<br>  timestamp<br>});</pre><h3>5. Deploy และ Demo</h3><p>และแล้วเราก็เดินทางมาถึงขั้นตอนสุดท้ายกันแล้ว โดยในขั้นตอนนี้ เราจะพาทุกคนไป deploy และ demo กัน</p><h3>5.1 Deploy</h3><p><strong>สำหรับ LIFF app</strong> ที่เราพัฒนาไปใน <strong>ข้อ 3</strong> ให้เราเลือกไป deploy ที่ไหนก็ได้ตามใจปรารถนา แต่ถ้านึกไม่ออกว่าที่ไหนดี ผมแนะนำ Firebase Hosting โฮสที่ให้เริ่มต้นใช้ฟรีกับพื้นที่ 10GB</p><p><a href="https://medium.com/firebasethailand/%E0%B8%A3%E0%B8%B9%E0%B9%89%E0%B8%88%E0%B8%B1%E0%B8%81-firebase-hosting-%E0%B8%95%E0%B8%B1%E0%B9%89%E0%B8%87%E0%B9%81%E0%B8%95%E0%B9%88-zero-%E0%B8%88%E0%B8%99%E0%B9%80%E0%B8%9B%E0%B9%87%E0%B8%99-hero-1f999edfe8ea">รู้จัก Firebase Hosting ตั้งแต่ Zero จนเป็น Hero</a></p><p>โดยเมื่อเรา deploy แล้ว ให้เอา URL ที่ได้ไปอัพเดทที่ LIFF detail ใน <a href="https://developers.line.biz/console/">LINE Login Channel</a> ให้เรียบร้อย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FeqlHtw_pn0c35O5TFqbGg.png" /></figure><p><strong>สำหรับ Callable Function และ Webhook API</strong> ที่เราพัฒนาไปใน <strong>ข้อ 2</strong> และ <strong>4</strong> ให้เราเปิด command line ขึ้นมาอีกครั้ง และให้แน่ใจว่าเราอยู่ที่ /<strong>functions</strong> จากนั้นใช้คำสั่งด้านล่างนี้เพื่อ deploy โค้ดของเรา</p><pre>firebase deploy --only functions</pre><p>เมื่อ deploy เสร็จเรียบร้อยเราจะเห็น Webhook URL โผล่อยู่ใน command line เลย ก็ให้เรา copy มา หรือกรณีที่ไม่พบก็ให้เข้าไปที่ <a href="https://console.firebase.google.com/">Firebase console</a> เลือกโปรเจคที่เราสร้างไว้ แล้วเลือกเมนู Functions จะเจอชื่อฟังก์ชันและ Webhook URL ที่นั่น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*K7tEm64DkRr1Vw0GVi2hXA.jpeg" /></figure><h4>5.2 เชื่อมต่อ Webhook API กับ Messaging API Channel</h4><p>เมื่อได้ Webhook URL มาอย่างสมใจปองแล้ว ก็เอา URL นี้ไปอัพเดทที่ <strong>Messaging API Channel</strong> ใน <a href="https://developers.line.biz/console/">LINE Developers Console</a> ตามที่ได้สร้างไว้ใน <strong>ขั้นตอนที่ 1</strong> และให้มั่นใจว่าได้เปิดใช้ <strong>Use webhook</strong> ด้วยแล้ว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*iaU-EivWYnD7Hl_i.png" /></figure><h4>5.3 Demo</h4><p>ขั้นตอนนี้จะทดสอบกับ LINE OA ที่เราเป็นเพื่อนอยู่แล้วไม่ได้ ดังนั้น <strong>ใครที่จะทดสอบกับตัว LINE OA ที่มีอยู่ จะต้อง block มันก่อน </strong>จากนั้นมาดูกันตั้งแต่กด URL ที่ฝังในที่ต่างๆจนไปถึงการ add/unblock ตัว LINE OA ว่าแต่ละแบบจะเป็นเช่นไร</p><h4><strong>5.3.1 เปิด LIFF URL และ Endpoint URL ในแอป LINE</strong></h4><p>ขั้นตอนนี้ผมจะเอา LIFF URL ไปใส่ใน Flex Message ให้เปรียบเสมือนผมไปลงโฆษณาที่แอป LINE และจะเอา Endpoint URL โพสลงไปในห้องแชทเพื่อทดสอบ</p><pre>LIFF URL<br>https://liff.line.me/1653575653-M4RbrV6y?utm_source=line<br><br>Endpoint URL<br>https://mokmoon.com/liff1/friendpath/index.html?utm_source=line</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Uxbu0ePpPFKXnCGDtZNrNA.gif" /></figure><p>ผลปรากฏว่าทั้ง LIFF URL และ Endpoint URL สามารถพาผู้ใช้ไปเปิดหน้า LINE OA ได้ทั้งคู่ และนอกจากนี้ข้อมูลก็ได้บันทึกลงไปใน Firestore แล้วว่าผมคลิกมาจากที่ไหน และค่า isAddedOrUnblockedFromAd จะเปลี่ยนเป็น true ก็ต่อเมื่อผมกด add/unblock ภายในเวลาที่กำหนด ซึ่งจากตัวอย่างก็คือภายใน 1 นาทีนั่นเอง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6_IzEgPJ1IvrMfKAyBqKQQ.jpeg" /></figure><h4><strong>5.3.2 เปิด LIFF URL และ Endpoint URL ในแอป Facebook</strong></h4><p>ขั้นตอนนี้ผมจะเอา LIFF URL และ Endpoint URL ไปโพสลง Facebook แบบแยกโพสกันเพื่อทดสอบ</p><pre>LIFF URL<br>https://liff.line.me/1653575653-M4RbrV6y?utm_source=facebook<br><br>Endpoint URL<br>https://mokmoon.com/liff1/friendpath/index.html?utm_source=facebook</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MhG9HwUVX4ryl5Bv-8JcWQ.gif" /></figure><p>ผลปรากฏว่าทั้ง LIFF URL และ Endpoint URL สามารถพาผู้ใช้จาก Facebook ไปเปิดหน้า LINE OA ได้ทั้งคู่ แต่จุดสังเกตของระบบที่ผมออกแบบไว้ก็คือ <strong>หากเรายังไม่ได้ add friend หรือ block ตัว LINE OA</strong> ไว้ เรา<strong>จะคลิก ad อีกกี่ครั้งกี่ที่</strong>ก็ได้ แต่ระบบจะจำ<strong>ช่วงเวลาและช่องทางล่าสุด</strong>ไว้ให้เสมอ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_MxMS2BO9SU_GANVqWhHeQ.jpeg" /></figure><h4>5.3.3 เปิด LIFF URL จาก QR code</h4><p>ขั้นตอนนี้ผมจะเอา LIFF URL ไปใส่ลงใน QR code โดยแนบ utm_source เป็น LINECafe เพื่อทดสอบ</p><pre>LIFF URL<br>https://liff.line.me/1653575653-M4RbrV6y?utm_source=LINECafe</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CN_EOKRn_wRsFMfHHSgf_w.gif" /></figure><p>ผลปรากฏว่าทั้งการแสกน QR code เพื่อเปิด LIFF URL ก็สามารถพาผู้ใช้ไปเปิดหน้า LINE OA ได้ และเราก็ track ได้ด้วยว่าผู้ใช้งานมาจากร้าน LINE Cafe</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uuwN2yuRSQe8i4up-RXRVA.jpeg" /></figure><h4><strong>5.3.4 เปิด LIFF URL และ Endpoint URL ใน PC</strong></h4><p>ขั้นตอนนี้ผมจะเอา LIFF URL ไปเปิดกับ browser ใน PC ซึ่งมันจะ redirect ไปที่ Endpoint URL ของเราเมื่อเปิด ซึ่งในการทดสอบนี้ผมให้ utm_source เป็น google เพื่อให้เหมือนเราเจอโฆษณา LINE OA ของเราเป็น ad ของ Google นั่นเอง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s0yoBGeXETLg3JTZWjgofw.gif" /></figure><p>ผลปรากฏว่าหากผู้ใช้งานไม่ได้ login กับ LINE ไว้ ตัวหน้าเว็บก็จะพาไป login ก่อนจึงจะพาต่อไปหน้าที่ให้ add friend ผ่าน QR code ของ LINE และข้อมูลใน Firestore ก็แสดงให้เห็นว่าเราเก็บค่า utm_source ได้จริง</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2mPgXOzI7A-29ZwdNY4Y5A.jpeg" /></figure><h3>Bonus วิธีดึงยอด followers ของช่องทางต่างๆไปทำ Report</h3><p>แน่นอนว่า เมื่อเราเก็บค่าต่างๆไว้ใน Firestore แล้ว ก็คงต้อง query ออกมาเพื่อทำรายงานสรุปผลประสิทธิภาพของช่องทางต่างๆที่คุณไปโปรโมท LINE OA กัน ผมจึงเตรียมวิธีการสร้าง API เพื่อดึงจำนวน followers แต่ละช่องทางมาให้แล้วดังนี้</p><p>ในไฟล์ <strong>/functions/index.js</strong> ให้เพิ่มโค้ดด้านล่างนี้ที่จุดล่างสุดของไฟล์</p><pre>exports.count = onRequest(async (req, res) =&gt; {<br>  const utmSource = (req.query.utm_source || &quot;none&quot;);<br>  const snapshot = await firebase.countUtmResource(utmSource);<br>  res.send(`The number of followers who added/unblocked the LINE OA from &lt;b&gt;${utmSource}&lt;/b&gt; is &lt;b&gt;${snapshot.data().count}&lt;/b&gt;`);<br>});</pre><p>โดยโค้ดด้านบนเราจะสร้าง API ที่ชื่อ <strong>count</strong> มาเพื่อรับ query param ชื่อ <strong>utm_source</strong> โดยค่าเริ่มต้นคือ <strong>none</strong> (follower เพิ่มเพื่อนเอง ไม่ได้มาจากช่องทางที่เราไปโปรโมทไว้) จากนั้นจะเอาค่า utmSource ที่ได้ส่งไปกับฟังก์ชัน <strong>firebase.countUtmResource()</strong> ที่เราเตรียมไว้ใน <strong>ขั้นตอนที่ 2.3</strong> เพื่อนับจำนวน followers ที่มาจากช่องทางนั้นๆ และได้ยืนยันแล้วว่ามาจากช่องทางที่เราไปโปรโมทจริงๆ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*enOVCVu80yG0ojQ0K7ieiA.jpeg" /></figure><p>โดยเมื่ออัพเดทโค้ดเรียบร้อยแล้ว ให้เปิด command line ขึ้นมาอีกครั้ง และให้แน่ใจว่าเราอยู่ที่ /<strong>functions</strong> จากนั้นใช้คำสั่งด้านล่างนี้เพื่อ deploy โค้ดของเรา</p><pre>firebase deploy --only functions</pre><p>เมื่อ deploy แล้วก็ไปเอา URL จาก <a href="https://console.firebase.google.com/">Firebase console</a> มาลองเปิดดู ก็จะได้ผลลัพธ์ประมาณนี้ค้าบ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aepMr_3y1ixm4AT-rQWKFQ.png" /></figure><h3>สิ่งที่ระบบ Friend Path ฉบับ DIY มีเพิ่มเติมจากระบบ Friend Path ใน OA Manager</h3><p>ระบบ Friend Path ฉบับ DIY ที่ผมพาทุกคนไปสร้างนั้น จะแก้ pain point บางอย่างที่ ณ วันที่เขียนบทความนี้ ระบบ Friend Path ของ OA Manager ยังไม่มี ได้แก่</p><p>✅ DIY รองรับการกระโดดจาก In-App Browser <strong>เพียงคลิกเดียว</strong></p><p>✅ DIY กดคลิกเปิดจาก <strong>LINE Desktop</strong> ได้</p><p>✅ DIY ไม่ได้รู้เฉพาะช่องทางว่ามาจากไหนเท่าไร แต่รู้ถึง<strong>ตัวตนของคนคนนั้น</strong>ด้วย</p><p>✅ DIY ได้ข้อมูล <strong>userId</strong> ซึ่งเอาไปใช้อะไรต่อได้มากมาย</p><p>✅ จำนวน <strong>Query param</strong> ของ DIY มีได้<strong>ไม่จำกัด</strong></p><p>✅ ชื่อ <strong>Query param</strong> ของ DIY ใช้ร่วมกับ <strong>Google Analytics</strong> ได้</p><p>✅ DIY ปรับแต่ง <strong>OG Meta Tags</strong> ได้เอง ช่วยให้ตอนแชร์ URL ตาม Social Platform สามารถกำหนด <strong>Image</strong> และ <strong>Title</strong> เองได้</p><p>✅ ข้อมูลที่ DIY ให้มีถึง <strong>ระดับเวลา</strong> ทำให้<strong>หาช่วงเวลาทอง</strong>ของการโปรโมทได้</p><p>✅ ข้อมูลที่ DIY จะ<strong>อัพเดท</strong>แบบ <strong>Real-time</strong></p><p>✅ ข้อมูลที่ DIY อยู่ใน <strong>DB ตัวเอง</strong> จึงยืดหยุ่น นำไปสร้าง <strong>API</strong> เพื่อ<strong>เชื่อมต่อกับ Platform อื่นๆ</strong>ได้ง่าย</p><h3>สรุป</h3><p>มาถึงตรงนี้ ถ้าใครที่อ่านมาเรื่อยๆ แล้วยังไม่ได้ลงมือทำ ผมแนะนำให้ clone repo ด้านล่างนี้ไปได้เลย(แต่ฝากกด Star ให้ด้วยจะขอบคุณมากๆครับ)</p><p><a href="https://github.com/jirawatee/Friend-Path-of-LINE-OA/">GitHub - jirawatee/Friend-Path-of-LINE-OA: A demo in how to track users to see which channels they added/unblocked the LINE OA from by integrating LINE Front-end Framework, LINE Messaging API, Cloud Functions for Firebase, and Cloud Firestore.</a></p><p>หวังว่าเนื้อหาการสร้างระบบ Friend Path ฉบับ DIY ในบทความนี้ จะตอบโจทย์นักพัฒนา ผู้ประกอบการ โดยเฉพาะอย่างยิ่งทีม Marketing ให้ทลายข้อจำกัดและปลดล๊อควิธีการได้มาซึ่งข้อมูล ที่จำเป็นสำหรับการวัดผล Marketing Campaign ตามช่องทางต่างๆที่โปรโมทตัว LINE OA ทั้งของตัวเองและของลูกค้าได้อย่างมีประสิทธิภาพมากขึ้นในอนาคตครับ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=213dd994b9a3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/friend-path-of-line-oa-213dd994b9a3">รู้หมดใครเพิ่มเพื่อน LINE OA จากช่องทางไหน ด้วยระบบ Friend Path ที่คุณก็สร้างเองได้</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[รู้จักกับ LIFF CLI ตั้งแต่ Zero จนเป็น Hero]]></title>
            <link>https://medium.com/linedevth/liff-cli-274586c999c5?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/274586c999c5</guid>
            <category><![CDATA[liff]]></category>
            <category><![CDATA[liff-cli]]></category>
            <category><![CDATA[liff-plugin]]></category>
            <category><![CDATA[line-front-end-framework]]></category>
            <category><![CDATA[liff-sdk]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Wed, 25 Sep 2024 02:30:23 GMT</pubDate>
            <atom:updated>2024-09-25T02:30:23.811Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fCVEkN9Okc05VUZkoEOksg.png" /></figure><p>ครั้งหนึ่งนานมาแล้ว ผมเคยเห็น LIFF CLI v0.1.0 ที่ทาง LINE ปล่อยให้นักพัฒนาได้ใช้งาน ผมใคร่สัมผัสจับต้องมัน แต่ถูกเพื่อนห้ามเอาไว้ เนื่องด้วยขณะนั้นบร๊ะลานุภาพของมันยังมิต้องตาต้องใจ แต่บัดนี้ บร๊ะลานุภาพของมันได้เพิ่มพูนถึง v0.2.0 ที่จะนำไปใช้ในงานได้แล้ว ผมจึงจะนำเครื่องมือนี้มารีวิวให้ชมกัน</p><h3>LIFF CLI คืออะไร</h3><p><a href="https://www.npmjs.com/package/@line/liff-cli"><strong>LIFF CLI</strong></a> คือ Command Line Interface ที่ช่วยให้นักพัฒนาสามารถบริหารจัดการ LIFF app ทั้งการ create, update, list และ delete รวมถึงการ run &amp; debug ใน local ได้ง่ายและสะดวกผ่าน Terminal หรือ Command-line tool (ต่อไปขอเรียกว่า Terminal)</p><p>สำหรับนักพัฒนา แค่อ่านมันไม่พอ! ดังนั้นบทความนี้ผมจะพาทุกคนไป learning by doing ทีละขั้นทีละตอน โดยเนื้อหาจะครอบคลุมตามหัวข้อดังนี้</p><ol><li>สร้าง LINE Login Channel</li><li>ติดตั้ง Node.js และ LIFF CLI</li><li>บริหารจัดการ LIFF app ด้วย CLI</li><li>Setup และ Run ตัว LIFF app ใน Local</li></ol><h3>1. สร้าง LINE Login Channel</h3><p>เนื่องจาก LIFF คือบริการที่อยู่ภายใต้ <strong>LINE Login Channel</strong> ดังนั้นจุดเริ่มต้นคือเราต้องมี LINE Login Channel กันซะก่อน โดยถ้าใครที่มีอยู่แล้วให้ข้ามขั้นตอนนี้ไป แต่ถ้ายังไม่มีให้ทำตามข้อ 1 และ 2 ของบทความด้านล่างนี้</p><p><a href="https://medium.com/linedevth/%E0%B9%80%E0%B8%89%E0%B8%A5%E0%B8%A2%E0%B8%A7%E0%B8%B4%E0%B8%98%E0%B8%B5%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%94%E0%B8%B6%E0%B8%87-email-%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B8%9C%E0%B8%B9%E0%B9%89%E0%B9%83%E0%B8%8A%E0%B9%89%E0%B9%83%E0%B8%99-liff-v2-93ce9b944b57">สร้างและพัฒนา LIFF v2 ให้ดึง Email ของผู้ใช้ออกมาได้</a></p><h3>2. ติดตั้ง Node.js และ LIFF CLI</h3><p>การติดตั้ง LIFF CLI นั้นสามารถทำผ่าน npm หรือ yarn ก็ได้ แต่เนื่องด้วย LIFF CLI จะ run อยู่บน Node.js ดังนั้นในบทความนี้ผมจะเลือกติดตั้งผ่าน npm ที่มาพร้อมกับการติดตั้ง Node.js ไปเลย</p><h4>2.1 ติดตั้ง Node.js และ npm</h4><p>ให้เข้าไปดาวน์โหลดและติดตั้ง <a href="https://nodejs.org/">Node.js</a> ให้เรียบร้อย ซึ่งเมื่อติดตั้งเรียบร้อยแล้วสามารถตรวจสอบว่าทั้ง node และ npm พร้อมใช้งานในเครื่องเราโดยเปิด Terminal ขึ้นมาแล้วพิมพ์คำสั่งตามด้านล่าง</p><pre>node --version // แนะนำว่าควรเป็น v20 หรือใหม่กว่า<br>npm --version</pre><p>หากเห็นตัวเลขเวอร์ชันโผล่มาก็เป็นอันว่าผ่าน</p><h4>2.2 ติดตั้ง LIFF CLI</h4><p>ใน Terminal ให้ใช้คำสั่งด้านล่างนี้เพื่อติดตั้ง LIFF CLI</p><pre>npm install -g @line/liff-cli</pre><h3>3. บริหารจัดการ LIFF app ด้วย CLI</h3><p>ปัจจุบันนักพัฒนาสามารถใช้ LIFF CLI ในการ Create, Update, List และ Delete ตัว LIFF app ได้ โดยมีขั้นตอนดังนี้</p><h4>3.1 เพิ่มและตั้งค่า Default กับ Channel</h4><p>จุดเริ่มขบวนของการจัดการ LIFF app ทั้งหลายใน LINE Login Channel นั้น เราจะต้องผูก Channel ที่ต้องการเข้ากับ LIFF CLI โดยให้เปิด Terminal แล้วพิมพ์คำสั่ง <br>liff-cli <strong>channel</strong> <strong>add</strong> ตามด้วย Channel ID ที่ได้จากข้อที่ 1 ลงไป (ตัวอย่าง Channel ID คือ 1234567890<em>)</em></p><pre>liff-cli channel add 1234567890</pre><p>จากนั้นตัว LIFF CLI จะถามหา Channel Secret ก็ให้ copy มาวาง ซึ่งถ้าถูกต้อง ก็จะมีข้อความยืนยันการเพิ่ม Channel ตามตัวอย่างด้านล่างนี้</p><pre>Channel Secret?: ********************************<br># Channel 1234567890 is now added.</pre><h4>3.2 ตั้งค่า Default ให้กับ Channel</h4><p>ขั้นตอนนี้เป็น <strong>optional</strong> เพื่อให้ LIFF CLI จดจำ Channel ID ซึ่งจะช่วยให้นักพัฒนาสามารถเรียกคำสั่งถัดๆไปในการจัดการ LIFF app สำหรับ Channel นั้นได้สั้นลง</p><p>ใน Terminal ให้พิมพ์คำสั่ง liff-cli <strong>channel</strong> <strong>use</strong> ตามด้วย Channel ID ที่เราได้เพิ่มไว้ในข้อก่อนหน้า ซึ่งถ้าเรียบร้อย ก็จะมีข้อความยืนยันตามตัวอย่างด้านล่างนี้</p><pre>liff-cli channel use 1234567890<br># Channel 1234567890 is now selected.</pre><h4>3.3 Create a LIFF app</h4><p>ขั้นตอนนี้เราจะมาลองสร้าง LIFF app ผ่านคำสั่ง liff-cli <strong>app</strong> <strong>create</strong> กับ LIFF CLI กัน โดยคำสั่งในการสร้างจะมีส่วนประกอบดังนี้</p><ul><li>--channel-id Channel ID ของ LINE Login Channel [<strong>optional</strong>]</li><li>--name ชื่อของ LIFF app (ห้ามมีคำว่า LINE) [<strong>Required</strong>]</li><li>--endpoint-url URL ขึ้นต้นด้วย HTTPS [<strong>Required</strong>]</li><li>--view-type ขนาดในการแสดงผล LIFF app ซึ่งมี 3 ขนาดได้แก่ full, tall และ compact [<strong>Required</strong>]</li></ul><p>โดยเมื่อ run คำสั่งสร้าง LIFF app เสร็จ จะมีข้อความยืนยันตามตัวอย่างด้านล่าง</p><pre>liff-cli app create \<br>   --name &quot;LIFF CLI Demo&quot; \<br>   --endpoint-url https://mokmoon.com/ \<br>   --view-type full<br># Successfully created LIFF app: 1234567890-AbcdEfgh</pre><p><strong>หมายเหตุ</strong>: ตัวอย่างคำสั่งด้านบนจะไม่ได้ระบุ --channel-id เนื่องจากได้มีการตั้งค่า default ให้กับ channel มาแล้ว</p><p>และถ้าเข้าไปดูใน <a href="https://developers.line.biz/console/">LINE Login Channel</a> จะพบว่ามี LIFF app ถูกสร้างขึ้นแล้ว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7lpGQoXr7Pub4BEaKyxAtQ.jpeg" /></figure><h4>3.4 Update a LIFF app</h4><p>ขั้นตอนนี้เราจะมาลองอัพเดท LIFF app ผ่านคำสั่ง liff-cli <strong>app</strong> <strong>update</strong> กับ LIFF CLI กัน โดยคำสั่งในการอัพเดทจะมีส่วนประกอบดังนี้</p><ul><li>--liff-id LIFF ID ของ LIFF app ที่ต้องการอัพเดท [<strong>Required</strong>]</li><li>--channel-id Channel ID ของ LINE Login Channel [<strong>optional</strong>]</li><li>--name ชื่อของ LIFF app (ห้ามมีคำว่า LINE) [<strong>optional</strong>]</li><li>--endpoint-url URL ขึ้นต้นด้วย HTTPS [<strong>optional</strong>]</li><li>--view-type ขนาดในการแสดงผล LIFF app ซึ่งมี 3 ขนาดได้แก่ full, tall และ compact [<strong>optional</strong>]</li></ul><p>โดยเมื่อ run คำสั่งอัพเดท LIFF app เสร็จ จะมีข้อความยืนยันตามตัวอย่างด้านล่าง</p><pre>liff-cli app update \<br>   --liff-id 1234567890-Jv5mrQdE \<br>   --view-type tall<br># Successfully updated LIFF app: 1234567890-Jv5mrQdE</pre><p>และถ้าเข้าไปดูใน <a href="https://developers.line.biz/console/">LINE Login Channel</a> จะพบว่า LIFF app ได้ถูกอัพเดทขนาดแล้ว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0d_KekLUWQSgQoQlJTtfpQ.jpeg" /></figure><h4>3.5 List LIFF apps</h4><p>ขั้นตอนนี้เราจะมาลอง list ตัว LIFF app ที่มีอยู่ใน Channel ออกมาให้หมด ผ่านคำสั่ง liff-cli <strong>list</strong> กับ LIFF CLI กัน โดยคำสั่งในการ list จะมีส่วนประกอบดังนี้</p><ul><li>--channel-id Channel ID ของ LINE Login Channel [<strong>optional</strong>]</li></ul><p>โดยเมื่อ run คำสั่ง list ตัว LIFF app เสร็จจะมี LIFF ID และ ชื่อ แสดงตามตัวอย่างด้านล่าง</p><pre>liff-cli app list<br># LIFF apps:<br># 1234567890-Jv5mrQdE: LIFF CLI Demo</pre><h4>3.6 Delete a LIFF app</h4><p>ขั้นตอนนี้เราจะมาลองลบ LIFF app ผ่านคำสั่ง liff-cli <strong>app</strong> <strong>delete</strong> กับ LIFF CLI กัน โดยคำสั่งในการลบจะมีส่วนประกอบดังนี้</p><ul><li>--liff-id LIFF ID ของ LIFF app ที่ต้องการอัพเดท [<strong>Required</strong>]</li><li>--channel-id Channel ID ของ LINE Login Channel [<strong>optional</strong>]</li></ul><p>โดยเมื่อ run คำสั่งลบ LIFF app เสร็จ จะมีข้อความยืนยันตามตัวอย่างด้านล่าง</p><pre>liff-cli app delete \<br>   --liff-id 1234567890-Jv5mrQdE \<br>   --channel-id 1234567890<br># Deleting LIFF app...<br># Successfully deleted LIFF app: 1234567890-Jv5mrQdE</pre><h3>4. Setup และ Run ตัว LIFF app ใน Local</h3><p>ฟีเจอร์ใหม่ใน LIFF CLI ที่มาตั้งแต่ v0.2.0 เป็นต้นไป คือมันสามารถสร้าง local development server ขึ้นมาเพื่อช่วยให้เรา run ตัว LIFF app ใน local ได้</p><h4>4.1 สร้าง Cerificate และ Private Key</h4><p>จุดเริ่มขบวนของการ run ตัว LIFF app ใน local คือเราจะต้องสร้าง valid certificate ขึ้นมาก่อน โดยให้เปิด Terminal ขึ้นมาแล้วติดตั้ง mkcert ผ่านคำสั่ง</p><pre># For macOS (using Homebrew)<br>brew install mkcert<br><br># For Windows (using Chocolatey)<br>choco install mkcert</pre><p>จากนั้นติดตั้ง Certificate Authority(CA) ใน local ด้วยคำสั่ง</p><pre>mkcert -install<br># The local CA is now installed in the system trust store! ⚡️</pre><p>แล้วสร้างไฟล์ certificate(localhost.pem) และไฟล์ private key (localhost-key.pem) ด้วยคำสั่ง</p><pre>mkcert localhost<br><br># Created a new certificate valid for the following names 📜<br># - &quot;localhost&quot;<br><br># The certificate is at &quot;./localhost.pem&quot; and the key at &quot;./localhost-key.pem&quot; ✅<br># It will expire on 24 December 2026 🗓</pre><p>เสร็จแล้วเราจะได้ไฟล์ทั้งสองมาละครับ</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q2EBQM_2KC-j_4hCrwpJeg.png" /></figure><h4>4.2 Run ตัว LIFF app ใน Local</h4><p>ขั้นตอนนี้เราจะสร้าง local proxy server เพื่อมา run ตัว LIFF app ผ่านคำสั่ง serve กับ LIFF CLI กัน โดยคำสั่งในการ run จะมีส่วนประกอบดังนี้</p><ul><li>--liff-id LIFF ID ของ LIFF app ที่ได้ตั้งค่า default channel [<strong>Required</strong>]</li><li>--url URL ของ local development server [<strong>Required</strong>]</li></ul><p>การ run คำสั่ง serve จะต้อง run ใน directory เดียวกับไฟล์ localhost.pem และ localhost-key.pem โดยเมื่อ run คำสั่ง serve แล้วก็จะพบข้อความยืนยันตามตัวอย่างด้านล่าง</p><pre>liff-cli serve \<br>   --liff-id 1234567890-Jv5mrQdE \<br>   --url http://localhost:5000/<br><br># Successfully updated endpoint url for LIFF ID: 1234567890-Jv5mrQdE.<br><br>→  LIFF URL:     https://liff.line.me/1234567890-Jv5mrQdE<br>→  Proxy server: https://localhost:9000/</pre><p>และถ้าเข้าไปดูใน <a href="https://developers.line.biz/console/">LINE Login Channel</a> จะพบว่า LIFF app ได้ถูกอัพเดท Endpoint URL ไปเป็น https://localhost:9000/ แล้ว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FSGR6qR1VOamlDmfjb02Kg.jpeg" /></figure><p><strong>คำเตือน</strong>: ห้ามใช้คำสั่ง serve กับ LIFF ID ที่อยู่ใน prod เป็นอันขาด เพราะตัว LIFF CLI จะเอา URL ของ local proxy server ไปแทนที่ใน Endpoint URL ของ LIFF app ทำให้ผู้ใช้งานทั่วไปจะไม่สามารถใช้งาน LIFF app ตัวนั้นได้</p><p>คราวนี้มาลองเปิด LIFF app ด้วย LIFF URL กัน ซึ่งผลปรากฏว่า…มันจะ redirect ไปเปิด https://localhost:9000/ ได้แล้วทุกคน!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*olUGvi8kR-vcDNEZVAD4tA.png" /></figure><p>ปล. โค้ดในหน้าเว็บตัวอย่างจะมีการรองรับการ Login สำหรับ External Brower เพื่อดึงข้อมูลผู้ใช้มาแสดง ซึ่งมันเวิร์คกับ https://localhost:9000/ ด้วย (Copy ไปลองกันได้ แต่เปลี่ยน LIFF ID กันด้วยนะ)</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html&gt;<br>&lt;head&gt;<br>  &lt;meta charset=&quot;UTF-8&quot;&gt;<br>  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover&quot;&gt;<br>  &lt;title&gt;LIFF CLI Demo&lt;/title&gt;<br>  &lt;style&gt;<br>    button { display: block; width: 100%; padding: 8px 0; margin: 8px auto }<br>    #pictureUrl { display: block; margin: 0 auto; width: 25%; display: -moz-box; display: -webkit-box; display: box; -moz-box-pack: center; -moz-box-align: center; -webkit-box-pack: center; -webkit-box-align: center; box-pack: center; box-align: center; -moz-border-radius: 256px; -webkit-border-radius: 256px; border-radius: 256px;<br>    }<br>  &lt;/style&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>  &lt;img id=&quot;pictureUrl&quot; width=&quot;25%&quot; /&gt;<br>  &lt;p id=&quot;displayName&quot;&gt;&lt;/p&gt;<br>  &lt;p id=&quot;statusMessage&quot;&gt;&lt;/p&gt;<br>  &lt;p id=&quot;language&quot;&gt;&lt;/p&gt;<br>  &lt;script src=&quot;https://static.line-scdn.net/liff/edge/2/sdk.js&quot;&gt;&lt;/script&gt;<br>  &lt;script&gt;<br>    function runApp() {<br>      liff.getProfile().then(profile =&gt; {<br>        document.getElementById(&quot;displayName&quot;).innerHTML = &#39;&lt;b&gt;DisplayName:&lt;/b&gt; &#39; + profile.displayName;<br>        document.getElementById(&quot;statusMessage&quot;).innerHTML = &#39;&lt;b&gt;StatusMessage:&lt;/b&gt; &#39; + profile.statusMessage;<br>        document.getElementById(&quot;pictureUrl&quot;).src = profile.pictureUrl;<br>        document.getElementById(&quot;language&quot;).innerHTML = &#39;&lt;b&gt;Language:&lt;/b&gt; &#39; + liff.getAppLanguage();<br>      }).catch(err =&gt; console.error(err));<br>    }<br>    liff.init({ liffId: &quot;1234567890-Jv5mrQdE&quot; }, () =&gt; {<br>      if (liff.isLoggedIn()) {<br>        runApp()<br>      } else {<br>        liff.login();<br>      }<br>    }, err =&gt; console.error(err.code, error.message));<br>  &lt;/script&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;</pre><h4>4.3 Debug ตัว LIFF app ด้วย LIFF Inspector</h4><p>ต่อยอดจากคำสั่ง serve ในข้อก่อนหน้า สำหรับใครที่ใช้ <a href="https://developers.line.biz/en/docs/liff/liff-plugin/#liff-inspector">LIFF Inspector</a> เพื่อ debug ตัว LIFF app ผ่าน ChromeDevTools เราสามารถเพิ่ม option --inspect เข้าไปกับคำสั่ง serve ตามตัวอย่างด้านล่างนี้</p><pre>liff-cli serve \<br>   --liff-id 1234567890-AbcdEfgh \<br>   --url http://localhost:3000/ \<br>   --inspect<br><br># Successfully updated endpoint url for LIFF ID: 1234567890-AbcdEfgh.<br><br>→  LIFF URL:     https://liff.line.me/1234567890-AbcdEfgh<br>→  Proxy server: https://localhost:9000/?li.origin=wss%3A%2F%2Flocalhost%3A9222<br>Debugger listening on wss://192.168.1.6:9222</pre><p>โดยเมื่อเราเปิด LIFF URL ตัว Terminal ก็จะพ่น DevTools URL ที่ขึ้นต้นด้วย devtools://devtools/ ออกมาตามตัวอย่างด้านล่างนี้</p><pre>connection from client, id: 1234567890-AbcdEfgh<br>DevTools URL: devtools://devtools/bundled/inspector.html?wss=localhost:9222/?hi_id=1234567890-AbcdEfgh</pre><p>ซึ่งเราสามารถนำ DevTools URL ที่ได้ไปเปิดผ่าน Google Chrome เพื่อ debug ตัว LIFF app ได้</p><h3>สรุป</h3><p>นอกจากที่ LIFF CLI จะช่วยให้นักพัฒนาสามารถ Create, Update, List และ Delete ตัว LIFF app ได้แล้ว การมาของคำสั่ง serve ใน LIFF CLI v0.2.0 ที่สามารถ run ตัว LIFF app ใน local ได้ ก็ถือเป็น killer feature ที่เหล่าพัฒนารอคอย เพราะการพัฒนา LIFF app จากนี้ไป จะสะดวกและรวดเร็วขึ้นอย่างมากแน่นอน</p><p>หวังว่าบทความนี้จะทำให้เพื่อนๆนักพัฒนาสามารถนำ LIFF CLI ไปใช้ต่อ ให้ก่อประโยชน์กับงานสืบต่อไป แล้วพบกันใหม่บทความหน้า สวัสดีค้าาาบ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=274586c999c5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/liff-cli-274586c999c5">รู้จักกับ LIFF CLI ตั้งแต่ Zero จนเป็น Hero</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Loading Animation สิ่งเล็กๆที่มาเติมเต็ม UX ให้กับ LINE Chatbot ระดับอัลตร้าสมูท!]]></title>
            <link>https://medium.com/linedevth/loading-animation-8329818a852e?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/8329818a852e</guid>
            <category><![CDATA[chatbots]]></category>
            <category><![CDATA[linemessagingapi]]></category>
            <category><![CDATA[line-bot]]></category>
            <category><![CDATA[loading-animation]]></category>
            <category><![CDATA[line-chatbot]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Wed, 24 Apr 2024 01:35:22 GMT</pubDate>
            <atom:updated>2026-07-23T05:55:41.460Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*A5s0du49zKT-plBeV3qavg.png" /></figure><p>หนึ่งใน Pain point ที่นักพัฒนา LINE มักจะเจอกันคือ การ handle ประสบการณ์ผู้ใช้ระหว่างที่รอ chatbot ตอบกลับ โดยเคสนี้จะเกิดกับการที่ระบบจำเป็นต้องใช้เวลาในการประมวลผลข้อมูลที่มากกว่าปกติ เช่น การนำรูปไปทำ Image Processing, การเอาข้อมูลไป manipulate, การออกรายงาน, หรือการเชื่อมต่อ Gen AI เป็นต้น ซึ่งตัวอย่างหนึ่งที่ผมเองก็เคยต้องรอและลุ้นการตอบกลับของ LINE Chatbot คือการ detect โลโก้จากภาพที่ส่งไปใน <a href="https://www.youtube.com/watch?v=wyfNCVDFGng&amp;t=3386s">Live Coding</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yjhSOTyx8mDoULlOxeOa1A.jpeg" /><figcaption>Live coding จากงาน LINE THAILAND DEVELOPER CONFERENCE 2019</figcaption></figure><p>การต้องรอการตอบสนองที่นานกว่าปกตินี้เอง ทำให้ผู้ใช้งานจำนวนมากรู้สึกว่าระบบขัดข้อง และนำมาซึ่งการส่งข้อความซ้ำๆไปในห้องแชท เพื่อหวังว่าจะกระตุ้นการตอบกลับ หรือที่วัยรุ่นเขาเรียกกันว่าการ “ฟลัด”(flood) และการฟลัดนี้เองที่เป็นหนึ่งในสาเหตุที่ทำให้ UX การใช้งาน LINE Chatbot เพี้ยนไป รวมถึงอาจทำให้ระบบทำงานหนักขึ้นและมีปัญหาตามมาได้</p><p>โดยปัญหานี้ถูกยกขึ้นมาถามใน community เรื่อยมา และ solution ที่เป็นไปได้ก่อนหน้านี้คือการ reply สถานะการทำงานไปก่อน แล้วต่อด้วยการ push ข้อความผลลัพธ์ตามมา แต่วิธีดังกล่าวนี้นักพัฒนาก็จำเป็นจะต้องเสียโควต้าของข้อความไปด้วย</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q3l4oHZyyVJHJJhM7wdTtg.jpeg" /></figure><p>และแล้ว เมษายน ปี 2024 ฝันของนักพัฒนาในบ้านเราก็เป็นความจริง เมื่อทาง LINE ได้รับฟัง feedback เรื่องนี้ และออกฟีเจอร์ <strong>Loading Animation</strong> เพื่อมาปรับปรุง UX การใช้งาน LINE Chatbot ให้ดีขึ้น</p><h3>รู้จักกับ Loading Animation</h3><p>ฟีเจอร์ Loading Animation นี้จะเป็นฟีเจอร์ที่มาในรูปแบบของ API ที่ให้นักพัฒนาสามารถ request เพื่อให้ตัว LINE Chatbot แสดงสถานะการกำลังประมวลผลอยู่ โดยมีหน้าตาเป็นแบบนี้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/540/1*mluNU3AFSYHA00oMapLVQQ.gif" /></figure><p>โดยตัว Loading Animation จะมี เงื่อนไขในการแสดงผล และ เงื่อนไขที่ตัวมันจะหายไปดังนี้</p><h4>เงื่อนไขที่ Loading Animation จะแสดงออกมาได้</h4><ul><li>มันจะสามารถแสดงผลได้ในห้องแชทแบบ <strong>one-on-one</strong> ระหว่าง ผู้ใช้งาน และ LINE Chatbot เท่านั้น</li><li>มันจะแสดงผลได้ใน<strong>ขณะที่ผู้ใช้งานเปิดหน้าแชท</strong>ของ LINE Chatbot เท่านั้น</li></ul><h4>เงื่อนไขที่ Loading Animation จะหายไป</h4><ul><li>มันจะหายไปทันทีเมื่อ LINE Chatbot <strong>ส่งข้อความมาใหม่</strong></li><li>มันจะหายไปทันทีเมื่อผู้ใช้งาน<strong>ออกจากหน้าแชท</strong>นั้นๆ</li><li>มันจะหายไปทันทีเมื่อ<strong>ผ่านระยะเวลาที่กำหนด</strong>ให้มันแสดงผลจาก API ที่ request</li></ul><h4>Loading Animation API</h4><p>คราวนี้เรามาดู spec ของ API กันหน่อยว่ามีรายละเอียดอะไรบ้าง</p><pre>Endpoint URL: https://api.line.me/v2/bot/chat/loading/start<br><br>Method: post<br><br>Headers:<br>  content-type: application/json<br>  Authorization: Bearer {channel access token}<br><br>Body:<br>  chatId: userId ของผู้ใช้งาน (groupId กับ roomId หมดสิทธิ์นาจา)<br>  loadingSeconds: [Optional] ระยะเวลาในการแสดงผลเป็นวินาที (5-60 เพิ่มลดได้ทีละ 5) โดยค่า default คือ 20</pre><blockquote><strong>Rate limit</strong> ของ Loading Animation API คือ <strong>100 requests/วินาที</strong></blockquote><p>ซึ่งผมก็ได้ลองเอามาประยุกต์ใช้งานจริงกับ LINE Chatbot ของ <a href="https://line.me/R/ti/p/@linedevth">LINE Developer TH</a> แล้ว โดยเอาง่ายๆคือเมื่อไรที่มี webhook events เข้ามาผมก็ request ตัว API ทันทีเลยตามตัวอย่าง JavaScript ด้านล่างนี้</p><pre>exports.webhook = onRequest(async (req, res) =&gt; {<br>  const events = req.body.events;<br>  for (const event of events) {<br>    const userId = event.source.userId;<br>    await loading(userId);<br>    switch (event.type) {<br>      case &quot;message&quot;:<br>        // ...<br>        break;<br>      // ...<br>    }<br>  }<br>  res.end();<br>});<br><br>function loading(userId) {<br>  return axios({<br>    method: &quot;post&quot;,<br>    url: &quot;https://api.line.me/v2/bot/chat/loading/start&quot;,<br>    headers: {<br>      &quot;Content-Type&quot;: &quot;application/json&quot;,<br>      Authorization: `Bearer ${process.env.CHANNEL_ACCESS_TOKEN}`<br>    },<br>    data: { chatId: userId }<br>  });<br>}</pre><p>ส่วนใครที่อยากลองสัมผัสประสบการณ์​ก่อนว่า Loading Animation มันเป็นอย่างไร ให้เพิ่มเพื่อนกับ <a href="https://line.me/R/ti/p/@linedevth">@linedevth</a> ใน LINE แล้วพูดคุยกับ LINE Chatbot ตัวนี้ดูได้ครับ</p><h3>หมายเหตุ</h3><ul><li><strong>Loading Animation API</strong> ใช้งานได้<strong>ฟรีไม่มีค่าใช้จ่าย</strong></li><li>Loading Animation สามารถแสดงผลได้ใน LINE สำหรับ <strong>iOS</strong> และ <strong>Android</strong> ตั้งแต่ <strong>v13.16.0 </strong>และใน LINE สำหรับ Desktop ตั้งแต่ <strong>v9.1.2</strong> เป็นต้นไป</li><li>กรณีที่เวอร์ชันของ LINE สำหรับ <strong>Android</strong> ตรงตามเงื่อนไข และ HTTP status code จากการ request ตัว API ได้ <strong>202</strong> กลับมาแต่ Loading Animation ไม่แสดงผล <strong>ให้ kill app แล้วลองใหม่</strong></li><li>หากมีการ <strong>request ตัว API ซ้ำ</strong>ในขณะที่ Loading Animation เดิมยังไม่หายไป ตัว Loading Animation จะแสดงผลต่อไป โดย<strong>จะยึดเวลาการหายไปของ request ล่าสุด</strong>ไปแทน</li></ul><h3>สรุป</h3><p>สำหรับฟีเจอร์ Loading Animation ต้องขอชื่นชมทาง LINE เลยว่างานนี้ เขาคิดมาครบดีจริงๆ ทั้งเรื่องการหายไปของ Loading Animation เมื่อมีข้อความใหม่มาจาก LINE Chatbot หรือเรื่องการหายไปแบบที่นักพัฒนาสามารถกำหนดเวลาเองได้ แถมใช้งานฟรีอีก ซึ่งผมมั่นใจเลยว่าฟีเจอร์นี้จะช่วยยกระดับ UX การใช้งาน LINE Chatbot ของคุณให้สมูธขึ้นได้อย่างแน่นอน</p><p>ท้ายนี้ ใครที่สนใจอยากลองใช้ Loading Animation ผมแนะนำให้ลองเอาไปประยุกต์เข้ากับ LINE Chatbot x Gen AI ดู ซึ่งมันจะเหมาะมากๆเมื่อนำไปคั่นจังหวะที่ Gen AI กำลังสร้าง content โดยทุกคนสามารถเริ่มต้นง่ายๆกับคอร์สเรียนออนไลน์ <a href="https://school.borntodev.com/course/building-line-chatbot-with-chatgpt-and-gemini"><strong>Building LINE Chatbot with ChatGPT and Gemini</strong></a> นี้ ซึ่งก็ฟรีอีก ^^ แล้วพบกันใหม่ในบทความหน้า สวัสดีค้าบ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8329818a852e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/loading-animation-8329818a852e">Loading Animation สิ่งเล็กๆที่มาเติมเต็ม UX ให้กับ LINE Chatbot ระดับอัลตร้าสมูท!</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[สร้าง LINE Chatbot x Gemini(Gen AI ที่เก่งที่สุดของ Google) ด้วย Cloud Functions for Firebase]]></title>
            <link>https://medium.com/linedevth/linebot-x-gemini-9cd2dc22d9c9?source=rss-8376e328ad37------2</link>
            <guid isPermaLink="false">https://medium.com/p/9cd2dc22d9c9</guid>
            <category><![CDATA[chatbots]]></category>
            <category><![CDATA[gemini]]></category>
            <category><![CDATA[cloud-functions-2nd-gen]]></category>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[gemini-api]]></category>
            <dc:creator><![CDATA[Jirawatee]]></dc:creator>
            <pubDate>Mon, 15 Jan 2024 02:22:43 GMT</pubDate>
            <atom:updated>2025-10-27T16:23:46.082Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UAUwEPmU0MmmcBc1SP3Vzg.png" /></figure><p>ปี 2023 ถือได้ว่าเป็นการเริ่มต้นยุค AI อย่างแท้จริง จากการที่ OpenAI เปิดตัว <strong>ChatGPT</strong> แล้วตามติดด้วย <strong>Bard</strong> จาก Google ที่ใครๆก็หันมาให้ความสนใจในการใช้ประโยชน์เพื่อความสะดวกสบาย เช่น ค้นหาข้อมูล, แต่งบทความ, คำนวณ, แปลความหมาย หรือแม้กระทั่ง เขียนโค้ด</p><p>ปีที่ผ่านมา ผมก็มีการหยิบเอา PaLM2 ซึ่งเป็น Large Language Models(LLMs) ที่อยู่เบื้องหลัง Bard มาเชื่อมต่อกับ LINE Chatbot แล้วบอกเล่าวิธีการผ่านบทความมาแล้ว ซึ่งหลายคนที่ได้อ่านคงจะทราบแล้วว่า PaLM2 มันรองรับเฉพาะภาษาอังกฤษ</p><p>แต่แล้วช่วงปลายปีที่ผ่านมา Google ก็ได้สร้างเซอร์ไพรส์ด้วยการเปิดตัว <strong>Gemini</strong> ซึ่งเป็น AI Model ที่เก่งที่สุดที่ทีม Google DeepMind เคยมีออกมา โดยมาพร้อมกับการรองรับ 38 ภาษารวมถึง<strong>ภาษาไทย</strong> และที่ล้ำไปกว่าเดิมคือมันสามารถเข้าใจ input ที่หลากหลายมากขึ้นแบบ <strong>Multimodal</strong> ทั้ง Text, Code, Audio, Image, และ Video</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RF-XxkumLyTaeKkqBFstmA.png" /></figure><p>เอาหละ บทความนี้ ผมจะพาทุกคนไปดูความสามารถของ Gemini ผ่านการสร้าง LINE Chatbot ที่เชื่อมต่อกับ <strong>Gemini API</strong> ทั้งแบบ <strong>Text-Only</strong> และแบบ <strong>Multimodal</strong> ด้วย <strong>Cloud Functions for Firebase 2nd Gen </strong>ตามสไตล์ Zero to Hero ง่ายๆเพียง 5 ขั้นตอนกัน</p><h3>1. สร้าง Provider และ Channel</h3><p>ใครที่ยังไม่เคยสร้าง Provider และ Channel สำหรับ LINE Chatbot มาก่อน ก็ให้อ่านบทความปฐมบทด้านล่างนี้ แต่ใครที่คุ้นเคยอยู่แล้วให้ข้ามขั้นตอนนี้ไปได้เลย</p><p><a href="https://medium.com/linedevth/%E0%B8%9B%E0%B8%90%E0%B8%A1%E0%B8%9A%E0%B8%97%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B8%AA%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%87-line-bot-b2cb90643901">ปฐมบทการสร้าง LINE Bot</a></p><h3>2. ติดตั้ง Cloud Functions for Firebase</h3><p>ใครที่ยังไม่เคยพัฒนาโปรแกรมด้วย Cloud Functions for Firebase มาก่อน ก็ให้อ่านบทความด้านล่างนี้(โดยเฉพาะข้อ 2) แต่สำหรับคนที่คุ้นเคยแล้วให้ข้ามโลด</p><p><a href="https://medium.com/firebasethailand/cloud-functions-for-firebase-2nd-gen-cdda33bbd7dd">รู้จัก Cloud Functions for Firebase 2nd Gen ตั้งแต่ Zero จนเป็น Hero</a></p><h3>3. สร้าง Webhook สำหรับ LINE Chatbot</h3><h4>3.1 สร้าง Module สำหรับการเรียกใช้ Messaging API</h4><p>ในตัวอย่างนี้ผมจะใช้ <strong>axios</strong> มาเป็นตัวช่วยในการสร้าง request ซึ่งจะต้องทำการติดตั้ง dependency ตัวนี้ก่อน โดยให้เปิด command line แล้วเข้าไปที่ <strong>/functions</strong> จากนั้นใช้คำสั่ง</p><pre>npm install axios --save</pre><p>ถัดไปให้สร้างไฟล์ <strong>/functions/.env</strong> แล้วให้ไป copy ค่า <strong>Channel Access Token</strong> ของ Messaging API Channel ใน <a href="https://developers.line.biz/console/">LINE Developers console</a> จากข้อ 1 มาระบุ</p><pre>CHANNEL_ACCESS_TOKEN=YOUR-CHANNEL-ACCESS-TOKEN</pre><p>และเพื่อโค้ดที่สั้น เป็นระเบียบ เราจะแยกฟังก์ชันสำหรับการเรียกใช้ Messaging API ออกมาเป็น module โดยภายในจะมีฟังก์ชัน</p><ul><li><strong>getImageBinary()</strong> สำหรับการดึง binary ของภาพที่ผู้ใช้ส่งผ่าน LINE</li><li><strong>reply()</strong> สำหรับการตอบข้อความกลับหาผู้ใช้</li></ul><p>ขั้นตอนนี้ให้สร้างไฟล์ <strong>/functions/utils/line.js</strong> แล้วให้ copy โค้ดด้านล่างนี้ไปวาง</p><pre>const axios = require(&quot;axios&quot;);<br><br>const LINE_HEADER = {<br>  &quot;Content-Type&quot;: &quot;application/json&quot;,<br>  Authorization: `Bearer ${process.env.CHANNEL_ACCESS_TOKEN}`<br>};<br><br>const getImageBinary = async (messageId) =&gt; {<br>  const originalImage = await axios({<br>    method: &quot;get&quot;,<br>    headers: LINE_HEADER,<br>    url: `https://api-data.line.me/v2/bot/message/${messageId}/content`,<br>    responseType: &quot;arraybuffer&quot;<br>  })<br>  return originalImage.data;<br>}<br><br>const reply = (token, payload) =&gt; {<br>  return axios({<br>    method: &quot;post&quot;,<br>    url: &quot;https://api.line.me/v2/bot/message/reply&quot;,<br>    headers: LINE_HEADER,<br>    data: { replyToken: token, messages: payload }<br>  });<br>};<br><br>module.exports = { getImageBinary, reply };</pre><h4>3.2 สร้างและเชื่อมต่อ Webhook กับ Messaging API Channel</h4><p>ให้เปิดไฟล์ <strong>/functions/index.js</strong> แล้วให้ copy โค้ดด้านล่างนี้ไปแทนที่โค้ดเดิม เพื่อเตรียมรับ <a href="https://medium.com/linedevth/4cb7da653274">Webhook event</a> ประเภท text และ image จาก LINE</p><pre>const {onRequest} = require(&quot;firebase-functions/v2/https&quot;);<br>const line = require(&quot;./utils/line&quot;);<br><br>exports.webhook = onRequest(async (req, res) =&gt; {<br>  if (req.method === &quot;POST&quot;) {<br>    const events = req.body.events;<br>    for (const event of events) {<br>      switch (event.type) {<br>        case &quot;message&quot;:<br>          if (event.message.type === &quot;text&quot;) {<br>          }<br>          if (event.message.type === &quot;image&quot;) {<br>          }<br>          break;<br>      }<br>    }<br>  }<br>  res.send(req.method);<br>});</pre><p>แล้วก็ให้เปิด command line ขึ้นมาอีกครั้ง และให้แน่ใจว่าเราอยู่ที่ <strong>functions/</strong> จากนั้นใช้คำสั่งด้านล่างนี้เพื่อ deploy ตัว webhook ของเราขึ้น production</p><pre>firebase deploy --only functions</pre><p>เมื่อ deploy เสร็จเรียบร้อยเราจะเห็น Webhook URL โผล่อยู่ใน command line เลย ก็ให้เรา copy มา หรือกรณีที่ไม่พบก็ให้เข้าไปที่ <a href="https://console.firebase.google.com/">Firebase console</a> เลือกโปรเจคที่เราสร้างไว้ แล้วเลือกเมนู Build &gt; Functions จะเจอชื่อฟังก์ชันและ Webhook URL ที่นั่น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6LMkgqaYrShwJQH4piCCBg.jpeg" /></figure><p>เมื่อได้ Webhook URL มาอย่างสมใจปองแล้ว ก็เอา URL นี้ไปอัพเดทที่ <strong>Messaging API Channel</strong> ใน <a href="https://developers.line.me/console/">LINE Developers console</a> ตามที่ได้สร้างไว้ในข้อที่ 1 และให้มั่นใจว่าได้เปิดใช้ <strong>Use webhook</strong> ด้วยแล้ว</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PJSdVlaTrlKWsoF_e9DReg.png" /></figure><h3>4. สร้าง API Key ใน Google AI Studio</h3><p>ขั้นตอนนี้เราจะพาทุกคนไปสร้าง API key สำหรับการใช้งาน Gemini API กัน โดยเริ่มจากคลิกลิงค์ <a href="https://aistudio.google.com/app/api-keys">https://aistudio.google.com/app/api-keys</a> แล้วให้กดปุ่ม Create API key ที่มุมขวา จากนั้นจะพบทางเลือกที่ให้เรากดสร้าง API key ด้วยการสร้างโปรเจคใหม่ หรือ จะเลือกโปรเจคที่มีอยู่แล้วใน Google Cloud ก็ได้(ถ้ามี) ซึ่งเมื่อสร้างเสร็จ เราก็จะได้ API key มา</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hLRSUdsfaGE8Tio4TBdrfA.png" /></figure><p>จากนั้นให้เราเพิ่มตัวแปร <strong>API_KEY</strong> ในไฟล์ <strong>/functions/.env</strong> แล้วให้เอา API key ระบุลงไปแบบนี้</p><pre>CHANNEL_ACCESS_TOKEN=YOUR-CHANNEL-ACCESS-TOKEN<br>API_KEY=YOUR-API-KEY</pre><h3>5. เชื่อมต่อ LINE Chatbot เข้ากับ Gemini API</h3><h4>5.1 สร้าง Module สำหรับการเรียกใช้ Gemini API</h4><p>สำหรับ module ที่เราจะสร้างจะแบ่งการทำงานออกเป็น 3 ฟังก์ชันดังนี้</p><ul><li><strong>textOnly()</strong> สร้างเนื้อหาจาก input ที่เป็น text เหมาะกับการโต้ตอบรายครั้ง และไม่จำเป็นต้องมีความต่อเนื่อง แนวๆถามมาตอบไป</li><li><strong>multimodal()</strong> สร้างเนื้อหาจาก input ที่เป็น text และ image เหมาะกับการโต้ตอบแบบรายครั้ง ที่ต้องการให้ output สื่อถึงความสัมพันธ์กับภาพ โดยภาพจะรองรับทั้ง PNG, JPEG, WebP, HEIC, HEIF และยังสามารถแนบภาพได้สูงสุด 16 ภาพ ใน 1 request (Google แนะนำว่าแนบภาพเดียวจะได้ผลลัพธ์ที่ตรงกว่า)</li><li><strong>chat()</strong> สร้างเนื้อหาจาก input ที่เป็น text เหมาะกับการโต้ตอบที่ต่อเนื่องกัน(multi-turn conversation) โดยเราสามารถเก็บประวัติการสนทนาเพื่อให้ AI เรียนรู้ในการสร้างเนื้อหาที่ต่อเนื่องในการตอบครั้งถัดไปได้ ซึ่งประวัติการสนทนาจะอยู่ในรูปแบบของ array ที่จะเก็บการโต้ตอบของ user และ model ตามตัวอย่างด้านล่างนี้</li></ul><pre>history: [<br>  {<br>    role: &quot;user&quot;,<br>    parts: [{ text: &quot;สวัสดีค่ะ&quot; }],<br>  },<br>  {<br>    role: &quot;model&quot;,<br>    parts: [{ text: &quot;สวัสดีครับ ผมชื่อตี๋, ผู้เชี่ยวชาญเกี่ยวกับ LINE API ที่ช่วยตอบคำถามให้กับคุณครับ&quot; }],<br>  }<br>]</pre><p>เมื่อได้ทราบถึง concept ของทั้ง 3 ฟังก์ชันแล้ว ถัดไปเราจะมาลงมือกัน เริ่มจากติดตั้ง <strong>GoogleGenerativeAI</strong> ซึ่งเป็น dependency สำหรับใช้งาน Gemini API โดยให้เปิด command line แล้วเข้าไปที่ <strong>/functions</strong> จากนั้นใช้คำสั่ง</p><pre>npm install @google/genai --save</pre><p>และเพื่อโค้ดไม่ยาวจนเกินไป เราจะแยกฟังก์ชันทั้ง 3 ออกมาเป็น module โดยให้สร้างไฟล์ <strong>/functions/utils/gemini.js</strong> แล้วให้ copy โค้ดด้านล่างนี้ไปวาง</p><pre>const { GoogleGenAI } = require(&quot;@google/genai&quot;);<br>const ai = new GoogleGenAI({ apiKey: `${process.env.API_KEY}` });<br><br>const textOnly = async (prompt) =&gt; {<br>  const response = await ai.models.generateContent({<br>    model: &quot;gemini-2.5-flash&quot;,<br>    contents: prompt,<br>  });<br>  return response.text;<br>};<br><br>const multimodal = async (imageBinary) =&gt; {<br>  const contents = [<br>    {<br>      inlineData: {<br>        data: Buffer.from(imageBinary, &quot;binary&quot;).toString(&quot;base64&quot;),<br>        mimeType: &quot;image/png&quot;<br>      }<br>    },<br>    { text: &quot;ช่วยบรรยายภาพนี้ให้หน่อย&quot; }<br>  ];<br><br>  const response = await ai.models.generateContent({<br>    model: &quot;gemini-2.5-flash&quot;,<br>    contents: contents<br>  });<br><br>  return response.text;<br>};<br><br>const chat = async (prompt) =&gt; {<br>  const chat = ai.chats.create({<br>    model: &quot;gemini-2.5-flash&quot;,<br>    history: [<br>      {<br>        role: &quot;user&quot;,<br>        parts: [{ text: &quot;สวัสดีจ้า&quot; }],<br>      },<br>      {<br>        role: &quot;model&quot;,<br>        parts: [{ text: &quot;สวัสดีครับ ผมชื่อตี๋ ผมเป็นผู้เชี่ยวชาญเกี่ยวกับ LINE API ที่ช่วยตอบคำถามและแบ่งปันความรู้ให้กับชุมขนนักพัฒนา&quot; }],<br>      },<br>      {<br>        role: &quot;user&quot;,<br>        parts: [{ text: &quot;ปัจจุบันมี LINE API อะไรบ้างที่ใช้งานได้ในประเทศไทย&quot; }],<br>      },<br>      {<br>        role: &quot;model&quot;,<br>        parts: [{ text: &quot;ปัจจุบันมีทั้ง Messaging API, LIFF, LINE Login, LINE Beacon, LINE Pay, และ LINE MINI App ที่สามารถใช้งานในไทยได้ครับ&quot; }],<br>      },<br>    ]<br>  });<br><br>  const response = await chat.sendMessage({<br>    message: prompt,<br>  });<br>  return response.text;<br>};<br><br>module.exports = { textOnly, multimodal, chat };</pre><p>เสร็จแล้วก็ให้เราไปอัพเดทโค้ดในไฟล์ <strong>/functions/index.js</strong> เพื่อดึงเอา module ของ gemini มาใช้งาน</p><pre>const {onRequest} = require(&quot;firebase-functions/v2/https&quot;);<br>const line = require(&quot;./utils/line&quot;);<br>const gemini = require(&quot;./utils/gemini&quot;);</pre><h4>5.2 การใช้งาน textOnly()</h4><p>ในไฟล์ <strong>/functions/index.js </strong>ให้เพิ่มโค้ดภายในส่วนที่ตรวจสอบเงื่อนไขว่าเป็นข้อความ text ตามนี้</p><pre>if (event.message.type === &quot;text&quot;) {<br>  const msg = await gemini.textOnly(event.message.text);<br>  await line.reply(event.replyToken, [{ type: &quot;text&quot;, text: msg }]);<br>  break;<br>}</pre><p>จากนั้นก็ deploy ผ่าน command line ด้วยคำสั่ง</p><pre>firebase deploy --only functions</pre><p>เสร็จแล้วก็มาดูผลทดสอบกัน…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EriuKvRRqjz9rImMCDerBA.png" /></figure><h4>5.3 การใช้งาน multimodal()</h4><p>ในไฟล์ <strong>/functions/index.js </strong>ให้เพิ่มโค้ดภายในส่วนที่ตรวจสอบเงื่อนไขว่าเป็นข้อความ image ตามนี้</p><pre>if (event.message.type === &quot;image&quot;) {<br>  const imageBinary = await line.getImageBinary(event.message.id);<br>  const msg = await gemini.multimodal(imageBinary);<br>  await line.reply(event.replyToken, [{ type: &quot;text&quot;, text: msg }]);<br>  break;<br>}</pre><p>จากนั้นก็ deploy ผ่าน command line ด้วยคำสั่ง</p><pre>firebase deploy --only functions</pre><p>การทดสอบนี้ ผมจะแบ่ง prompt ออกเป็น 3 แบบ โดยเรียงลำดับตามผลลัพธ์จากซ้ายไปขวา ซึ่งเนื้อหาที่ Gemini ส่งออกมามันน่าทึ่งมากๆ (AI มันเห็นสิ่งเดียวกับที่เราเห็นเลย)</p><ul><li>ช่วยบรรยายภาพนี้ให้หน่อย</li><li>ช่วยแต่งนิทานจากภาพนี้ให้หน่อย</li><li>ช่วยหาคำตอบของภาพนี้ให้หน่อย</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1ClN2CKO-Q0mjeyebJ65LA.png" /></figure><h4>5.4 การใช้งาน chat()</h4><p>ในไฟล์ <strong>/functions/index.js </strong>ให้เพิ่มโค้ดภายในส่วนที่ตรวจสอบเงื่อนไขว่าเป็นข้อความ text ตามนี้</p><pre>if (event.message.type === &quot;text&quot;) {<br>  const msg = await gemini.chat(event.message.text);<br>  await line.reply(event.replyToken, [{ type: &quot;text&quot;, text: msg }]);<br>  break;<br>}</pre><p>จากนั้นก็ deploy ผ่าน command line ด้วยคำสั่ง</p><pre>firebase deploy --only functions</pre><p>ผลทดสอบคือมันสามารถสวมบทเป็นเรา ตาม context ที่เราเตรียมไว้ และตอบคำถามเกี่ยวกับ LINE API ได้ดีเลย ซึ่งในอนาคต ถ้าใครจะต่อยอดก็เก็บบทสนทนาและเพิ่มเข้าไปใน history: [] ก่อนจะส่งให้มันสร้างเนื้อหาถัดๆไป ตัว AI มันก็จะเข้าใจบริบททั้งหมดที่คุยกันมาก่อนหน้าได้ แล้วตอบเนื้อหาที่แม่นยำมากยิ่งขึ้น</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7XfdWaCtZvAMGETGcTYs-w.png" /></figure><h3>การตั้งค่าความปลอดภัย</h3><p>เบื้องหลังเนื้อหาที่ Gemini สร้างขึ้นมาแต่ละครั้งนั้น จะมีการกรองเนื้อหาที่ไม่เหมาะสมอยู่ด้วย โดยจะแบ่งออกเป็น 4 หมวดหมู่</p><ul><li>Harassment: เนื้อหาเกี่ยวกับการล่วงละเมิด</li><li>Hate speech: เนื้อหาที่แสดงถึงความเกลียดชัง</li><li>Sexually explicit: เนื้อหาที่ไม่เหมาะสมทางเพศ</li><li>Dangerous: เนื้อหาที่ส่งเสริมการกระทำที่เป็นอันตราย</li></ul><p>ซึ่งค่าเริ่มต้นของการกรองเนื้อหาทั้ง 4 หมวดหมูจะเป็น BLOCK_MEDIUM_AND_ABOVE หรือจะกรองเอาเนื้อหาที่เสี่ยงจะเข้าข่ายระดับกลางถึงสูงออก โดยระดับของการกรองที่เราสามารถปรับได้ก็จะมีด้วยกัน 4 ระดับ</p><ul><li>BLOCK_NONE: ไม่กรองเนื้อหาใดๆ</li><li>BLOCK_ONLY_HIGH: กรองเนื้อหาที่เสี่ยงสูงออกเท่านั้น</li><li>BLOCK_MEDIUM_AND_ABOVE: กรองเนื้อหาที่เสี่ยงปานกลางถึงสูงออก</li><li>BLOCK_LOW_AND_ABOVE: กรองเนื้อหาที่เสี่ยงต่ำถึงสูงออก</li></ul><p>ซึ่งจากที่ได้ทดลองใช้งาน multimodal() ไปเรื่อยๆผมพบว่ามีภาพนึงมันถูก block ซึ่งก็งงว่ามันสุ่มเสี่ยงตรงไหน 55</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a-BSumAIuTJwmu6Wz9mQIg.png" /></figure><p>ผมจึงลองตั้งค่าความปลอดภัยให้ Gemini โดยสร้าง array ของ <strong>safetySettings</strong> ในทั้ง 4 หมวดหมู แล้วตั้งค่าให้กรองเนื้อหาที่เสี่ยงสูงออกเท่านั้น</p><pre>const safetySettings = [<br>  {<br>    category: &quot;HARM_CATEGORY_HARASSMENT&quot;,<br>    threshold: &quot;BLOCK_ONLY_HIGH&quot;<br>  },<br>  {<br>    category: &quot;HARM_CATEGORY_HATE_SPEECH&quot;,<br>    threshold: &quot;BLOCK_ONLY_HIGH&quot;<br>  },<br>  {<br>    category: &quot;HARM_CATEGORY_SEXUALLY_EXPLICIT&quot;,<br>    threshold: &quot;BLOCK_ONLY_HIGH&quot;<br>  },<br>  {<br>    category: &quot;HARM_CATEGORY_DANGEROUS_CONTENT&quot;,<br>    threshold: &quot;BLOCK_ONLY_HIGH&quot;<br>  }<br>];</pre><p>และเพิ่ม <strong>safetySettings</strong> เข้าไปตอน <strong>generateContent()</strong></p><pre>const response = await ai.models.generateContent({<br>  model: &quot;gemini-2.5-flash&quot;,<br>  contents: contents,<br>  config: { safetySettings: safetySettings }<br>});</pre><p>ทดสอบกับ prompt “บอกหน่อยว่าซ้ายหรือขวาคือสุนัข” แล้วผลลัพธ์ก็ออกมาจนได้</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7-zc2uKU9hKL322XKz-vwQ.png" /></figure><h3>เกร็ดน่ารู้เกี่ยวกับ Gemini</h3><ul><li>ข้อมูลที่เอามาใช้ train ตัวโมเดล Gemini <strong>ล่าสุดคือต้นปี 2023</strong></li><li><strong>Rate limit</strong> การใช้เรียกใช้งาน Gemini API คือ <strong>60 requests/minute</strong></li><li>วิธีขยาย Rate limit ทำได้ 2 วิธี โดยวิธีแรกคือขอไปทาง Google <a href="https://docs.google.com/forms/d/e/1FAIpQLSdiP6BWJyNNN65lnwnlOr-5Kv0MOFp0jLQyqi_ixVCfddqWBw/viewform">ผ่านฟอร์ม</a>(ไม่มีค่าใช้จ่ายแต่จะขึ้นอยู่กับการพิจารณา) หรืออีกวิธีคืออัพเกรดไปใช้ <a href="https://ai.google.dev/docs/migrate_to_cloud">Vertex AI</a></li></ul><h3>สรุป</h3><p>การเชื่อมต่อ LINE Chatbot กับ Gemini API ด้วย Cloud Functions for Firebase 2nd Gen สำหรับผมนั้นก็เรียกว่าง่ายมากๆ เพราะโค้ดกระชับและตรงไปตรงมา โดย source code ทั้งหมดผมเอาขึ้น GitHub ไว้ให้แล้ว ใครไม่อยากเขียนเองก็ clone มาเล่นกันได้เลย</p><p><a href="https://github.com/jirawatee/LINE-Chatbot-x-Gemini-API">GitHub - jirawatee/LINE-Chatbot-x-Gemini-API: A repo of building Gen AI Chatbot with Gemini by using LINE Messaging API and Cloud Functions for Firebase 2nd Gen</a></p><p>Gemini-1.5-Flash-8b ที่เราได้ใช้กันในบทความนี้ มันเก่งจริงๆ นี่ขนาดเป็นโมเดลที่ Google บอกว่าสำหรับใช้งานทั่วไปนะ ปี 2024 นี้จะมีโมเดล <strong>Gemini Ultra</strong> ที่เก่งกว่าสำหรับงานที่ซับซ้อนกว่า แถมมีการขิงผลการทดสอบที่ชนะ GPT-4 แทบทุกแง่มุม ซึ่งคาดว่าน่าจะเปิดตัวในงาน Google I/O ปีนี้ จะโหดขนาดไหนรอติดตามชมกัน</p><p>สุดท้ายนี้หวังว่าตัวอย่างการสร้าง LINE Chatbot x Gemini API ในบทความนี้ ทั้งแบบ Text-Only และ Multimodal จะทำให้ผู้อ่านเห็นภาพและเห็นโอกาสในการนำไปประยุกต์ใช้กับ Use case ของตัวเอง ซึ่งผมจะรอชมผลงานจากทุกคน แล้วพบกันใหม่บทความหน้าครับ</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9cd2dc22d9c9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/linedevth/linebot-x-gemini-9cd2dc22d9c9">สร้าง LINE Chatbot x Gemini(Gen AI ที่เก่งที่สุดของ Google) ด้วย Cloud Functions for Firebase</a> was originally published in <a href="https://medium.com/linedevth">LINE Developers Thailand</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>