วิธีสร้าง Discord Bot: ภาพรวมและบทช่วยสอน
เผยแพร่แล้ว: 2022-03-11Discord เป็นแพลตฟอร์มการส่งข้อความแบบเรียลไทม์ที่เรียกเก็บเงินเป็น "การแชทด้วยเสียงและข้อความแบบ all-in-one สำหรับนักเล่นเกม" เนื่องจากอินเทอร์เฟซที่ลื่นไหล การใช้งานง่าย และคุณสมบัติมากมาย Discord จึงมีการเติบโตอย่างรวดเร็วและกำลังเป็นที่นิยมมากขึ้นเรื่อยๆ แม้กระทั่งในหมู่ผู้ที่มีความสนใจในวิดีโอเกมเพียงเล็กน้อย ระหว่างเดือนพฤษภาคม 2560 ถึงพฤษภาคม 2561 ฐานผู้ใช้มีเพิ่มขึ้นจาก 45 ล้านคนเป็นมากกว่า 130 ล้านคน โดยมีผู้ใช้มากกว่า Slack รายวันมากกว่าสองเท่า
หนึ่งในคุณสมบัติที่น่าสนใจที่สุดของ Discord จากมุมมองของนักพัฒนา Chatbot คือการสนับสนุนที่แข็งแกร่งสำหรับบอทที่ตั้งโปรแกรมได้ ซึ่งช่วยผสานรวม Discord เข้ากับโลกภายนอกและมอบประสบการณ์ที่น่าดึงดูดใจแก่ผู้ใช้ บอทมีอยู่ทั่วไปใน Discord และให้บริการที่หลากหลาย รวมถึงความช่วยเหลือในการกลั่นกรอง เกม เพลง การค้นหาทางอินเทอร์เน็ต การประมวลผลการชำระเงิน และอื่นๆ
ในบทช่วยสอนบอท Discord นี้ เราจะเริ่มต้นด้วยการพูดถึงอินเทอร์เฟซผู้ใช้ Discord และ REST และ WebSocket API สำหรับบอทก่อนที่จะไปยังบทช่วยสอนที่เราจะเขียนบอท Discord แบบง่ายใน JavaScript สุดท้ายนี้ เราจะรับฟังความคิดเห็นจากนักพัฒนาซอฟต์แวร์ของบอทยอดนิยมของ Discord และประสบการณ์ของเขาในการพัฒนาและบำรุงรักษาโครงสร้างพื้นฐานและโค้ดเบสที่สำคัญของเขา
ส่วนต่อประสานผู้ใช้ Discord
ก่อนที่เราจะพูดถึงรายละเอียดทางเทคนิค สิ่งสำคัญคือต้องเข้าใจวิธีที่ผู้ใช้โต้ตอบกับ Discord และวิธีที่ Discord นำเสนอต่อผู้ใช้ วิธีการนำเสนอตัวเองต่อบอทนั้นมีแนวความคิดที่คล้ายคลึงกัน (แต่แน่นอนว่าไม่ใช่ภาพ) อันที่จริง แอปพลิเคชั่น Discord อย่างเป็นทางการนั้นสร้างขึ้นบน API เดียวกันกับที่บอทใช้ ในทางเทคนิค เป็นไปได้ในทางเทคนิคที่จะเรียกใช้บอทภายในบัญชีผู้ใช้ทั่วไปโดยมีการดัดแปลงเพียงเล็กน้อย แต่สิ่งนี้เป็นสิ่งต้องห้ามตามข้อกำหนดในการให้บริการของ Discord บอทจะต้องทำงานในบัญชีบอท
ต่อไปนี้คือลักษณะเบราว์เซอร์เวอร์ชัน 1 ของแอปพลิเคชัน Discord ที่ทำงานอยู่ใน Chrome
1 Discord UI สำหรับแอปพลิเคชันเดสก์ท็อปแทบจะเหมือนกับเว็บแอปพลิเคชันที่บรรจุด้วยอิเล็กตรอน แอปพลิเคชัน iOS สร้างขึ้นด้วย React Native แอปพลิเคชัน Android เป็นโค้ด Java ดั้งเดิมของ Android
มาทำลายมันกันเถอะ
1. รายชื่อเซิร์ฟเวอร์
ทางด้านซ้ายสุดคือรายชื่อเซิร์ฟเวอร์ที่ฉันเป็นสมาชิก หากคุณคุ้นเคยกับ Slack เซิร์ฟเวอร์จะมีลักษณะคล้ายกับพื้นที่ทำงาน Slack และเป็นตัวแทนของกลุ่มผู้ใช้ที่สามารถโต้ตอบซึ่งกันและกันภายในหนึ่งช่องทางหรือมากกว่าในเซิร์ฟเวอร์ เซิร์ฟเวอร์ได้รับการจัดการโดยผู้สร้างและ/หรือพนักงานคนใดก็ตามที่พวกเขาเลือกและเลือกที่จะมอบหมายความรับผิดชอบให้ ผู้สร้างและ/หรือเจ้าหน้าที่กำหนดกฎเกณฑ์ โครงสร้างของช่องสัญญาณในเซิร์ฟเวอร์ และจัดการผู้ใช้
ในกรณีของฉัน เซิร์ฟเวอร์ Discord API อยู่ที่ด้านบนสุดของรายการเซิร์ฟเวอร์ของฉัน เป็นสถานที่ที่ดีในการรับความช่วยเหลือและพูดคุยกับนักพัฒนาคนอื่นๆ ด้านล่างเป็นเซิร์ฟเวอร์ที่ฉันสร้างชื่อ Test เราจะทำการทดสอบบอทที่เราสร้างขึ้นในภายหลัง ด้านล่างเป็นปุ่มสร้างเซิร์ฟเวอร์ใหม่ ทุกคนสามารถสร้างเซิร์ฟเวอร์ได้ด้วยการคลิกเพียงไม่กี่ครั้ง
โปรดทราบว่าแม้คำที่ใช้ในอินเทอร์เฟซผู้ใช้ของ Discord คือ Server คำที่ใช้ในเอกสารสำหรับนักพัฒนาและ API คือ Guild เมื่อเราพูดถึงหัวข้อทางเทคนิคแล้ว เราจะเปลี่ยนไปพูดถึง กิลด์ คำสองคำนี้ใช้แทนกันได้
2. รายการช่อง
ทางด้านขวาของรายการเซิร์ฟเวอร์คือรายการช่องสำหรับเซิร์ฟเวอร์ที่ฉันกำลังดูอยู่ (ในกรณีนี้คือเซิร์ฟเวอร์ Discord API) ช่องสามารถแบ่งออกเป็นหมวดหมู่ได้ตามต้องการ ในเซิร์ฟเวอร์ Discord API หมวดหมู่รวมถึง INFORMATION, GENERAL และ LIBS ดังที่แสดง แต่ละช่องทำหน้าที่เป็นห้องสนทนาที่ผู้ใช้สามารถพูดคุยเกี่ยวกับหัวข้อต่างๆ ของช่องได้ ช่องที่เรากำลังดูอยู่ (ข้อมูล) มีพื้นหลังสีอ่อนกว่า ช่องที่มีข้อความใหม่ตั้งแต่เราดูครั้งล่าสุดจะมีข้อความสีขาว
3. ช่องดู
นี่คือมุมมองช่องที่เราสามารถดูว่าผู้ใช้กำลังพูดถึงอะไรในช่องที่เรากำลังดูอยู่ เราเห็นข้อความหนึ่งที่นี่ มองเห็นได้เพียงบางส่วนเท่านั้น เป็นรายการลิงก์เพื่อสนับสนุนเซิร์ฟเวอร์สำหรับไลบรารีบอท Discord แต่ละแห่ง ผู้ดูแลระบบเซิร์ฟเวอร์ได้กำหนดค่าช่องนี้เพื่อให้ผู้ใช้ทั่วไปเช่นฉันไม่สามารถส่งข้อความในช่องนี้ได้ ผู้ดูแลระบบใช้ช่องนี้เป็นกระดานข่าวเพื่อโพสต์ข้อมูลสำคัญบางอย่างที่สามารถมองเห็นได้ง่ายและจะไม่ถูกแชทด้วยเสียงกลบ
4. รายชื่อผู้ใช้
ทางด้านขวาคือรายชื่อผู้ใช้ที่ออนไลน์อยู่ในเซิร์ฟเวอร์นี้ ผู้ใช้ถูกจัดเป็นหมวดหมู่ต่างๆ และชื่อของพวกเขามีสีต่างกัน ซึ่งเป็นผลมาจาก บทบาท ที่ตนมี บทบาทอธิบายถึงหมวดหมู่ (ถ้ามี) ที่ผู้ใช้ควรปรากฏภายใต้ ชื่อสีที่ควรจะเป็น และสิทธิ์ที่พวกเขาได้รับในเซิร์ฟเวอร์ ผู้ใช้สามารถมีบทบาทได้มากกว่าหนึ่งบทบาท (และบ่อยครั้งมาก) และมีบางลำดับความสำคัญทางคณิตศาสตร์ที่กำหนดว่าจะเกิดอะไรขึ้นในกรณีนั้น อย่างน้อย ผู้ใช้ทุกคนมีบทบาท @ทุกคน บทบาทอื่น ๆ ถูกสร้างขึ้นและกำหนดโดยเจ้าหน้าที่เซิร์ฟเวอร์
5. การป้อนข้อความ
นี่คือการป้อนข้อความที่ฉันสามารถพิมพ์และส่งข้อความได้ หากฉันได้รับอนุญาต เนื่องจากฉันไม่ได้รับอนุญาตให้ส่งข้อความในช่องนี้ ฉันจึงไม่สามารถพิมพ์ที่นี่ได้
6. ผู้ใช้
นี่คือผู้ใช้ปัจจุบัน ฉันตั้งค่าชื่อผู้ใช้เป็น "ฉัน" เพื่อช่วยไม่ให้สับสน และเพราะฉันเลือกชื่อได้แย่มาก ด้านล่างชื่อผู้ใช้ของฉันคือตัวเลข (#9484) ซึ่งเป็นตัวแบ่งแยกของฉัน อาจมีผู้ใช้หลายคนที่ชื่อ “ฉัน” แต่ฉันเป็นเพียง “ฉัน#9484” นอกจากนี้ยังเป็นไปได้สำหรับฉันที่จะตั้งชื่อเล่นให้ตัวเองตามแต่ละเซิร์ฟเวอร์ เพื่อให้รู้จักฉันโดยใช้ชื่อที่แตกต่างกันในเซิร์ฟเวอร์ที่ต่างกัน
นี่เป็นส่วนพื้นฐานของอินเทอร์เฟซผู้ใช้ Discord แต่ยังมีอีกมากเช่นกัน เริ่มต้นใช้ Discord ได้ง่ายๆ โดยไม่ต้องสร้างบัญชี ดังนั้นโปรดสละเวลาสักครู่เพื่อสำรวจ คุณสามารถเข้าสู่ Discord ได้โดยไปที่หน้าแรกของ Discord คลิก "เปิด Discord ในเบราว์เซอร์" เลือกชื่อผู้ใช้และอาจเล่น "คลิกรูปภาพรถบัส" รอบใหม่หรือสองครั้ง
ความไม่ลงรอยกัน API
Discord API ประกอบด้วยสองส่วนแยกกัน: WebSocket และ REST API โดยทั่วไปแล้ว WebSocket API จะใช้เพื่อรับเหตุการณ์จาก Discord แบบเรียลไทม์ ในขณะที่ REST API ใช้เพื่อดำเนินการต่างๆ ภายใน Discord
WebSocket API
WebSocket API ใช้เพื่อรับเหตุการณ์จาก Discord รวมถึงการสร้างข้อความ การลบข้อความ เหตุการณ์การเตะ/แบนของผู้ใช้ การอัปเดตการอนุญาตผู้ใช้ และอื่นๆ อีกมากมาย การสื่อสารจากบอทไปยัง WebSocket API มีข้อจำกัดมากกว่า บอทใช้ WebSocket API เพื่อขอการเชื่อมต่อ ระบุตัวเอง ฮาร์ตบีต จัดการการเชื่อมต่อด้วยเสียง และทำสิ่งพื้นฐานเพิ่มเติมสองสามอย่าง คุณสามารถอ่านรายละเอียดเพิ่มเติมได้ที่เอกสารเกตเวย์ของ Discord (การเชื่อมต่อเดียวไปยัง WebSocket API เรียกว่าเกตเวย์) สำหรับการดำเนินการอื่นๆ จะใช้ REST API
เหตุการณ์จาก WebSocket API มีเพย์โหลดรวมถึงข้อมูลที่ขึ้นอยู่กับประเภทของเหตุการณ์ ตัวอย่างเช่น เหตุการณ์ สร้างข้อความ ทั้งหมดจะมาพร้อมกับวัตถุผู้ใช้ที่เป็นตัวแทนของผู้เขียนข้อความ อย่างไรก็ตาม วัตถุผู้ใช้เพียงอย่างเดียวไม่มีข้อมูลทั้งหมดที่จำเป็นเกี่ยวกับผู้ใช้ ตัวอย่างเช่น ไม่มีข้อมูลเกี่ยวกับการอนุญาตของผู้ใช้ หากคุณต้องการข้อมูลเพิ่มเติม คุณสามารถค้นหา REST API ได้ แต่สำหรับเหตุผลที่อธิบายเพิ่มเติมในหัวข้อถัดไป โดยทั่วไปแล้วคุณควรเข้าถึงแคชที่คุณควรสร้างจากเพย์โหลดที่ได้รับจากเหตุการณ์ก่อนหน้าแทน มีเหตุการณ์หลายอย่างที่ส่งเพย์โหลดที่เกี่ยวข้องกับการอนุญาตของผู้ใช้ ซึ่งรวมถึงแต่ไม่จำกัดเพียงการ สร้าง กิลด์ การอัปเดตบทบาท ของกิลด์ และ การอัปเดตช่อง
บอทสามารถแสดงได้สูงสุด 2,500 กิลด์ต่อการเชื่อมต่อ WebSocket เพื่อให้บอทปรากฏในกิลด์มากขึ้น บอทต้องใช้การแบ่งส่วนข้อมูลและเปิดการเชื่อมต่อ WebSocket แยกกันหลายรายการไปยัง Discord หากบอทของคุณทำงานภายในกระบวนการเดียวบนโหนดเดียว นี่เป็นเพียงความซับซ้อนที่เพิ่มขึ้นสำหรับคุณซึ่งอาจดูเหมือนไม่จำเป็น แต่ถ้าบอทของคุณได้รับความนิยมอย่างมากและจำเป็นต้องกระจายแบ็คเอนด์ข้ามโหนดที่แยกจากกัน การสนับสนุนการแบ่งส่วนย่อยของ Discord จะทำให้สิ่งนี้ง่ายกว่าที่เคยเป็นมา
ส่วนที่เหลือ API
Discord REST API ถูกใช้โดยบอทเพื่อดำเนินการส่วนใหญ่ เช่น การส่งข้อความ การเตะ/แบนผู้ใช้ และการอัปเดตการอนุญาตของผู้ใช้ (คล้ายกับเหตุการณ์ที่ได้รับจาก WebSocket API) REST API ยังสามารถใช้เพื่อสืบค้นข้อมูล อย่างไรก็ตาม บอทส่วนใหญ่อาศัยเหตุการณ์จาก WebSocket API แทน และแคชข้อมูลที่ได้รับจากเหตุการณ์ WebSocket
มีเหตุผลหลายประการนี้. การสอบถาม REST API เพื่อรับข้อมูลผู้ใช้ทุกครั้งที่ได้รับเหตุการณ์ Message Create จะไม่ถูกปรับขนาดเนื่องจากการจำกัดอัตราของ REST API ในกรณีส่วนใหญ่ยังซ้ำซ้อน เนื่องจาก WebSocket API ส่งข้อมูลที่จำเป็น และคุณควรมีข้อมูลดังกล่าวในแคชของคุณ
อย่างไรก็ตาม มีข้อยกเว้นบางประการ และบางครั้งคุณอาจต้องการข้อมูลที่ไม่มีอยู่ในแคชของคุณ เมื่อบอทเชื่อมต่อกับเกตเวย์ WebSocket ในขั้นต้น กิจกรรม Ready และหนึ่งกิจกรรม สร้าง กิลด์ต่อกิลด์ที่มีบอทอยู่ในชาร์ดนั้นในขั้นต้นจะถูกส่งไปยังบอทเพื่อให้สามารถเติมแคชด้วยสถานะปัจจุบันได้ กิจกรรม สร้างกิ ลด์สำหรับกิลด์ที่มีประชากรหนาแน่นจะรวมเฉพาะข้อมูลเกี่ยวกับผู้ใช้ออนไลน์เท่านั้น หากบอทของคุณต้องการรับข้อมูลเกี่ยวกับผู้ใช้ออฟไลน์ ข้อมูลที่เกี่ยวข้องอาจไม่ปรากฏในแคชของคุณ ในกรณีนี้ คุณควรส่งคำขอไปยัง REST API หรือหากคุณพบว่าตัวเองต้องการรับข้อมูลเกี่ยวกับผู้ใช้ออฟไลน์อยู่บ่อยครั้ง คุณสามารถเลือกส่ง Opcode ขอ สมาชิกกิลด์ไปยัง WebSocket API เพื่อขอสมาชิกกิลด์แบบออฟไลน์ได้
ข้อยกเว้นอีกประการหนึ่งคือ ถ้าแอปพลิเคชันของคุณไม่ได้เชื่อมต่อกับ WebSocket API เลย ตัวอย่างเช่น หากบอทของคุณมีเว็บแดชบอร์ดที่ผู้ใช้สามารถเข้าสู่ระบบและเปลี่ยนการตั้งค่าของบอทในเซิร์ฟเวอร์ของตนได้ แดชบอร์ดของเว็บสามารถทำงานในกระบวนการแยกต่างหากโดยไม่ต้องเชื่อมต่อกับ WebSocket API และไม่มีแคชของข้อมูลจาก Discord อาจจำเป็นต้องส่งคำขอ REST API เพียงไม่กี่ครั้งในบางครั้งเท่านั้น ในสถานการณ์เช่นนี้ คุณควรพึ่งพา REST API เพื่อรับข้อมูลที่คุณต้องการ
API Wrappers
แม้ว่าจะเป็นความคิดที่ดีเสมอที่จะมีความเข้าใจในทุกระดับของเทคโนโลยีของคุณ แต่การใช้ Discord WebSocket และ REST API โดยตรงนั้นใช้เวลานาน เกิดข้อผิดพลาดได้ง่าย โดยทั่วไปไม่จำเป็น และเป็นอันตราย
Discord จัดทำรายชื่อห้องสมุดที่ได้รับการตรวจสอบอย่างเป็นทางการและเตือนว่า:
การใช้การใช้งานแบบกำหนดเองหรือไลบรารีที่ไม่เป็นไปตามข้อกำหนดซึ่งใช้ API ในทางที่ผิดหรือทำให้เกิดการจำกัดอัตราที่มากเกินไปอาจส่งผลให้มีการแบนถาวร
ห้องสมุดที่ตรวจสอบอย่างเป็นทางการโดย Discord นั้นโดยทั่วไปแล้วจะมีการพัฒนา มีเอกสารประกอบอย่างดี และมีความครอบคลุมของ Discord API อย่างเต็มรูปแบบ นักพัฒนาบอทส่วนใหญ่จะไม่มีเหตุผลที่ดีในการพัฒนาการใช้งานแบบกำหนดเอง ยกเว้นเพราะความอยากรู้หรือความกล้าหาญ!
ในขณะนี้ ไลบรารีที่ได้รับการตรวจสอบอย่างเป็นทางการนั้นรวมถึงการใช้งาน Crystal, C#, D, Go, Java, JavaScript, Lua, Nim, PHP, Python, Ruby, Rust และ Swift อาจมีห้องสมุดที่แตกต่างกันสองแห่งขึ้นไปสำหรับภาษาที่คุณเลือก การเลือกใช้สิ่งใดอาจเป็นการตัดสินใจที่ยาก นอกเหนือจากการตรวจสอบเอกสารที่เกี่ยวข้องแล้ว คุณอาจต้องการเข้าร่วมเซิร์ฟเวอร์ Discord API ที่ไม่เป็นทางการและทำความเข้าใจว่าชุมชนประเภทใดอยู่เบื้องหลังแต่ละไลบรารี
วิธีการสร้าง Discord Bot
มาลงมือทำธุรกิจกันเถอะ เรากำลังจะสร้างบอท Discord ที่แฮงเอาท์ในเซิร์ฟเวอร์ของเราและฟังเว็บฮุคจาก Ko-fi Ko-fi เป็นบริการที่ให้คุณรับเงินบริจาคเข้าบัญชี PayPal ของคุณได้อย่างง่ายดาย การตั้งค่าเว็บฮุคที่นั่นนั้นง่ายมาก เมื่อเทียบกับ PayPal ที่คุณจำเป็นต้องมีบัญชีธุรกิจ ดังนั้นจึงเหมาะอย่างยิ่งสำหรับการสาธิตหรือดำเนินการบริจาคในขนาดเล็ก
เมื่อผู้ใช้บริจาคเงิน $10 ขึ้นไป บอทจะมอบหมายบทบาท Premium Member
ให้กับพวกเขา ซึ่งจะเปลี่ยนสีชื่อของพวกเขาและย้ายไปยังด้านบนสุดของรายชื่อผู้ใช้ออนไลน์ สำหรับโครงการนี้ เราจะใช้ Node.js และไลบรารี Discord API ชื่อ Eris (ลิงก์เอกสารประกอบ: https://abal.moe/Eris/) Eris ไม่ใช่ไลบรารี่ JavaScript เดียว คุณสามารถเลือก discord.js แทนได้ รหัสที่เราจะเขียนจะคล้ายกันมากไม่ว่าจะด้วยวิธีใด
นอกเหนือจากนั้น Patreon ซึ่งเป็นผู้ประมวลผลการบริจาคอีกรายให้บอท Discord อย่างเป็นทางการและสนับสนุนการกำหนดค่าบทบาท Discord เป็นผลประโยชน์ของผู้บริจาค เรากำลังจะใช้สิ่งที่คล้ายกัน แต่แน่นอนว่าเป็นพื้นฐานมากกว่า
รหัสสำหรับทุกขั้นตอนของบทช่วยสอนมีอยู่ใน GitHub (https://github.com/mistval/premium_bot) บางขั้นตอนที่แสดงในโพสต์นี้จะไม่ใส่โค้ดที่ไม่เปลี่ยนแปลงเพื่อความกระชับ ดังนั้นให้ทำตามลิงก์ที่ให้มาที่ GitHub หากคุณคิดว่าคุณอาจพลาดบางอย่างไป
การสร้างบัญชีบอท
ก่อนที่เราจะสามารถเริ่มเขียนโค้ดได้ เราจำเป็นต้องมีบัญชีบอท ก่อนที่เราจะสามารถสร้างบัญชีบอทได้ เราจำเป็นต้องมีบัญชีผู้ใช้ก่อน หากต้องการสร้างบัญชีผู้ใช้ ให้ทำตามคำแนะนำที่นี่
จากนั้น ในการสร้างบัญชีบอท เรา:
1) สร้างแอปพลิเคชันในพอร์ทัลนักพัฒนา
2) กรอกรายละเอียดพื้นฐานเกี่ยวกับแอปพลิเคชัน (โปรดทราบ CLIENT ID ที่แสดงไว้ที่นี่—เราจะต้องใช้ในภายหลัง)
3) เพิ่มผู้ใช้บอทที่เชื่อมต่อกับแอปพลิเคชัน
4) ปิดสวิตช์ PUBLIC BOT และสังเกตโทเค็นของบอทที่แสดง (เราจะต้องใช้สิ่งนี้ในภายหลังด้วย) หากคุณเคยทำให้บอทโทเค็นของคุณรั่วไหล เช่น โดยการเผยแพร่ในรูปภาพในโพสต์ Toptal Blog คุณจำเป็นต้องสร้างมันขึ้นมาใหม่ทันที ใครก็ตามที่ครอบครองโทเค็นบอทของคุณสามารถควบคุมบัญชีบอทของคุณ และก่อให้เกิดปัญหาร้ายแรงและถาวรสำหรับคุณและผู้ใช้ของคุณ
5) เพิ่มบอทในกิลด์ทดสอบของคุณ ในการเพิ่มบอทในกิลด์ ให้แทนที่ ID ไคลเอนต์ของมัน (ที่แสดงไว้ก่อนหน้านี้) ลงใน URI ต่อไปนี้และไปที่มันในเบราว์เซอร์
https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX
หลังจากคลิกอนุญาต บอทก็อยู่ในกิลด์ทดสอบของฉันแล้ว และฉันสามารถเห็นมันในรายชื่อผู้ใช้ ออฟไลน์อยู่ แต่เราจะแก้ไขในไม่ช้านี้
การสร้างโครงการ
สมมติว่าคุณติดตั้ง Node.js แล้ว สร้างโครงการและติดตั้ง Eris (ไลบรารีบอทที่เราจะใช้), Express (เฟรมเวิร์กแอปพลิเคชันเว็บที่เราจะใช้เพื่อสร้าง webhook listener) และ body-parser (สำหรับการแยกวิเคราะห์เนื้อหา webhook ).
mkdir premium_bot cd premium_bot npm init npm install eris express body-parser
รับบอทออนไลน์และตอบสนอง
เริ่มต้นด้วยขั้นตอนของทารก ขั้นแรกเราจะให้บอทออนไลน์และตอบกลับเรา เราสามารถทำได้ในโค้ด 10-20 บรรทัด ภายในไฟล์ bot.js ใหม่ เราต้องสร้างอินสแตนซ์ Eris Client ส่งโทเค็นบอทของเรา (ได้มาเมื่อเราสร้างแอปพลิเคชันบอทด้านบน) สมัครรับข้อมูลบางเหตุการณ์ในอินสแตนซ์ไคลเอ็นต์ และบอกให้เชื่อมต่อกับ Discord . เพื่อจุดประสงค์ในการสาธิต เราจะฮาร์ดโค้ดโทเค็นบอทของเราลงในไฟล์ bot.js แต่การสร้างไฟล์กำหนดค่าแยกต่างหากและการยกเว้นจากการควบคุมแหล่งที่มาเป็นแนวทางปฏิบัติที่ดี
(ลิงก์รหัส GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step1.js)
const eris = require('eris'); // Create a Client instance with our bot token. const bot = new eris.Client('my_token'); // When the bot is connected and ready, log to console. bot.on('ready', () => { console.log('Connected and ready.'); }); // Every time a message is sent anywhere the bot is present, // this event will fire and we will check if the bot was mentioned. // If it was, the bot will attempt to respond with "Present". bot.on('messageCreate', async (msg) => { const botWasMentioned = msg.mentions.find( mentionedUser => mentionedUser.id === bot.user.id, ); if (botWasMentioned) { try { await msg.channel.createMessage('Present'); } catch (err) { // There are various reasons why sending a message may fail. // The API might time out or choke and return a 5xx status, // or the bot may not have permission to send the // message (403 status). console.warn('Failed to respond to mention.'); console.warn(err); } } }); bot.on('error', err => { console.warn(err); }); bot.connect();
ถ้าทุกอย่างเป็นไปด้วยดี เมื่อคุณรันโค้ดนี้ด้วยโทเค็นบอทของคุณเอง Connected and ready.
จะถูกพิมพ์ไปที่คอนโซล และคุณจะเห็นบอทของคุณออนไลน์ในเซิร์ฟเวอร์ทดสอบของคุณ คุณสามารถพูดถึง 2 บอทของคุณได้โดยคลิกขวาและเลือก "พูดถึง" หรือโดยการพิมพ์ชื่อที่นำหน้าด้วย @ บอทควรตอบกลับโดยพูดว่า "นำเสนอ"
2 การพูดถึงเป็นวิธีการเรียกความสนใจจากผู้ใช้รายอื่นแม้ว่าพวกเขาจะไม่ได้อยู่ด้วยก็ตาม เมื่อกล่าวถึงผู้ใช้ทั่วไป จะได้รับการแจ้งเตือนทางเดสก์ท็อป การแจ้งเตือนแบบพุชบนมือถือ และ/หรือไอคอนสีแดงเล็กๆ ที่ปรากฏเหนือไอคอนของ Discord ในซิสเต็มเทรย์ ลักษณะที่ผู้ใช้ได้รับแจ้งขึ้นอยู่กับการตั้งค่าและสถานะออนไลน์ของผู้ใช้ ในทางกลับกัน บอทจะไม่ได้รับการแจ้งเตือนพิเศษใดๆ เมื่อมีการกล่าวถึง พวกเขาได้รับเหตุการณ์สร้างข้อความปกติเช่นเดียวกับข้อความอื่น ๆ และสามารถตรวจสอบการกล่าวถึงที่แนบมากับกิจกรรมเพื่อดูว่ามีการกล่าวถึงหรือไม่
บันทึกคำสั่งจ่ายเงิน
ตอนนี้เรารู้แล้วว่าเราสามารถรับบอทออนไลน์ได้แล้ว มากำจัดตัวจัดการเหตุการณ์ สร้างข้อความ ปัจจุบันของเราและสร้างใหม่ที่ช่วยให้เราแจ้งบอทว่าเราได้รับเงินจากผู้ใช้แล้ว
เพื่อแจ้งบอทของการชำระเงิน เราจะออกคำสั่งที่มีลักษณะดังนี้:
pb!addpayment @user_mention payment_amount
ตัวอย่างเช่น pb!addpayment @Me 10.00
เพื่อบันทึกการชำระเงินจำนวน $10.00 ที่ทำโดยฉัน
พีบี! ส่วนหนึ่งเรียกว่าคำนำหน้าคำสั่ง เป็นวิธีที่ดีในการเลือกคำนำหน้าซึ่งคำสั่งทั้งหมดสำหรับบอทของคุณจะต้องเริ่มต้นด้วย สิ่งนี้จะสร้างการวัดเนมสเปซสำหรับบอทและช่วยหลีกเลี่ยงการชนกับบอทอื่น บอทส่วนใหญ่มีคำสั่งช่วยเหลือ แต่ลองจินตนาการถึงความยุ่งเหยิงถ้าคุณมีบอทสิบตัวในกิลด์ของคุณและพวกมันทั้งหมดก็ตอบรับ ความช่วยเหลือ ! ใช้ pb! เนื่องจากคำนำหน้าไม่ใช่วิธีแก้ปัญหา เนื่องจากอาจมีบ็อตอื่นๆ ที่ใช้คำนำหน้าเดียวกันด้วย บอทยอดนิยมส่วนใหญ่อนุญาตให้กำหนดค่าคำนำหน้าตามแต่ละกิลด์เพื่อช่วยป้องกันการชนกัน อีกทางเลือกหนึ่งคือใช้การกล่าวถึงของบอทเป็นคำนำหน้า แม้ว่าสิ่งนี้จะทำให้การออกคำสั่งมีรายละเอียดมากขึ้น
(ลิงก์รหัส GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step2.js)
const eris = require('eris'); const PREFIX = 'pb!'; const bot = new eris.Client('my_token'); const commandHandlerForCommandName = {}; commandHandlerForCommandName['addpayment'] = (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }; bot.on('messageCreate', async (msg) => { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts of the command and the command name const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the appropriate handler for the command, if there is one. const commandHandler = commandHandlerForCommandName[commandName]; if (!commandHandler) { return; } // Separate the command arguments from the command prefix and command name. const args = parts.slice(1); try { // Execute the command. await commandHandler(msg, args); } catch (err) { console.warn('Error handling command'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();
มาลองดูกัน.
เราไม่เพียงแค่ให้บอทตอบสนองต่อคำสั่ง pb!addpayment
แต่เราได้สร้างรูปแบบทั่วไปสำหรับการจัดการคำสั่ง เราสามารถเพิ่มคำสั่งต่างๆ ได้เพียงแค่เพิ่มตัวจัดการลงในพจนานุกรม commandHandlerForCommandName
เรามีการสร้างกรอบคำสั่งง่ายๆ ที่นี่ การจัดการคำสั่งเป็นส่วนพื้นฐานในการสร้างบอทที่หลายคนเขียนและเฟรมเวิร์กคำสั่งแบบโอเพนซอร์สที่คุณสามารถใช้ได้แทนการเขียนของคุณเอง กรอบคำสั่งมักจะอนุญาตให้คุณระบุคูลดาวน์ สิทธิ์ผู้ใช้ที่จำเป็น ชื่อแทนคำสั่ง คำอธิบายคำสั่ง และตัวอย่างการใช้งาน (สำหรับคำสั่งวิธีใช้ที่สร้างขึ้นโดยอัตโนมัติ) และอื่นๆ Eris มาพร้อมกับกรอบคำสั่งในตัว
เมื่อพูดถึงการอนุญาต บอทของเรามีปัญหาด้านความปลอดภัยเล็กน้อย ทุกคนสามารถดำเนินการคำสั่ง addpayment
เรามาจำกัดมันเพื่อให้เฉพาะเจ้าของบอทเท่านั้นที่สามารถใช้ได้ เราจะปรับโครงสร้างพจนานุกรม commandHandlerForCommandName
และกำหนดให้มีออบเจกต์ JavaScript เป็นค่าของมัน ออบเจ็กต์เหล่านั้นจะมีคุณสมบัติ execute
พร้อมตัวจัดการคำสั่ง และคุณสมบัติ botOwnerOnly
ที่มีค่าบูลีน นอกจากนี้ เราจะฮาร์ดโค้ด ID ผู้ใช้ของเราในส่วนค่าคงที่ของบอทเพื่อให้รู้ว่าใครเป็นเจ้าของ คุณสามารถค้นหา ID ผู้ใช้ของคุณได้โดยเปิดใช้งานโหมดนักพัฒนาซอฟต์แวร์ในการตั้งค่า Discord จากนั้นคลิกขวาที่ชื่อผู้ใช้ของคุณแล้วเลือก Copy ID
(ลิงก์รหัส GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step3.js)
const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const bot = new eris.Client('my_token'); const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();
ตอนนี้บอทจะโกรธปฏิเสธที่จะรันคำสั่ง addpayment
ถ้าใครก็ตามที่ไม่ใช่เจ้าของบอทพยายามที่จะรันคำสั่งนั้น

ต่อไป ให้บอทกำหนดบทบาท Premium Member
ให้กับทุกคนที่บริจาคสิบเหรียญขึ้นไป ที่ส่วนบนของไฟล์ bot.js:
(ลิงก์รหัส GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step4.js)
const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };
ตอนนี้ฉันสามารถลองพูดว่า pb!addpayment @Me 10.00
และบอทควรกำหนดบทบาท Premium Member
ให้ฉัน
อ๊ะ ข้อผิดพลาด Missing Permissions ปรากฏขึ้นในคอนโซล
DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013
บอทไม่มีสิทธิ์จัดการบทบาทในกิลด์ทดสอบ ดังนั้นจึงไม่สามารถสร้างหรือกำหนดบทบาทได้ เราสามารถให้สิทธิ์ผู้ดูแลระบบแก่บอทได้ และเราจะไม่พบปัญหาแบบนี้อีก แต่เช่นเดียวกับระบบอื่นๆ จะเป็นการดีที่สุดที่จะให้สิทธิ์ขั้นต่ำแก่ผู้ใช้เท่านั้น (หรือในกรณีนี้คือบอท)
เราสามารถให้สิทธิ์ Manage Roles แก่บอทได้โดยการสร้างบทบาทในการตั้งค่าเซิร์ฟเวอร์ เปิดใช้งานการอนุญาต Manage Roles สำหรับบทบาทนั้น และกำหนดบทบาทให้กับบอท
ตอนนี้ เมื่อฉันพยายามรันคำสั่งอีกครั้ง บทบาทจะถูกสร้างขึ้นและมอบหมายให้ฉัน และฉันมีสีชื่อแฟนซีและตำแหน่งพิเศษในรายชื่อสมาชิก
ในตัวจัดการคำสั่ง เรามีความคิดเห็นเกี่ยวกับสิ่งที่ต้องทำซึ่งแนะนำว่าเราจำเป็นต้องตรวจสอบอาร์กิวเมนต์ที่ไม่ถูกต้อง มาดูแลกันตอนนี้เลย
(ลิงก์รหัส GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)
const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };
นี่คือรหัสเต็มจนถึงตอนนี้:
(ลิงก์รหัส GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)
const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();
สิ่งนี้ควรให้แนวคิดพื้นฐานที่ดีแก่คุณเกี่ยวกับวิธีสร้างบอท Discord ตอนนี้เราจะมาดูวิธีการรวมบอทกับ Ko-fi กัน หากต้องการ คุณสามารถสร้างเว็บฮุคในแดชบอร์ดของคุณได้ที่ Ko-fi ตรวจสอบให้แน่ใจว่าเราเตอร์ของคุณได้รับการกำหนดค่าให้ส่งต่อพอร์ต 80 และส่งเว็บฮุคทดสอบจริงถึงตัวคุณเอง แต่ฉันแค่จะใช้บุรุษไปรษณีย์เพื่อจำลองคำขอ
Webhooks จาก Ko-fi นำเสนอเพย์โหลดที่มีลักษณะดังนี้:
data: { "message_id":"3a1fac0c-f960-4506-a60e-824979a74e74", "timestamp":"2017-08-21T13:04:30.7296166Z", "type":"Donation","from_name":"John Smith", "message":"Good luck with the integration!", "amount":"3.00", "url":"https://ko-fi.com" }
มาสร้างไฟล์ต้นฉบับใหม่ชื่อ webhook_listener.js และใช้ Express เพื่อฟัง webhooks เราจะมีเส้นทางด่วนเพียงเส้นทางเดียว และนี่เพื่อจุดประสงค์ในการสาธิต ดังนั้นเราจะไม่กังวลมากเกินไปเกี่ยวกับการใช้โครงสร้างไดเรกทอรีที่มีสำนวน เราจะใส่ตรรกะของเว็บเซิร์ฟเวอร์ทั้งหมดไว้ในไฟล์เดียว
(ลิงก์รหัส GitHub: https://github.com/mistval/premium_bot/blob/master/src/webhook_listener_step6.js)
const express = require('express'); const app = express(); const PORT = process.env.PORT || 80; class WebhookListener { listen() { app.get('/kofi', (req, res) => { res.send('Hello'); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;
จากนั้น ให้กำหนดให้ไฟล์ใหม่ที่ด้านบนสุดของ bot.js เพื่อให้ผู้ฟังเริ่มทำงานเมื่อเราเรียกใช้ bot.js
(ลิงก์รหัส GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)
const eris = require('eris'); const webhookListener = require('./webhook_listener.js');
หลังจากเปิดบอทแล้ว คุณจะเห็น “สวัสดี” เมื่อคุณไปที่ http://localhost/kofi ในเบราว์เซอร์ของคุณ
ตอนนี้ ให้ WebhookListener
ประมวลผลข้อมูลจาก webhook และปล่อยเหตุการณ์ และตอนนี้เราได้ทดสอบว่าเบราว์เซอร์ของเราสามารถเข้าถึงเส้นทางได้แล้ว ให้เปลี่ยนเส้นทางเป็นเส้นทาง POST เนื่องจากเว็บฮุคจาก Ko-fi จะเป็นคำขอ POST
(ลิงก์รหัส GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)
const express = require('express'); const bodyParser = require('body-parser'); const EventEmitter = require('events'); const PORT = process.env.PORT || 80; const app = express(); app.use(bodyParser.json()); class WebhookListener extends EventEmitter { listen() { app.post('/kofi', (req, res) => { const data = req.body.data; const { message, timestamp } = data; const amount = parseFloat(data.amount); const senderName = data.from_name; const paymentId = data.message_id; const paymentSource = 'Ko-fi'; // The OK is just for us to see in Postman. Ko-fi doesn't care // about the response body, it just wants a 200. res.send({ status: 'OK' }); this.emit( 'donation', paymentSource, paymentId, timestamp, amount, senderName, message, ); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;
Next we need to have the bot listen for the event, decide which user donated, and assign them a role. To decide which user donated, we'll try to find a user whose username is a substring of the message received from Ko-fi. Donors must be instructed to provide their username (with the discriminator) in the message than they write when they make their donation.
At the bottom of bot.js:
(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)
function findUserInString(str) { const lowercaseStr = str.toLowerCase(); // Look for a matching username in the form of username#discriminator. const user = bot.users.find( user => lowercaseStr.indexOf(`${user.username.toLowerCase()}#${user.discriminator}`) !== -1, ); return user; } async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await updateMemberRoleForDonation(guild, guildMember, amount); } catch (err) { console.warn('Error handling donation event.'); console.warn(err); } } webhookListener.on('donation', onDonation); bot.connect();
In the onDonation
function, we see two representations of a user: as a User, and as a Member. These both represent the same person, but the Member object contains guild-specific information about the User, such as their roles in the guild and their nickname. Since we want to add a role, we need to use the Member representation of the user. Each User in Discord has one Member representation for each guild that they are in.
Now I can use Postman to test the code.
I receive a 200 status code, and I get the role granted to me in the server.
If the message from Ko-fi does not contain a valid username; however, nothing happens. The donor doesn't get a role, and we are not aware that we received an orphaned donation. Let's add a log for logging donations, including donations that can't be attributed to a guild member.
First we need to create a log channel in Discord and get its channel ID. The channel ID can be found using the developer tools, which can be enabled in Discord's settings. Then you can right-click any channel and click “Copy ID.”
The log channel ID should be added to the constants section of bot.js.
(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)
const LOG_CHANNEL_;
And then we can write a logDonation
function.
(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)
function logDonation(member, donationAmount, paymentSource, paymentId, senderName, message, timestamp) { const isKnownMember = !!member; const memberName = isKnownMember ? `${member.username}#${member.discriminator}` : 'Unknown'; const embedColor = isKnownMember ? 0x00ff00 : 0xff0000; const logMessage = { embed: { title: 'Donation received', color: embedColor, timestamp: timestamp, fields: [ { name: 'Payment Source', value: paymentSource, inline: true }, { name: 'Payment ID', value: paymentId, inline: true }, { name: 'Sender', value: senderName, inline: true }, { name: 'Donor Discord name', value: memberName, inline: true }, { name: 'Donation amount', value: donationAmount.toString(), inline: true }, { name: 'Message', value: message, inline: true }, ], } } bot.createMessage(LOG_CHANNEL_ID, logMessage); }
Now we can update onDonation
to call the log function:
async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await Promise.all([ updateMemberRoleForDonation(guild, guildMember, amount), logDonation(guildMember, amount, paymentSource, paymentId, senderName, message, timestamp), ]); } catch (err) { console.warn('Error updating donor role and logging donation'); console.warn(err); } }
Now I can invoke the webhook again, first with a valid username, and then without one, and I get two nice log messages in the log channel.
Previously, we were just sending strings to Discord to display as messages. The more complex JavaScript object that we create and send to Discord in the new logDonation
function is a special type of message referred to as a rich embed. An embed gives you some scaffolding for making attractive messages like those shown. Only bots can create embeds, users cannot.
Now we are being notified of donations, logging them, and rewarding our supporters. We can also add donations manually with the addpayment command in case a user forgets to specify their username when they donate. Let's call it a day.
The completed code for this tutorial is available on GitHub here https://github.com/mistval/premium_bot
ขั้นตอนถัดไป
We've successfully created a bot that can help us track donations. Is this something we can actually use? Well, perhaps. It covers the basics, but not much more. Here are some shortcomings you might want to think about first:
- If a user leaves our guild (or if they weren't even in our guild in the first place), they will lose their
Premium Member
role, and if they rejoin, they won't get it back. We should store payments by user ID in a database, so if a premium member rejoins, we can give them their role back and maybe send them a nice welcome-back message if we were so inclined. - Paying in installments won't work. If a user sends $5 and then later sends another $5, they won't get a premium role. Similar to the above issue, storing payments in a database and issuing the
Premium Member
role when the total payments from a user reaches $10 would help here. - It's possible to receive the same webhook more than once, and this bot will record the payment multiple times. If Ko-fi doesn't receive or doesn't properly acknowledge a code 200 response from the webhook listener, it will try to send the webhook again later. Keeping track of payments in a database and ignoring webhooks with the same ID as previously received ones would help here.
- Our webhook listener isn't very secure. Anyone could forge a webhook and get a
Premium Member
role for free. Ko-fi doesn't seem to sign webhooks, so you'll have to rely on either no one knowing your webhook address (bad), or IP whitelisting (a bit better). - The bot is designed to be used in one guild only.
Interview: When a Bot Gets Big
There are over a dozen websites for listing Discord bots and making them available to the public at large, including DiscordBots.org and Discord.Bots.gg. Although Discord bots are mostly the foray of small-time hobbyists, some bots experience tremendous popularity and maintaining them evolves into a complex and demanding job.
By guild-count, Rythm is currently the most widespread bot on Discord. Rythm is a music bot whose specialty is connecting to voice channels in Discord and playing music requested by users. Rythm is currently present in over 2,850,000 guilds containing a sum population of around 90 million users, and at its peak plays audio for around 100,000 simultaneous users in 20,000 separate guilds. Rythm's creator and main developer, ImBursting, kindly agreed to answer a few questions about what it's like to develop and maintain a large-scale bot like Rythm.
Interviewer: Can you tell us a bit about Rythm's high level architecture and how it's hosted?
ImBursting: Rythm is scaled across 9 physical servers, each have 32 cores, 96GB of RAM and a 10gbps connection. These servers are collocated at a data center with help from a small hosting company, GalaxyGate.
I imagine that when you started working on Rythm, you didn't design it to scale anywhere near as much as it has. Can you tell us about about how Rythm started, and its technical evolution over time?
Rythm's first evolution was written in Python, which isn't a very performant language, so around the time we hit 10,000 servers (after many scaling attempts) I realised this was the biggest roadblock and so I began recoding the bot to Java, the reason being Java's audio libraries were a lot more optimised and it was generally a better suited language for such a huge application. After re-coding, performance improved tenfold and kept the issues at bay for a while. And then we hit the 300,000 servers milestone when issues started surfacing again, at which point I realised that more scaling was required since one JVM just wasn't able to handle all that. So we slowly started implementing improvements and major changes like tuning the garbage collector and splitting voice connections onto separate microservices using an open source server called Lavalink. This improved performance quite a bit but the final round of infrastructure was when we split this into 9 seperate clusters to run on 9 physical servers, and made custom gateway and stats microservices to make sure everything ran smoothly like it would on one machine.
I noticed that Rythm has a canary version and you get some help from other developers and staff. I imagine you and your team must put a lot of effort into making sure things are done right. Can you tell us about what processes are involved in updating Rythm?
Rythm canary is the alpha bot we use to test freshly made features and performance improvements before usually deploying them to Rythm 2 to test on a wider scale and then production Rythm. The biggest issue we encounter is really long reboot times due to Discord rate limits, and is the reason I try my best to make sure an update is ready before deciding to push it.
I do get a lot of help from volunteer developers and people who genuinely want to help the community, I want to make sure everything is done correctly and that people will always get their questions answered and get the best support possible which means im constantly on the lookout for new opportunities.
Wrapping It Up
Discord's days of being a new kid on the block are past, and it is now one of the largest real-time communication platforms in the world. While Discord bots are largely the foray of small-time hobbyists, we may well see commercial opportunities increase as the population of the service continues to increase. Some companies, like the aforementioned Patreon, have already waded in.
In this article, we saw a high-level overview of Discord's user interface, a high-level overview of its APIs, a complete lesson in Discord bot programming, and we got to hear about what it's like to operate a bot at enterprise scale. I hope you come away interested in the technology and feeling like you understand the fundamentals of how it works.
Chatbots are generally fun, except when their responses to your intricate queries have the intellectual the depth of a cup of water. To ensure a great UX for your users see The Chat Crash - When a Chatbot Fails by the Toptal Design Blog for 5 design problems to avoid.