ตัวจัดการภายนอก
ตัวจัดการภายนอกคือสะพานเชื่อมระหว่างโค้ดของคุณกับ Telegram เมื่อลอจิกของตัวสร้างไม่เพียงพอ คุณมอบรีแอคชันที่แยกต่างหากให้กับเซิร์ฟเวอร์ของคุณ: เซิร์ฟเวอร์รับเหตุการณ์และตอบด้วยคำสั่งในรูปแบบ Telegram Bot API ในขณะที่ GetMyBot จัดการการขนส่ง การส่งมอบ และ Telegram ทั้งหมด
คืออะไร
รีแอคชันใน GetMyBot สร้างด้วยภาพ: ทริกเกอร์ → เงื่อนไข → การกระทำ ลอจิกที่ซับซ้อนหรือไม่ได้มาตรฐาน — ฐานข้อมูลของคุณเอง การคำนวณ การเรียกระบบอื่น ML สถานการณ์ที่มีหลายสาขา — นั้นยากที่จะแสดงออกในตัวสร้าง ตัวจัดการภายนอกขจัดเพดานนี้: คุณเชื่อมต่อแบ็กเอนด์ของคุณเองและมอบรีแอคชันทั้งหมดให้มันจัดการ
บริการภายนอกรับเหตุการณ์จาก GetMyBot (ทริกเกอร์ทำงาน ผู้ใช้ตอบข้อความ กดปุ่ม) และในการตอบสนองจะสั่งบอทว่าจะส่งอะไร GetMyBot ดำเนินการคำสั่งด้วยโทเค็นบอทของตัวเอง — rate-limit ลองใหม่ ยกเว้นซ้ำ และบันทึกไดอะล็อกทำงานอยู่เบื้องหลัง
โมเดลการทำงาน
- GetMyBot คือ WebSocket เซิร์ฟเวอร์ บริการของคุณเชื่อมต่อกับ GetMyBot เองและไม่จำเป็นต้องมีที่อยู่สาธารณะหรือ webhook endpoint GetMyBot ไม่ได้สร้างการเชื่อมต่อขาออกไปยังบริการของคุณ ดังนั้นจึงไม่มีพื้นผิว SSRF
- การพิสูจน์ตัวตนด้วยโทเค็นการเชื่อมต่อ บริการแสดงโทเค็นเมื่อเชื่อมต่อ GetMyBot ตรวจสอบกับแฮชและรักษาการเชื่อมต่อ
- เหตุการณ์และคำสั่งในรูปแบบ Telegram Bot API บริการรับเหตุการณ์และตอบด้วยอาร์เรย์การเรียกในรูปแบบ «เมธอด + พารามิเตอร์» เหมือนกับการทำงานกับบอทปกติทุกประการ
- GetMyBot ดำเนินการคำสั่งด้วยโทเค็นของตัวเอง บริการไม่เคยสื่อสารกับ Telegram โดยตรง: คำสั่งผ่านการตรวจสอบและเข้าสู่ outbox ภายในของ GetMyBot ซึ่งส่งด้วยโทเค็นบอทพร้อม rate-limit ลองใหม่ ยกเว้นซ้ำ และบันทึก
การเชื่อมต่อเดียวรองรับไดอะล็อกทั้งหมดของการเชื่อมต่อหนึ่ง — เหตุการณ์ของผู้ใช้ต่าง ๆ ถูกมัลติเพล็กซ์และแยกแยะด้วยช่อง session_id
การตั้งค่าการเชื่อมต่อ
- เปิดหัวข้อ การเชื่อมต่อ ในเมนูด้านซ้ายและคลิก เพิ่มการเชื่อมต่อ
- เลือกบริการ ตัวจัดการภายนอก และตั้งชื่อ
- หลังสร้างแล้ว การ์ดจะแสดง:
- ที่อยู่ WebSocket — ในรูปแบบ
wss://api.mybot.app/ext/ws/<integrationID>โดยที่<integrationID>คือ UUID ของการเชื่อมต่อนี้ บริการของคุณเชื่อมต่อผ่านที่อยู่นี้ การเชื่อมต่อจะไม่ถูกสร้างหากไม่มี id ในเส้นทาง - โทเค็นการเชื่อมต่อ — แสดง ครั้งเดียว เมื่อสร้าง คัดลอกและบันทึกในที่ปลอดภัย ไม่สามารถดูโทเค็นซ้ำได้ (จัดเก็บเป็นแฮชเท่านั้น)
- ออกโทเค็นใหม่ — ปุ่มสร้างโทเค็นใหม่และทำให้โทเค็นเก่าหมดอายุทันที หลังออกใหม่ ให้อัปเดตโทเค็นในบริการของคุณ
- ตัวบ่งชี้ออนไลน์ — แสดงว่าบริการของคุณกำลังรักษาการเชื่อมต่อที่ใช้งานอยู่หรือไม่
- ที่อยู่ WebSocket — ในรูปแบบ
พารามิเตอร์เพิ่มเติมของการเชื่อมต่อที่มีผลต่อโปรโตคอล:
session_ttl_seconds— ระยะเวลาของเซสชันเป็นวินาที (ค่าเริ่มต้น3600)on_unavailable_reaction_id— รีแอคชันสำรองที่ทำงานหากบริการออฟไลน์เมื่อรีแอคชันทำงาน (ดู ความเชื่อถือได้)
การผูกกับรีแอคชัน
ตัวจัดการภายนอกเชื่อมต่อไม่ใช่เป็นทริกเกอร์แยกต่างหาก แต่เป็น การกระทำ ในตัวแก้ไขรีแอคชัน ให้เพิ่มการกระทำ ส่งไปยังตัวจัดการภายนอก และเลือกการเชื่อมต่อที่ต้องการ
ทริกเกอร์ GetMyBot ที่มีอยู่ใด ๆ สามารถเปิดไดอะล็อกได้ — คำสั่ง ข้อความ การกดปุ่ม พารามิเตอร์จากลิงก์ คำขอเว็บขาเข้า ตารางเวลา เมื่อทริกเกอร์ดังกล่าวทำงานและถึงการกระทำ GetMyBot จะเปิดเซสชันพร็อกซีและส่งเหตุการณ์ session.open ให้บริการ
หากในขณะนั้นบริการไม่ได้เชื่อมต่อ GetMyBot จะเรียกใช้ รีแอคชันสำรอง จากการตั้งค่าการเชื่อมต่อ (on_unavailable_reaction_id) หากไม่ได้กำหนดรีแอคชันสำรอง — จะไม่มีอะไรเกิดขึ้น (เงียบ ไม่มีข้อผิดพลาดถึงผู้ใช้)
การเชื่อมต่อและการพิสูจน์ตัวตน
Endpoint: GET /ext/ws/{integrationID} ผ่าน WebSocket (wss) integrationID — UUID ของการเชื่อมต่อประเภท «ตัวจัดการภายนอก» จำกัดขนาดเฟรม — 256 KiB
สามารถพิสูจน์ตัวตนได้หนึ่งในสองวิธี
1. ส่วนหัวระหว่าง handshake (แนะนำ) ส่งโทเค็นในส่วนหัว Authorization:
Authorization: Bearer <token>
2. Auth เฟรม หาก Bearer header ไม่ได้ส่ง ให้ส่งเฟรมแรกภายใน 10 วินาที หลังสร้างการเชื่อมต่อ:
{ "type": "auth", "token": "<token>" }
หาก auth เฟรมไม่ถูกส่งภายใน 10 วินาที — การเชื่อมต่อจะปิดด้วยรหัส 4401 โทเค็นที่ไม่ถูกต้องในวิธีใดก็ตาม — ปิดด้วย 4401 เช่นกัน
โทเค็นถูกสร้างเมื่อสร้างการเชื่อมต่อ แสดงครั้งเดียวและจัดเก็บเป็นแฮชเท่านั้น (sha256) การตรวจสอบแบบ constant-time รองรับการหมุนเวียน:
POST /api/bots/{botID}/integrations/{integrationID}/rotate-token
Endpoint ส่งคืนโทเค็น plaintext ใหม่ โทเค็นเก่าจะหยุดทำงานทันที
เหตุการณ์: บอท → บริการ
แต่ละเหตุการณ์คือ JSON เฟรม (EventEnvelope) ช่อง type — หนึ่งใน: session.open, message, callback, session.cancel, session.expired
ชุดช่องทั้งหมดของ envelope:
{
"type": "session.open",
"session_id": "string",
"bot_id": "string",
"user": {
"tg_user_id": 123456789,
"first_name": "Ivan",
"username": "ivan",
"params": { "utm": "promo" }
},
"chat": { "id": 123456789, "type": "private" },
"update": { "...": "raw Telegram Update (raw JSON)" },
"event": { "...": "normalized GetMyBot event (raw JSON)" }
}
session_id— ตัวระบุเซสชัน (ไดอะล็อก)bot_id— ตัวระบุบอทuser— ข้อมูลผู้ใช้:tg_user_id(int64)first_nameไม่บังคับusernameและparams— พารามิเตอร์ผู้ใช้ (รวมถึงจากลิงก์) map คีย์→ค่าchat— แชท:id(int64) และtype(เช่น"private")update— raw Telegram Update (raw JSON) มีสำหรับsession.open,message,callbackevent— เหตุการณ์ GetMyBot ที่ normalize แล้ว (raw JSON)
เนื้อหาแตกต่างกันตามประเภท:
session.open— ทริกเกอร์ทำงาน เปิดไดอะล็อกใหม่ มีupdate(raw Update) และevent(เหตุการณ์ normalize แบบสมบูรณ์)user.paramsถูกกรอกmessage— มาเมื่อบริการ «รอ» (expect=text/any) และผู้ใช้ส่งข้อความ เนื้อหาเหมือนกับsession.opencallback— ผู้ใช้กดปุ่มที่บริการส่งมาก่อนหน้าupdateเป็น raw; ในuserมีเพียงtg_user_id;eventมีเฉพาะ callback_data ต้นฉบับที่บริการกำหนดในปุ่ม:
{ "callback_data": "<ค่าต้นฉบับที่บริการกำหนดในปุ่ม>" }
session.cancel/session.expired— envelope ขั้นต่ำ:session_id,bot_id,user.tg_user_id,chat.id(สำหรับcancelยังมีchat.type) ไม่มีช่องupdate/eventเหตุการณ์เหล่านี้ส่งได้ เฉพาะเมื่อบริการออนไลน์ — ไม่ถูกใส่ในคิวออฟไลน์
คำสั่ง: บริการ → บอท
คำสั่งคือ JSON เฟรม (CommandEnvelope) ช่อง type — หนึ่งใน: execute, session.close, auth, ping, pong
{
"type": "execute",
"session_id": "string",
"methods": [
{ "method": "sendMessage", "params": { "text": "สวัสดีจากตัวจัดการภายนอก!" } }
],
"expect": "none"
}
session_id— เซสชันที่คำสั่งเกี่ยวข้องtoken— เฉพาะสำหรับtype: "auth"(ดู การเชื่อมต่อและการพิสูจน์ตัวตน)methods— อาร์เรย์ของออบเจกต์{ "method": "<ชื่อเมธอด Telegram Bot API>", "params": { ... } }ชื่อเมธอดตรงกับ Telegram Bot API (เช่นsendMessage) และparamsคือออบเจกต์พารามิเตอร์ของเมธอดนั้นexpect— บริการรอการตอบกลับของผู้ใช้หรือไม่:"none","text"หรือ"any"(ค่าว่างถือเป็นnone)session.close— ปิดไดอะล็อกอย่างชัดเจน
คำสั่ง ไม่ไปยัง Telegram โดยตรง: GetMyBot ตรวจสอบและใส่ใน outbox ของตัวเอง ซึ่งส่งด้วยโทเค็นบอท (พร้อม rate-limit ลองใหม่ ยกเว้นซ้ำ และบันทึก)
วิธีจัดการ execute
คำสั่ง execute จะถูกทิ้งทั้งหมดหากว่า: session_id ว่าง; เซสชันไม่พบหรือไม่อยู่ในสถานะ open; เซสชันเป็นของการเชื่อมต่ออื่น; เซสชันหมดอายุ
จากนั้น:
- Rate-limit: สูงสุด 60
executeต่อนาทีต่อเซสชัน (หน้าต่างเลื่อน) เกินกว่านั้น — ทิ้ง - สูงสุด 30 เมธอด ในหนึ่ง
execute; เมธอดที่เกินจะถูกตัดทิ้งเงียบ ๆ - แต่ละเมธอดตรวจสอบ whitelist (ดู เมธอดที่อนุญาต) — เมธอดที่ไม่อยู่ในรายการจะถูกปฏิเสธ
- Chat-guard: หาก
paramsมีchat_idหรือfrom_chat_idที่มีค่าไม่เท่ากับ0และไม่เท่ากับchat_idของเซสชัน — เมธอดทั้งหมดจะถูกปฏิเสธ (ไม่สามารถส่งไปยังแชทอื่น) สามารถไม่ระบุchat_id: GetMyBot จะกำหนดแชทของเซสชันให้โดยบังคับ - ปุ่ม inline ที่มี
callback_dataจะถูก tokenize โดยอัตโนมัติ (รูปแบบภายในx:<token>); ระยะเวลาของปุ่ม = จนกว่าเซสชันจะหมดอายุ ปุ่มurl/webapp/switch_inlineจะไม่ถูกแตะต้อง
เมธอดที่อนุญาต
บริการสั่งบอท ดังนั้นชุดเมธอดจำกัดด้วย whitelist — เฉพาะการส่งและทำงานกับข้อความของไดอะล็อกปัจจุบัน เมธอดที่ไม่อยู่ในรายการ จะถูกละเว้นเงียบ ๆ (ไม่ใช่ข้อผิดพลาด)
- การส่ง:
sendMessage,sendPhoto,sendDocument,sendVideo,sendAudio,sendMediaGroup,sendAnimation,sendVoice,sendLocation,sendChatAction - การแก้ไขและลบ:
editMessageText,editMessageCaption,editMessageReplyMarkup,deleteMessage - การตอบการกด:
answerCallbackQuery - การส่งต่อและคัดลอก:
forwardMessage,copyMessage - การปักหมุด:
pinChatMessage,unpinChatMessage
เมธอดระดับบัญชี (setWebhook, getUpdates, logOut, close, setMyCommands เป็นต้น) ไม่ได้รับอนุญาต
การรอและวงจรชีวิตของเซสชัน
หลังจากแต่ละ execute ชะตากรรมของเซสชันขึ้นอยู่กับ expect และว่ามีปุ่ม inline ที่มี callback_data หรือไม่:
expect=textหรือany— ข้อความถัดไปของผู้ใช้จะถูกส่งไปยังบริการเป็นเหตุการณ์messageexpect=noneและคำสั่งไม่มีปุ่ม inline ที่มี callback — เซสชันจะปิดโดยอัตโนมัติ นี่คือข้อความ «สิ้นสุด»expect=noneแต่มีปุ่ม callback — เซสชันยังคงเปิดอยู่: การกดปุ่มจะถูกจับ ตราบเท่าที่โทเค็นปุ่มยังมีชีวิต (จนถึง TTL)
สำคัญ: expect ไม่มีผลต่อการรับการกดปุ่ม การกดปุ่มที่ tokenize แล้วจะถูกจับโดยไม่คำนึงถึงค่า expect — ตราบเท่าที่เซสชันและโทเค็นปุ่มยังมีชีวิต expect ควบคุมเฉพาะว่าบริการรอ การตอบกลับด้วยข้อความ/อะไรก็ตาม หรือไม่
ขอบเขตของไดอะล็อก
บริบทของไดอะล็อกมีชีวิตอยู่ตราบเท่าที่เซสชันเปิดอยู่:
- การตอบกลับด้วยข้อความเมื่อรอใช้งาน (
expect=text/any) — ส่งไปยังบริการเป็นเหตุการณ์messageไม่ใช่เรียกใช้รีแอคชันปกติของบอท - การกดปุ่มที่ tokenize — ส่งไปยังบริการเป็นเหตุการณ์
callbackพร้อมcallback_dataต้นฉบับ GetMyBot ยืนยันการกดเสมอ (answerCallbackQuery); ตรวจสอบว่าการกดเป็นของเซสชัน (บอท + ผู้ใช้ + แชท) การกดปุ่ม ไม่ปิดเซสชัน /cancel(รวมถึง/cancel@botและ «ยกเลิก») เมื่อมีเซสชันภายนอกที่ใช้งานอยู่ — เซสชันจะปิด ส่งsession.cancelให้บริการ และส่ง «ยกเลิกแล้ว» ให้ผู้ใช้- ไดอะล็อกเดียวต่อผู้ใช้ การเปิดเซสชันใหม่จะแทนที่เซสชันที่เปิดอยู่ก่อนหน้าของผู้ใช้ — โดยส่ง
session.cancelให้เซสชันนั้น - การหมดอายุ TTL เมื่อหมดเวลา เซสชันจะถูกทำเครื่องหมายว่า
expiredและ (หากบริการออนไลน์) จะส่งsession.expiredให้บริการ
ข้อจำกัดและหมดเวลา
- ขนาดเฟรม: 256 KiB
- TTL เซสชัน: ค่าเริ่มต้น 3600 วินาที (ปรับได้ด้วยพารามิเตอร์
session_ttl_secondsของการเชื่อมต่อ) - ความถี่คำสั่ง: 60
executeต่อนาทีต่อเซสชัน (หน้าต่างเลื่อน) - เมธอดในหนึ่ง
execute: สูงสุด 30 (เมธอดที่เกินจะถูกทิ้ง) - เซสชันที่เปิดต่อการเชื่อมต่อ: สูงสุด 1000
- คิวออฟไลน์: สูงสุด 100 เหตุการณ์ต่อเซสชัน
- Heartbeat:
pingทุก 30 วินาที; หมดเวลาหากไม่มีกิจกรรม — 60 วินาที - หมดเวลา auth เฟรม: 10 วินาที
ความเชื่อถือได้ (heartbeat, ออฟไลน์, คิว, fallback)
- Heartbeat เซิร์ฟเวอร์ส่งเฟรม
{"type":"ping"}ทุก 30 วินาที หากไม่มีกิจกรรมจากไคลเอนต์นานกว่า 60 วินาที — การเชื่อมต่อจะปิด ไคลเอนต์สามารถส่งping/pongของตัวเองเพื่อรักษากิจกรรม การตัดการเชื่อมต่อ ไม่ทำลายไดอะล็อก: เซสชันมีชีวิตอยู่ในฐานข้อมูลจนถึง TTL หรือการเชื่อมต่อใหม่ - การเชื่อมต่อใหม่ — ฝั่งบริการ หากการเชื่อมต่อขาด บริการของคุณต้องเชื่อมต่อใหม่ เซสชันที่เปิดอยู่ในฐานข้อมูลจะรอดชีวิตจากการรีสตาร์ทจนกว่า TTL จะหมด
- Fallback เมื่อออฟไลน์ตั้งแต่เริ่ม หากบริการไม่ได้เชื่อมต่อเมื่อรีแอคชันทำงาน จะเรียกใช้รีแอคชันสำรองจากการตั้งค่าการเชื่อมต่อ (
on_unavailable_reaction_id) หากไม่ได้กำหนด — จะไม่มีอะไรเกิดขึ้น (เงียบ) - คิวออฟไลน์ เหตุการณ์
message/callbackที่ไม่ได้ส่งเนื่องจากออฟไลน์จะถูกใส่ในคิวออฟไลน์ (สูงสุด 100 เหตุการณ์ FIFO เก็บไว้จนเซสชันหมดอายุ) และจะถูกส่งเมื่อเชื่อมต่อใหม่ เหตุการณ์session.open/session.cancel/session.expiredไม่ถูกใส่ในคิว - การรับประกันการส่ง — at-most-once เมื่อคิวเต็มหรือหมดอายุ เหตุการณ์จะถูกทิ้ง ออกแบบลอจิกให้การข้ามเหตุการณ์เดียวไม่ทำลายสถานการณ์
ตัวบ่งชี้ออนไลน์
ตรวจสอบว่าบริการกำลังรักษาการเชื่อมต่อที่ใช้งานอยู่ได้ผ่าน endpoint:
GET /api/bots/{botID}/integrations/{integrationID}/status
ผลลัพธ์:
{ "online": true }
ตัวบ่งชี้เดียวกันอยู่บนการ์ดการเชื่อมต่อในแดชบอร์ด ใน MCP เทียบเท่ากับเครื่องมือ get_integration_status
ความปลอดภัย
GetMyBot ดำเนินการคำสั่งของบริการภายนอกด้วยโทเค็นของตัวเอง ดังนั้นการป้องกันจึงเข้มงวด
- ไม่มีพื้นผิว SSRF GetMyBot ทำหน้าที่เป็น WebSocket เซิร์ฟเวอร์ และไม่ได้สร้างการเชื่อมต่อขาออกไปยังบริการ — บริการเป็นผู้เชื่อมต่อกับ GetMyBot ไม่มี URL ที่ควบคุมจากภายนอกที่แพลตฟอร์มจะเรียกถึง
- โทเค็นจัดเก็บเป็นแฮช (sha256) การตรวจสอบแบบ constant-time รองรับการหมุนเวียน
- การแยกผู้เช่า คำสั่งผูกแน่นกับการเชื่อมต่อของตน เซสชันผูกกับบอท + ผู้ใช้ + แชท เหตุการณ์ของบอทอื่นจะไม่เข้าสู่ซ็อกเก็ตของคุณ
- Whitelist เมธอด + บังคับ
chat_idป้องกันไม่ให้บอทกลายเป็นผู้ส่งข้อความไปยังแชทใด ๆ ก็ตาม - โทเค็นบอทไม่ถูกส่งออกไปภายนอก — บริการไม่เคยสื่อสารกับ Telegram โดยตรง
- การปกปิดความลับ โทเค็นการเชื่อมต่อและค่าที่ละเอียดอ่อนจะถูกซ่อนในบันทึกและ log
ตัวอย่าง
ตัวจัดการขั้นต่ำ: เมื่อเปิดไดอะล็อกจะส่งข้อความและปิดเซสชัน (expect: "none") ใส่ id การเชื่อมต่อและโทเค็น ที่อยู่ต้องมี <integrationID> ในเส้นทางเสมอ
Node.js
import WebSocket from "ws";
const URL = `wss://api.mybot.app/ext/ws/${process.env.MYBOT_INTEGRATION_ID}`;
const ws = new WebSocket(URL, {
headers: { Authorization: `Bearer ${process.env.MYBOT_TOKEN}` },
});
ws.on("message", (raw) => {
const ev = JSON.parse(raw);
if (ev.type === "session.open") {
ws.send(JSON.stringify({
type: "execute",
session_id: ev.session_id,
expect: "none",
methods: [{ method: "sendMessage", params: { text: "สวัสดีจากตัวจัดการภายนอก!" } }],
}));
}
});
Python
import json, os, asyncio, websockets
async def main():
url = f"wss://api.mybot.app/ext/ws/{os.environ['MYBOT_INTEGRATION_ID']}"
headers = {"Authorization": f"Bearer {os.environ['MYBOT_TOKEN']}"}
async with websockets.connect(url, additional_headers=headers) as ws:
async for raw in ws:
ev = json.loads(raw)
if ev["type"] == "session.open":
await ws.send(json.dumps({
"type": "execute",
"session_id": ev["session_id"],
"expect": "none",
"methods": [
{"method": "sendMessage", "params": {"text": "สวัสดีจากตัวจัดการภายนอก!"}}
],
}))
asyncio.run(main())
ปุ่ม inline และการจัดการ callback
เพื่อจับการกด ให้ส่งปุ่มที่มี callback_data (GetMyBot จะ tokenize โดยอัตโนมัติ) เมื่อใช้ expect: "none" กับปุ่ม เซสชันจะยังเปิดอยู่ตราบเท่าที่โทเค็นปุ่มยังมีชีวิต — การกดจะมาเป็นเหตุการณ์ callback โดยที่ event.callback_data เท่ากับค่าต้นฉบับ ไม่จำเป็นต้องยืนยันการกด (answerCallbackQuery) — GetMyBot ทำให้เอง
if (ev.type === "session.open") {
ws.send(JSON.stringify({
type: "execute",
session_id: ev.session_id,
expect: "none",
methods: [{
method: "sendMessage",
params: {
text: "กดปุ่ม",
reply_markup: { inline_keyboard: [[{ text: "ไปเลย", callback_data: "go" }]] },
},
}],
}));
} else if (ev.type === "callback" && ev.event.callback_data === "go") {
ws.send(JSON.stringify({
type: "execute",
session_id: ev.session_id,
expect: "none",
methods: [{ method: "sendMessage", params: { text: "กดปุ่มแล้ว!" } }],
}));
}
ขั้นตอนถัดไป
- การเชื่อมต่อ — ภาพรวมของบริการที่เชื่อมต่อได้
- รีแอคชัน — วิธีสร้างทริกเกอร์ เงื่อนไข และการกระทำ
- คำขอเว็บและ webhook — วิธีที่ง่ายกว่าในการเรียก URL ภายนอกโดยไม่ต้องมีไดอะล็อก