คู่มือการเชื่อมต่อ Discord Bot Gateway & API
ระบบเกตเวย์ส่งการแจ้งเตือนทางเดียวแบบรวมศูนย์ (Centralized One-Way Discord Notification Gateway) ทำหน้าที่ส่งข้อความแจ้งเตือนหาเจ้าหน้าที่ (Direct Message) หรือประกาศลงห้อง Discord Channel โดยตรง เชื่อมโยงบัญชี SSO และเลขบัตรประชาชนอัตโนมัติ พร้อมระบบความปลอดภัยและการควบคุมสิทธิ์ระดับสูง
• ไม่รองรับปฏิกิริยารีแอ็คชัน (No Reactions): บอทจะไม่ใส่อีโมจิหรือรับรีแอ็คชันใดๆ จากผู้ใช้
• ไม่รองรับการตอบกลับใดๆ (No Replies / No Inbound Chat): บอทจะไม่ประมวลผลข้อความแชทตอบกลับใน DM หรือการพิมพ์คุยกับบอท เพื่อความปลอดภัยและป้องกันสแปม
• ทำได้เฉพาะการส่งข้อความหา User ทางเดียว (Outbound Dispatch Only): ส่ง Text, Rich Embed และปุ่มประเภท
LINK (สำหรับคลิกเปิดหน้าเว็บระบบภายนอกเท่านั้น)
1. แม่แบบคำสั่งสำหรับป้อนให้ AI ของระบบลูก (AI Integration Prompt)
หากท่านกำลังพัฒนาระบบลูก (เช่น ระบบระบาดวิทยา Epitrack, ระบบแผนยุทธศาสตร์ หรือระบบรายงาน KPI) และใช้ AI ช่วยเขียนโค้ด (Cursor, Copilot, ChatGPT) สามารถคัดลอกข้อความด้านล่างนี้ไปให้ AI เรียนรู้เพื่อเขียนฟังก์ชันส่งแจ้งเตือนได้ทันที:
2. สถาปัตยกรรมและความปลอดภัย (Architecture & Security)
ระบบเกตเวย์ทำหน้าที่เป็น Outbound Notification Hub ตัวกลางระหว่างเว็บแอปพลิเคชันสาธารณสุขกับ Discord โดยมีมาตรการรักษาความปลอดภัยระดับสูง:
- One-Way Outbound Architecture: ระบบปิดการรับอินพุตโต้ตอบที่ไม่จำเป็น (No Reactions / No User Chat Replies) เพื่อตัดช่องทางการโจมตี (Attack Surface), ป้องกันสแปม และรักษาความเสถียรสูงสุด
- SSO Identity Mapping: ระบบลูกไม่จำเป็นต้องทราบ Discord ID ของเจ้าหน้าที่ เพียงส่งเลขบัตร ปชช. 13 หลัก (
target_citizen_id) ระบบจะจับคู่กับฐานข้อมูล SSO ของปัตตานีอัตโนมัติ - Client ID & Secret Isolation: แต่ละระบบลูกจะได้รับกุญแจประจำระบบ แยกโควตา Rate Limit อย่างอิสระ ป้องกันผลกระทบข้ามระบบ
- Process Supervisor & Token Security: กระบวนการทำงานของบอทแยกออกจาก Web Server อย่างชัดเจน พร้อมระบบ Heartbeat/Ping ตรวจสถานะที่ปลอดภัยโดยไม่ทำให้ Bot Token ถูกรีเซ็ต
- Centralized Admin Permissions: คำสั่งควบคุมระบบบน Discord ได้รับการปกป้องด้วยระบบตรวจสอบสิทธิ์แบบ Multi-tier อนุญาตเฉพาะแอดมินที่กำหนดสิทธิ์ไว้ใน MyWellnet เท่านั้น
2. สถาปัตยกรรมและความปลอดภัย (Architecture & Security)
ระบบเกตเวย์ทำหน้าที่เป็น Hub ตัวกลางระหว่างเว็บแอปพลิเคชันสาธารณสุขกับ Discord โดยมีมาตรการรักษาความปลอดภัยระดับสูง:
- Client ID & Secret: แต่ละระบบลูกจะได้รับกุญแจประจำระบบ แยกโควตา Rate Limit อย่างอิสระ
- SSO Identity Mapping: ระบบลูกไม่จำเป็นต้องทราบ Discord ID ของเจ้าหน้าที่ เพียงส่งเลขบัตร ปชช. 13 หลัก (`target_citizen_id`) ระบบจะจับคู่กับฐานข้อมูล SSO ของปัตตานีอัตโนมัติ
- HMAC-SHA256 Webhook Verification: เมื่อเจ้าหน้าที่กดปุ่มตอบรับ ทุกการกระทำจะถูกเข้ารหัสลายเซ็นดิจิทัลส่งกลับไประบบลูก เพื่อป้องกันการปลอมแปลงคำสั่ง
3. รายละเอียด API Endpoints
/api/v1/notify — ส่งการแจ้งเตือนทางเดียว (Outbound Notification)
ส่งข้อความหาเจ้าหน้าที่แบบส่วนตัว (Direct Message) หรือส่งเข้าห้องดิสคอร์ด
พารามิเตอร์ Request Body (JSON):
| ฟิลด์ | ชนิด | ความจำเป็น | คำอธิบาย |
|---|---|---|---|
| target_citizen_id | String | อย่างใดอย่างหนึ่ง | เลขประจำตัวประชาชน 13 หลักของผู้รับ (ระบบจะดึง Discord ID อัตโนมัติ) |
| target_email | String | อย่างใดอย่างหนึ่ง | อีเมลที่ผูกไว้กับ SSO |
| target_discord_id | String | อย่างใดอย่างหนึ่ง | Discord User Snowflake ID 18-19 หลักของผู้รับโดยตรง |
| bot_id | String | ทางเลือก | ระบุบอทที่ต้องการใช้ส่ง เช่น portal, epitrack, strat, kpi (ค่าเริ่มต้น: บอทที่ผูกไว้) |
| content | String | ทางเลือก | ข้อความธรรมดา (Plain text) เช่น ข้อความแจ้งเตือนด่วน |
| embed | Object | ทางเลือก | Rich Embed Card สวยงาม (title, description, color, fields, footer) |
| actions | Array | ทางเลือก | ปุ่มกดลิงก์ภายนอก (รองรับเฉพาะสไตล์ LINK สำหรับเปิดหน้าเว็บระบบงานภายนอก • ปัจจุบันไม่รองรับปุ่มกดโต้ตอบ/รีแอ็คชัน) |
/api/v1/users/lookup — ตรวจสอบการผูกบัญชี SSO กับ Discord
ตรวจสอบว่าบุคลากรคนดังกล่าวได้ยืนยันตัวตนในระบบ Discord หรือยัง ก่อนที่จะส่งการแจ้งเตือน
/api/v1/bots/status — ดูสถานะคลัสเตอร์บอท
ตรวจสอบสถานะความพร้อม (Online / Standby / Latency) ของบอททุกตัวในเครือข่าย
4. Official Drop-in SDK (ไฟล์เดียวจบ ไม่ต้องติดตั้ง Library เพิ่ม)
ดาวน์โหลดไฟล์ SDK ภาษาที่ระบบลูกของท่านใช้งาน วางไว้ในโปรเจกต์ แล้ว Require/Import ใช้งานได้ทันที มีฟังก์ชันครอบคลุมทั้งส่งแจ้งเตือน ตรวจสอบ SSO และรับ Webhook Verification:
PHP Standalone SDK
ไฟล์เดียวจบ MyWellnetGateway.php ไม่ต้องมี Composer รองรับ PHP 7.4 - 8.3+ เหมาะสำหรับทั้งเว็บดั้งเดิมและ Laravel
Node.js SDK
ไฟล์เดียวจบ mywellnet-gateway.js ใช้ Native Fetch & Crypto ไม่ต้องมี dependencies เพิ่ม พร้อม Express Webhook Middleware
Python SDK
ไฟล์เดียวจบ mywellnet_gateway.py ใช้ Standard Library ล้วน (urllib/hmac) ไม่ต้อง pip install อะไรเลย
ตัวอย่างโค้ดการเรียกใช้งานและโครงสร้าง Payload
# กำหนดตัวแปร APP_BASE_URL จากไฟล์ .env
curl -X POST "$APP_BASE_URL/api/v1/notify" \
-H "X-Client-ID: client_epitrack" \
-H "X-Client-Secret: sec_epi_live_994182" \
-H "Content-Type: application/json" \
-d '{
"target_citizen_id": "1949900123456",
"embed": {
"title": "🚨 มีเคสต้องสงสัยโรคติดต่อใหม่ในพื้นที่",
"description": "พบผู้ป่วยสงสัยไข้เลือดออก 1 ราย ในเขต รพ.สต.บานา กรุณาตรวจสอบและดำเนินการ",
"color": "#EF4444",
"fields": [
{ "name": "รหัสเคส", "value": "EPI-2026-0089", "inline": true },
{ "name": "ระดับความรุนแรง", "value": "เฝ้าระวังเร่งด่วน", "inline": true }
],
"footer": { "text": "ระบบ Epitrack • กองสาธารณสุข อบจ.ปัตตานี" }
},
"actions": [
{ "label": "รับเคสนี้ทันที", "style": "SUCCESS", "action_id": "accept_epi_0089", "emoji": "✅" },
{ "label": "ดูรายละเอียดในเว็ป", "style": "LINK", "url": "https://epitrack.mywellnetptn.org/cases/0089" }
]
}'
<?php
// ตัวอย่างการส่งแจ้งเตือนผ่านภาษา PHP (cURL)
// ดึง Gateway URL จากตัวแปรสภาพแวดล้อม (.env) เป็นหลัก 100%
$gatewayUrl = rtrim(getenv('DISCORD_GATEWAY_URL') ?: getenv('APP_BASE_URL'), '/') . '/api/v1/notify';
$clientId = 'client_epitrack';
$clientSecret = 'sec_epi_live_994182';
$payload = [
'target_citizen_id' => '1949900123456',
'embed' => [
'title' => '🚨 มีเคสต้องสงสัยโรคติดต่อใหม่ในพื้นที่',
'description' => 'พบผู้ป่วยสงสัยไข้เลือดออก 1 ราย ในเขต รพ.สต.บานา',
'color' => '#EF4444',
'fields' => [
['name' => 'รหัสเคส', 'value' => 'EPI-2026-0089', 'inline' => true],
['name' => 'ระดับ', 'value' => 'ด่วนมาก', 'inline' => true]
],
'footer' => ['text' => 'ระบบระบาดวิทยา Epitrack • กองสาธารณสุข อบจ.ปัตตานี']
],
'actions' => [
['label' => 'รับเคสนี้', 'style' => 'SUCCESS', 'action_id' => 'accept_epi_0089', 'emoji' => '✅'],
['label' => 'เปิดดูเคส', 'style' => 'LINK', 'url' => 'https://epitrack.mywellnetptn.org/cases/0089']
]
];
$ch = curl_init($gatewayUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-Client-ID: ' . $clientId,
'X-Client-Secret: ' . $clientSecret
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode === 200 && $result['success']) {
echo "ส่งการแจ้งเตือนสำเร็จ Tracking ID: " . $result['tracking_id'];
} else {
echo "เกิดข้อผิดพลาด: " . ($result['error'] ?? 'Unknown error');
}
?>
// ตัวอย่างการส่งแจ้งเตือนผ่าน Node.js (Fetch API หรือ Axios)
// ยึด DISCORD_GATEWAY_URL หรือ APP_BASE_URL จาก .env เป็นหลัก 100%
const BASE_URL = (process.env.DISCORD_GATEWAY_URL || process.env.APP_BASE_URL || '').replace(/\/+$/, '');
const GATEWAY_URL = `${BASE_URL}/api/v1/notify`;
const CLIENT_ID = 'client_strat';
const CLIENT_SECRET = 'sec_strat_live_771829';
async function sendDiscordNotification(citizenId, taskData) {
const payload = {
target_citizen_id: citizenId,
embed: {
title: '📋 แจ้งเตือนการติดตามโครงการตามแผนยุทธศาสตร์',
description: `โครงการ "${taskData.title}" ถึงกำหนดรายงานผลความก้าวหน้าไตรมาส 2`,
color: '#3B82F6',
fields: [
{ name: 'งบประมาณ', value: taskData.budget + ' บาท', inline: true },
{ name: 'กลุ่มงาน', value: taskData.department, inline: true }
],
footer: { text: 'ระบบแผนยุทธศาสตร์สาธารณสุข • กองสาธารณสุข อบจ.ปัตตานี' }
},
actions: [
{ label: 'บันทึกความก้าวหน้า', style: 'PRIMARY', action_id: `report_${taskData.id}`, emoji: '📝' },
{ label: 'ดูเอกสารโครงการ', style: 'LINK', url: taskData.viewUrl }
]
};
const res = await fetch(GATEWAY_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-ID': CLIENT_ID,
'X-Client-Secret': CLIENT_SECRET
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok || !data.success) {
throw new Error(data.error || 'Failed to send notification');
}
return data; // { success: true, tracking_id: 'mwptn_track_...' }
}
# ตัวอย่างการส่งแจ้งเตือนผ่านภาษา Python (Requests)
# ยึด DISCORD_GATEWAY_URL หรือ APP_BASE_URL จาก .env เป็นหลัก 100%
import os
import requests
BASE_URL = (os.getenv("DISCORD_GATEWAY_URL") or os.getenv("APP_BASE_URL") or "").rstrip("/")
GATEWAY_URL = f"{BASE_URL}/api/v1/notify"
HEADERS = {
"X-Client-ID": "client_kpi",
"X-Client-Secret": "sec_kpi_live_441029",
"Content-Type": "application/json"
}
payload = {
"target_citizen_id": "1949900123456",
"embed": {
"title": "📊 สรุปตัวชี้วัดสาธารณสุขประจำเดือน (KPI Alert)",
"description": "รายงานตัวชี้วัดความครอบคลุมวัคซีน รพ.สต.บานา ประจำเดือนนี้ผ่านเกณฑ์แล้ว",
"color": "#10B981",
"fields": [
{ "name": "ผลงาน", "value": "92.4%", "inline": True },
{ "name": "เป้าหมาย", "value": ">= 90.0%", "inline": True }
],
"footer": { "text": "ระบบตัวชี้วัด KPI • กองสาธารณสุข อบจ.ปัตตานี" }
},
"actions": [
{ "label": "รับทราบผลงาน", "style": "SUCCESS", "action_id": "kpi_ack_2026_03", "emoji": "👍" }
]
}
response = requests.post(GATEWAY_URL, json=payload, headers=HEADERS)
data = response.json()
print("Result:", data)
// ตัวอย่าง Webhook Endpoint รับการกดปุ่มจาก Discord ในระบบลูก (Express.js)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const CLIENT_SECRET = 'sec_epi_live_994182';
app.post('/api/webhook/discord', (req, res) => {
// 1. ตรวจสอบลายเซ็นดิจิทัล HMAC-SHA256 ป้องกันการปลอมแปลง
const signature = req.headers['x-gateway-signature'];
const expectedSig = crypto
.createHmac('sha256', CLIENT_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expectedSig) {
return res.status(401).json({ error: 'INVALID_SIGNATURE' });
}
// 2. ดึงข้อมูลว่าใครเป็นคนกดปุ่ม
const { event, action_id, user } = req.body;
console.log(`เจ้าหน้าที่: ${user.full_name} (ปชช: ${user.citizen_id}) ได้กด: ${action_id}`);
// 3. ปรับปรุงสถานะเคสในฐานข้อมูลระบบลูก
// await updateCaseStatus(action_id, user);
res.json({ success: true, message: 'บันทึกการรับเคสเรียบร้อย' });
});
app.listen(8080, () => console.log('Child Webhook listening on port 8080'));
5. คำสั่งควบคุมบอทสำหรับผู้ดูแลระบบ (Admin Slash Commands) & การกำหนดสิทธิ์ผ่าน MyWellnet Portal
เนื่องจากระบบเกตเวย์ทำงานเป็น One-Way Outbound Notification (ไม่รองรับรีแอ็คชันและการตอบกลับข้อความทั่วไปจาก User) บอทจึงมีชุดคำสั่งควบคุมและดูแลความเรียบร้อยบน Discord (Slash Commands) เฉพาะสำหรับผู้ดูแลระบบเท่านั้น โดยสามารถควบคุมสิทธิ์การใช้งานของแต่ละบุคคลได้อย่างละเอียดผ่านระบบกลาง MyWellnet:
ล้างข้อความในช่องทางประสานงานที่ระบุ (ตั้งแต่ 1 ถึง 100 ข้อความ) เพื่อรักษาความเป็นระเบียบและลบข้อมูลการแจ้งเตือนเก่าที่เสร็จสิ้นภารกิจแล้ว พร้อมรายงานสรุปจำนวนข้อความที่ถูกลบ
ตรวจสอบความพร้อมของบอทแบบเรียลไทม์: สภาวะตื่น/หลับ (Live Status), WebSocket Latency / Ping (ms), ระยะเวลาออนไลน์ Uptime, ปริมาณการใช้ RAM/Heap, สถานะการเชื่อมต่อฐานข้อมูล MySQL และ Host Environment พร้อมปุ่มกดรีเฟรชข้อมูลสด
สั่งรีสตาร์ทกระบวนการทำงานของบอทอย่างปลอดภัย (Safe Supervisor Restart) ผ่านระบบ Process Supervisor (app.js) โดยไม่กระทบต่อการทำงานของ Web Server หลัก
สั่งหยุดพักการทำงานของบอท (Safe Standby / Shutdown) โดยระบบ Supervisor จะบันทึกสถานะไว้และไม่รันบอทขึ้นมาใหม่โดยพลการ จนกว่าผู้ดูแลระบบจะสั่งเปิดใช้งานใหม่ผ่านแผงควบคุมหน้าเว็บ Admin Portal
ระบบกำหนดสิทธิ์การใช้คำสั่งบอทผ่าน MyWellnet Admin Portal
ผู้ดูแลระบบสามารถเข้าจัดการสิทธิ์คำสั่งบอทได้ที่หน้า Admin Portal (/admin.html) ผ่านปุ่ม "กำหนดสิทธิ์คำสั่งบอท" โดยมีกลไกความปลอดภัยดังนี้:
- กำหนดสิทธิ์รายบุคคลและรายคำสั่ง: ระบุ Discord User ID และเลือกได้ว่าจะให้ใช้คำสั่งใดได้บ้าง (
all,clear,status,restart,stop) ข้อมูลจะบันทึกในตารางbot_admin_permissionsทันที - การตรวจสอบสิทธิ์แบบหลายระดับ (Multi-tier Security Guard): เมื่อมีการเรียกใช้คำสั่ง ระบบจะตรวจสอบสิทธิ์จาก Environment SuperAdmin → Config Admin → ตารางสิทธิ์กลาง MyWellnet → Administrator ของ Discord Server
- การปฏิเสธการเข้าถึง (Access Denied Guard): หากสมาชิกทั่วไปหรือผู้ที่ไม่มีสิทธิ์พยายามใช้คำสั่ง บอทจะปฏิเสธการทำงานและส่งการแจ้งเตือนเตือนความปลอดภัยเฉพาะบุคคล (Ephemeral Message) ทันที
- ความปลอดภัยของโทเค็น (Token Safety): การส่ง Ping หรือ Heartbeat เฝ้าระวังบอท ไม่มีผลทำให้โทเค็นของบอทถูกรีเซ็ต เนื่องจากเป็นเพียงการตรวจสอบสถานะเชื่อมต่อตามมาตรฐาน WebSocket ธรรมดา
6. ระบบส่งประกาศ & บรอดแคสต์ (Discord Broadcast API)
รองรับการส่งข้อความประกาศแจ้งเตือนหมู่ (Bulk Broadcast) เข้าสู่หลายห้อง Discord หรือส่ง Direct Message ถึงบุคลากรหลายคนพร้อมกันตามรายชื่อสังกัด/หน่วยงาน โดยระบบมีกลไก Throttling Delay (250ms) ป้องกันการชน Discord Rate Limit (HTTP 429) โดยอัตโนมัติ
/api/v1/broadcast
ต้องใช้ X-Client-Id / Secret
โครงสร้าง Request Body (JSON)
{
"channels": ["1545347262669000754", "1551842442997661716"],
"departments": ["รพ.สต.บานา", "กองสาธารณสุข"],
"title": "📢 ประกาศเร่งด่วน: ตรวจสอบรายงานผลการดำเนินงาน",
"message": "ขอให้หน่วยงานในสังกัดตรวจสอบความถูกต้องของข้อมูลตัวชี้วัดและส่งรายงานภายในวันนี้ 16:30 น.",
"color": "#2563EB",
"mention_roles": ["@everyone"],
"action_url": "https://mywellnet.pattanipao.go.th/portal/",
"bot_id": "portal"
}
ตัวอย่างการเรียกใช้งานผ่าน cURL
curl -X POST https://mywellnet.pattanipao.go.th/api/v1/broadcast \
-H "Content-Type: application/json" \
-H "X-Client-Id: your_client_id" \
-H "X-Client-Secret: your_client_secret" \
-d '{
"channels": ["1545347262669000754"],
"title": "ประกาศจากศูนย์ข้อมูล",
"message": "ระบบพร้อมให้บริการแล้ว",
"color": "#059669"
}'
ตัวอย่างโค้ด PHP (cURL Implementation)
<?php
$payload = [
'channels' => ['1545347262669000754'],
'departments' => ['รพ.สต.บานา'],
'title' => '📢 แจ้งเตือนประชุมด่วน',
'message' => 'ขอเชิญประชุมติดตามงาน ณ ห้องประชุม อบจ. เวลา 13:30 น.',
'color' => '#7C3AED',
'action_url' => 'https://mywellnet.pattanipao.go.th/'
];
$ch = curl_init('https://mywellnet.pattanipao.go.th/api/v1/broadcast');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Client-Id: ' . $clientId,
'X-Client-Secret: ' . $clientSecret
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE)
]);
$response = curl_exec($ch);
$result = json_decode($response, true);
echo "ส่งสำเร็จ: " . $result['stats']['success'] . "/" . $result['stats']['total'];
?>
7. ระบบเฝ้าระวังสุขภาพบอท & Webhook Alert (Bot Health Sentinel)
แก้ปัญหาบอทหยุดทำงานโดยไม่ทราบสาเหตุ ด้วยระบบ Health Sentinel ที่ทำหน้าที่เสมือนตัวบอทมีชีวิตและรายงานอาการป่วยของตนเองแบบอัตโนมัติผ่าน Discord Webhook แยกอิสระ ทำให้แม้ว่า WebSocket ของบอทจะหลุด หรือติดปัญหา Token เซิร์ฟเวอร์ก็ยังสามารถแจ้งเตือนผู้ดูแลระบบได้ทันที
🟢 บอทตื่นและพร้อมทำงาน (Ready & Online)
แจ้งเตือนเมื่อบอทล็อกอินและเชื่อมต่อ Shard สำเร็จ พร้อมรายงาน Latency Ping (ms) และรายชื่อเซิร์ฟเวอร์ที่ประจำการอยู่
🔴 บอทปิดตัวลง / พักเครื่อง (Shutdown / Restart)
ดักจับสัญญาณ SIGINT / SIGTERM รายงานเหตุผลการหยุดโปรเซส ไม่ว่าจะเป็นการสั่งอัปเดตระบบ หรือเครื่องเซิร์ฟเวอร์รีสตาร์ต
💥 บั๊คและข้อยกเว้นร้ายแรง (Unhandled Bugs & Crash)
ดักจับ Uncaught Exceptions, Unhandled Rejections และ Interaction Failures พร้อมระบุ Stack Trace และชื่อฟังก์ชันที่มีปัญหา
🚫 ปัญหา Token & Privileged Intents
แจ้งเตือนทันทีเมื่อ Token ไม่ถูกต้อง โทเค็นถูก Reset หรือขาดสิทธิ์ Server Members / Message Content Intents พร้อมขั้นตอนแก้ไขแบบละเอียด
ขั้นตอนการตั้งค่า Discord Webhook สำหรับแจ้งเตือน
- เปิด Discord ในเซิร์ฟเวอร์ที่ต้องการ -> เข้าช่องทางสำหรับดูสถานะ เช่น
#bot-health-alert - กดไอคอนฟันเฟือง Edit Channel (แก้ไขช่อง) -> ไปที่เมนู Integrations (การรวมระบบ)
- คลิก Webhooks -> New Webhook (สร้างเว็บฮุก) -> ตั้งชื่อบอท เช่น
Bot Health Doctor - คลิก Copy Webhook URL
- เข้าหน้า Admin Portal (/admin.html) -> ที่แถบเครื่องมือด้านบนคลิก "ตรวจสุขภาพบอท & Alert"
- วาง Webhook URL ลงในช่อง -> กดปุ่ม "🔔 ส่งข้อความทดสอบ (Test Ping)" เพื่อตรวจสอบว่าข้อความเข้าห้องจริง
- เลือกประเภทเหตุการณ์ที่ต้องการให้บอทแจ้งเตือน -> กด "บันทึกการตั้งค่า"
9. แนวทางระบบระบาดวิทยา (EpiTrack Disease Surveillance): Private Tiered DM Only
กำหนด target_type: "user" และระบุเลขประจำตัวประชาชน 13 หลัก target_citizen_id ของเจ้าหน้าที่ผู้รับผิดชอบ ห้ามใช้ channel เพื่อป้องกันข้อมูลผู้ป่วยรั่วไหลสู่ผู้อื่น
ระดับ 1: รพ.สต. พื้นที่เกิดเหตุ เพื่อลงพื้นที่สอบสวนโรคภายใน 24 ชม.
ระดับ 2: สสอ. อำเภอ (ทีม CDCU/SRRT) เพื่อติดตามและสนับสนุนยา/เวชภัณฑ์
ระดับ 3: สสจ. จังหวัด เมื่อเป็นโรคติดต่ออันตรายร้ายแรง หรือเกิด Cluster ระบาด
ส่งหาเจ้าหน้าที่ที่ผูกบัญชี SSO กับ Discord แล้วเท่านั้น หากระบบแจ้งเตือนว่ายังไม่ได้ผูก (404 USER_NOT_LINKED) ให้ระบบแสดงข้อความเตือนบนหน้าเว็บและใช้ช่องทางสำรอง
10. ระบบคิวส่งซ้ำ Webhook Callback & Dead-Letter Queue (DLQ Guarantee)
เมื่อเจ้าหน้าที่คลิกปุ่มใน Discord ระบบเกตเวย์จะส่ง Webhook Callback พร้อมลายเซ็นดิจิทัล HMAC-SHA256 กลับไปยังระบบลูกทันที แต่หากเซิร์ฟเวอร์ระบบลูกขัดข้อง (เช่น HTTP 500, 502, 504 หรือ Timeout) เกตเวย์จะรับประกันความปลอดภัยของข้อมูลด้วยระบบ Retry Queue & DLQ:
Exponential Backoff Retries (5 รอบ)
ระบบจะส่งซ้ำอัตโนมัติตามช่วงเวลา: 1 นาที, 5 นาที, 15 นาที, 30 นาที และ 60 นาที โดยเก็บข้อมูลคิวลงดิสก์อย่างปลอดภัย แม้เซิร์ฟเวอร์เกตเวย์รีสตาร์ตคิวก็ไม่สูญหาย
Dead-Letter Queue (DLQ) & Manual Replay
หากส่งครบ 5 ครั้งแล้วยังไม่สำเร็จ ข้อมูลจะถูกย้ายเข้าสู่ DLQ เพื่อให้ผู้ดูแลระบบตรวจสอบผ่านหน้า Admin Portal (/admin.html) และกดปุ่ม "ส่งซ้ำทันที (Replay Now)" ได้ด้วยตนเอง
11. การแนบไฟล์เอกสาร PDF และภาพถ่ายผ่าน API (`attachments`)
รองรับการแนบไฟล์เอกสาร PDF (เช่น แบบสอบสวนโรคเบื้องต้น) หรือภาพถ่ายรอยโรค/ผื่น/ผลตรวจแล็บ ส่งตรงเข้าสู่ Discord DM โดยอัตโนมัติ:
- ขนาดไฟล์สูงสุด: ไม่เกิน 8 MB ต่อไฟล์ (ตามข้อจำกัด Discord API)
- จำนวนไฟล์: สูงสุดไม่เกิน 5 ไฟล์ ต่อหนึ่งคำขอ
- รูปแบบ: รองรับทั้ง
url(ลิงก์ตรง) และdata(Base64 string พร้อมcontentType)
12. ระบบป้องกัน Discord Rate Limiting & Outbound Pacing Engine
เพื่อป้องกันปัญหา HTTP 429 (Too Many Requests) หรือการถูกระงับ Bot Token ชั่วคราว เมื่อระบบลูกส่งข้อความกระจายตัวพร้อมกันจำนวนมาก ระบบเกตเวย์มี Token-based Serial Queue & Auto-Pacer ในตัว:
Auto-Pacing (80ms spacing)
เว้นระยะการส่งระหว่างข้อความขั้นต่ำ 80ms ต่อโทเค็นโดยอัตโนมัติ เพื่อให้เป็นไปตามข้อกำหนดความปลอดภัยของ Discord API
Auto-Retry on 429 Backoff
หากพบ HTTP 429 ระบบจะอ่าน Retry-After และหน่วงเวลารอให้อัตโนมัติ พร้อมส่งซ้ำสูงสุด 3 ครั้งโดยไม่เกิดข้อผิดพลาดกับระบบลูก
13. มาตรฐานความปลอดภัย HMAC-SHA256 Webhook Verification
เมื่อเจ้าหน้าที่คลิกปุ่มหรือส่งแบบฟอร์ม เกตเวย์จะยิง Callback กลับไปยัง webhook_url ของระบบลูก พร้อม Headers ตรวจสอบความถูกต้อง:
X-Signature-SHA256:sha256=<hash>ที่คำนวณจาก$timestamp . '.' . $rawBodyX-Signature-Timestamp: UNIX Timestamp ป้องกันการโจมตีซ้ำ (Replay Attack ภายใน 5 นาที)X-Client-ID: Client ID ประจำระบบ
14. ระบบประวัติและการตรวจสอบย้อนหลัง Audit Trail & Activity Log (PDPA Compliant)
บันทึกข้อมูลการปฏิบัติงานเพื่อเป็นหลักฐานทางราชการ โดยซ่อนเลขบัตรประชาชน (Data Masking) ตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล:
- สามารถดูและค้นหาประวัติได้ที่หน้า Admin Portal (/admin.html) ปุ่ม "Audit Trail & Log"
- หรือเรียกดูผ่าน REST API สำหรับระบบลูก:
GET /api/v1/audit-logs?status=actioned&limit=50
15. ระบบตรวจสอบสถานะเซิร์ฟเวอร์ Healthcheck & Prometheus Metrics
รองรับการมอนิเตอร์ระดับ Production ผ่าน Uptime Kuma, Zabbix, Grafana และ Prometheus:
GET /health (JSON Healthcheck)
รายงานสถานะ Gateway (UP / DEGRADED), Uptime, จำนวนบอทที่ออนไลน์, จำนวนคิว Webhook และสถานะ Outbound Pacer
GET /metrics (Prometheus Exporter)
ส่งออกข้อมูลมาตรฐาน OpenMetrics เช่น bot_portal_uptime_seconds, bot_portal_cluster_bots_total, bot_portal_rate_limit_hits_total
16. ข้อควรระวังผลกระทบร้ายแรงจากการ Hardcode localhost:300... และมาตรฐานการตั้งค่า .env
การระบุ URL แบบฝังตายตัว (Hardcoded URL) เช่น http://localhost:3000 หรือ http://localhost:3001 ในซอร์สโค้ดของระบบลูก เป็นข้อผิดพลาดระดับวิกฤต (Critical Anti-Pattern) ที่ทำให้ระบบไม่สามารถทำงานได้เมื่อขึ้นโฮสต์จริง (Production Hosting / VPS)
- Client-Side Fetch ล้มเหลว 100%: เมื่อเจ้าหน้าที่เปิดเว็บจากภายนอก เบราว์เซอร์จะพยายามส่งคำขอไปหา
localhostของเครื่องเจ้าหน้าที่เอง เกิดERR_CONNECTION_REFUSEDหน้าจอขาวทันที - ถูกเบราว์เซอร์บล็อกตามกฎ Mixed Content: เว็บจริงเป็น
https://แต่โค้ดเรียกhttp://localhostบราวเซอร์ความปลอดภัยสูงจะบล็อกคำขอทันที - CORS & Content Security Policy (CSP) ปฏิเสธการเข้าถึง: นโยบายความปลอดภัยบนโฮสต์จริงจะตัดการเชื่อมต่อไปยัง localhost ทั้งหมด
- Single Sign-On (SSO) พัง: การ Redirect หลังยืนยันตัวตนจะพาผู้ใช้กลับมาที่ localhost ของเครื่องตัวเอง ทำให้ล็อกอินไม่สำเร็จ
- Webhook 2-Way Callback ส่งกลับไม่ถึง: แม่ข่ายบอทไม่สามารถส่งผลการกดปุ่มกลับไปยัง localhost ของระบบลูกได้
- ไม่รองรับ Docker & Multi-Server: เมื่อแยกโฮสต์ Web Server กับ Gateway ออกจากกัน จะไม่สามารถสื่อสารข้ามเครื่องได้
แนวทางแก้ไขที่ถูกต้อง: บังคับใช้ Environment Variables (.env) 100%
ห้ามมี http://localhost:... ในไฟล์โค้ดใดๆ แม้แต่เป็นค่า fallback ตัวแปร ให้ดึงจาก process.env หรือ getenv() เท่านั้น:
# .env สำหรับระบบลูก (Production)
DISCORD_GATEWAY_URL="https://bot.mywellnet.com"
NEXT_PUBLIC_APP_URL="https://strat.pattani.go.th"
NEXT_PUBLIC_OAUTH_REDIRECT_URI="https://strat.pattani.go.th/api/auth/callback"
DISCORD_GATEWAY_WEBHOOK_SECRET="sec_xxxxxxxxxxxxxxxxxxxx"
17. สถาปัตยกรรมการจัดเก็บ Interactive Webhook Callbacks ลงฐานข้อมูลจริง (MySQL / Prisma)
การบันทึกประวัติการกดปุ่มตอบกลับของเจ้าหน้าที่จาก Discord (เช่น อนุมัติ/ไม่อนุมัติโครงการ) ต้องบันทึกลงฐานข้อมูลจริงแบบถาวร (Persistent Storage) ห้ามพึ่งพาไฟล์ชั่วคราว JSON ในโฟลเดอร์ทดสอบ เพื่อป้องกันข้อมูลสูญหายและให้สอดคล้องกับระเบียบ พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล (PDPA)
ข้อเสียของการเก็บลงไฟล์ JSON ชั่วคราว
• ข้อมูลสูญหายทันทีเมื่อเซิร์ฟเวอร์รีสตาร์ตหรือ Deploy ใหม่
• เกิด Race Conditions และ File Corruption เมื่อมีผู้ใช้กดปุ่มพร้อมกัน
• ระบบคลัสเตอร์ (PM2 / Load Balancer) แต่ละโหนดจะไม่เห็นไฟล์ของกันและกัน
• ขาด Audit Trail ทางราชการ ไม่สามารถสืบค้นย้อนหลังได้จริง
ข้อดีของ Hybrid Persistence (MySQL + Cache)
• บันทึกลงตารางจริง discord_callback_logs ข้อมูลไม่มีวันสูญหาย
• รองรับ Concurrent Users สูงผ่าน Database Transactions
• แคช 30 รายการล่าสุดใน Memory เพื่อให้หน้าบ้านแสดงผล Real-time Zero Latency
• มี Index บน citizen_id, tracking_id และ created_at ค้นหาได้ทันที
โครงสร้าง Prisma Schema (MySQL DDL)
model DiscordCallbackLog {
id Int @id @default(autoincrement())
trackingId String? @map("tracking_id")
actionId String @map("action_id")
event String @default("interaction.action")
interactionType String @default("BUTTON") @map("interaction_type")
discordId String? @map("discord_id")
discordTag String? @map("discord_tag")
citizenId String? @map("citizen_id")
fullName String? @map("full_name")
departmentName String? @map("department_name")
formData String? @db.Text @map("form_data")
resultMessage String? @db.Text @map("result_message")
createdAt DateTime @default(now()) @map("created_at")
@@index([citizenId])
@@index([trackingId])
@@index([createdAt])
@@map("discord_callback_logs")
}