ทำแอปคุมเพจผ่าน Graph API สามปลายทางที่ลืมแล้วไม่ผ่านรีวิว
คนส่วนใหญ่ตกรีวิวไม่ใช่เพราะโค้ดหลักไม่ดี แต่เพราะลืมปลายทางสามตัวที่ไม่ได้ทำงานอะไรเลยในระบบของเรา แต่แพลตฟอร์มถือว่าเป็นสัญญาที่ให้ไว้กับผู้ใช้
สรุปใน 1 นาที
- อาการ/ปัญหา
- ระบบหลังบ้านที่อ่านและตอบข้อความเพจ Facebook ตกรีวิวแอป เพราะลืมปลายทางที่บังคับ
- ใครควรอ่าน
- นักพัฒนาที่ต้องเชื่อมระบบกับเพจ Facebook
- ทางแก้แบบสั้น
- ทำปลายทางที่บังคับสามตัวให้ครบ ตรวจลายเซ็นคำขอให้ถูกวิธี และแลกโทเคนสั้นเป็นโทเคนเพจเก็บนอกรากเว็บ
พอดีผมมีงานที่ต้องให้ระบบหลังบ้านอ่านข้อความที่เข้ามาทางเพจ แล้วตอบกลับอัตโนมัติในบางกรณี กับโพสต์ประกาศตามรอบเวลาได้เอง โจทย์แบบนี้มีทางทำที่ถูกต้องอยู่ทางเดียว คือสมัครทำแอปกับแพลตฟอร์ม ขอสิทธิ์ที่ต้องใช้ แล้วส่งเข้ารีวิวให้ผ่านก่อน
ผมเขียนตัวหลักเสร็จภายในวันเดียว แต่ไปตกรีวิวรอบแรกด้วยเรื่องที่ไม่ได้เกี่ยวกับตัวหลักเลย คือปลายทางบังคับสามตัวที่ผมไม่ได้ทำ เพราะตอนอ่านเอกสารครั้งแรกผมนึกว่ามันเป็นของเสริมสำหรับแอปที่เก็บข้อมูลผู้ใช้จริงจัง ซึ่งเข้าใจผิดเต็มๆ
รหัสแอป ค่าลับ โทเคน ชื่อเพจ และโดเมนทั้งหมดในบทความนี้ผมสมมติขึ้นใหม่ทั้งชุด ส่วนโครงสร้างโค้ด ชื่อฟิลด์ และลำดับการทำงาน เป็นของจริงจากงานนั้น
สองเส้นทางที่ต้องเลือกก่อนเขียนโค้ดบรรทัดแรก
ก่อนจะลงมือ ผมแนะนำให้ตัดสินใจเรื่องนี้ก่อน เพราะมันเปลี่ยนงานทั้งก้อน
ใช้บริการของผู้ให้บริการที่ผ่านรีวิวแล้ว
- ได้ใช้งานภายในวันเดียว ไม่ต้องรอรีวิว
- ไม่ต้องดูแลปลายทางบังคับใดๆ เอง
- ข้อความของลูกค้าวิ่งผ่านมือที่สาม
- มีค่าใช้จ่ายรายเดือน และถูกจำกัดว่าทำอะไรได้บ้าง
ทำแอปเอง แล้วส่งเข้ารีวิว
- ข้อมูลอยู่ในระบบเราทั้งหมด ต่ออะไรก็ได้
- ไม่มีค่าบริการรายเดือน
- ต้องมีโดเมนที่เปิดให้เข้าถึงได้จากภายนอก และต้องมีปลายทางบังคับครบ
- รอบรีวิวกินเวลาเป็นวันถึงสัปดาห์ และตกได้
ส่วนตัวผมเลือกทางที่สอง เพราะงานนี้ต้องต่อเข้าระบบหลังบ้านที่มีอยู่แล้ว และไม่อยากให้ข้อความของลูกค้าวิ่งผ่านที่อื่น แต่ถ้าใครทำแค่ตอบอัตโนมัติง่ายๆ ทางที่หนึ่งคุ้มกว่าเยอะ ไม่ต้องฝืน
ทุกอย่างในบทความนี้เป็นการเรียกใช้ส่วนต่อประสานที่แพลตฟอร์มเปิดให้อย่างเป็นทางการ ด้วยโทเคนที่ได้มาจากการที่เจ้าของเพจกดอนุญาตเอง วิธีอื่นที่พยายามเข้าถึงข้อมูลโดยไม่ผ่านช่องทางนี้ ผิดข้อตกลงการใช้งาน และเสี่ยงที่บัญชีของลูกค้าจะโดนระงับ ไม่ใช่แค่แอปของเราเท่านั้น ถ้าจะทำงานให้ลูกค้า ต้องไม่พาลูกค้าไปเสี่ยงแบบนั้น
สามปลายทางที่ลืมแล้วตกรีวิว
เมื่อแอปขอสิทธิ์เข้าถึงข้อมูลของผู้ใช้ แพลตฟอร์มถือว่าเราให้สัญญาสามข้อกับผู้ใช้ และสัญญาสามข้อนั้นต้องมีปลายทางจริงบนโดเมนของเรารองรับ
ตัวแรกต้องรับคำขอแบบ POST ที่มีลายเซ็นกำกับ แล้วตอบกลับเป็น JSON ที่มีสองคีย์คือที่อยู่หน้าสถานะกับรหัสยืนยัน ตัวที่สองคือหน้าที่เปิดด้วยรหัสยืนยันนั้นแล้วต้องแสดงสถานะได้ ตัวที่สามต้องรับคำขอตอนผู้ใช้ถอนสิทธิ์แล้วตอบ 200 กลับไป ถ้าตัวใดตัวหนึ่งตอบผิดรูปแบบ รีวิวตกทั้งชุด และข้อความที่ได้กลับมามักบอกแค่ว่าไม่ผ่าน ไม่ได้บอกว่าตัวไหน
ผมเขียนทั้งสามตัวเป็นไฟล์ PHP ธรรมดาไฟล์ละไม่ถึงสามสิบบรรทัด ใช้เวลารวมไม่ถึงชั่วโมง เทียบกับการรอรีวิวรอบใหม่อีกหลายวันแล้วถือว่าถูกมาก
ตรวจลายเซ็นของคำขอที่ส่งเข้ามา อย่าเชื่อเนื้อความเปล่าๆ
คำขอสองตัวแรกมาพร้อมพารามิเตอร์ชื่อ signed_request ซึ่งเป็นข้อความสองท่อนคั่นด้วยจุด ท่อนหน้าคือลายเซ็น ท่อนหลังคือเนื้อความ ทั้งสองท่อนเข้ารหัสแบบฐานหกสิบสี่ชนิดที่ปลอดภัยกับที่อยู่เว็บ ซึ่งใช้อักขระขีดกลางกับขีดล่างแทนอักขระสองตัวของฐานหกสิบสี่ปกติ
ถ้าเราถอดรหัสท่อนหลังแล้วเชื่อเลยโดยไม่ตรวจลายเซ็น ใครก็ปลอมคำขอมาสั่งลบข้อมูลของคนอื่นได้
<?php
// ตรวจและถอด signed_request — คืน null ถ้าลายเซ็นไม่ตรง
function fb_parse_signed_request($signed_request, $app_secret) {
if (!$signed_request || strpos($signed_request, '.') === false) return null;
list($encoded_sig, $payload) = explode('.', $signed_request, 2);
// base64url -> base64 ปกติ ก่อนถอด
$sig = base64_decode(strtr($encoded_sig, '-_', '+/'));
$data = json_decode(base64_decode(strtr($payload, '-_', '+/')), true);
if (!is_array($data)) return null;
// เซ็นจาก "ท่อนหลังดิบ" ไม่ใช่จากข้อมูลที่ถอดแล้ว
$expected = hash_hmac('sha256', $payload, $app_secret, true);
// เทียบแบบเวลาคงที่ ป้องกันการเดาทีละไบต์จากเวลาที่ใช้ตอบ
if (!hash_equals($expected, $sig)) return null;
return $data;
}
จุดแรกคือเอาข้อมูลที่ถอดรหัสแล้วไปคำนวณลายเซ็นซ้ำ ซึ่งจะไม่มีทางตรง เพราะลายเซ็นเซ็นจากข้อความดิบท่อนหลังก่อนถอด จุดที่สองคือเทียบลายเซ็นด้วยเครื่องหมายเท่ากับสามตัวธรรมดา ซึ่งหยุดเทียบทันทีที่เจอไบต์แรกที่ต่าง ทำให้เวลาที่ใช้ตอบบอกใบ้ได้ว่าเดาถูกไปกี่ไบต์ ต้องใช้ฟังก์ชันเทียบแบบเวลาคงที่เสมอ
ปลายทางรับคำขอลบข้อมูล ต้องตอบสองคีย์
ตัวนี้คือตัวที่คนตกกันมากที่สุด เพราะมันไม่ได้ตอบหน้าเว็บ แต่ต้องตอบ JSON ที่มีโครงตายตัว
<?php
// data-deletion.php — แพลตฟอร์มยิง POST มาเมื่อผู้ใช้กดขอลบข้อมูล
$cfg = require __DIR__ . '/fb_config.php';
require __DIR__ . '/_fbhelp.php';
$data = fb_parse_signed_request($_POST['signed_request'] ?? '', $cfg['app_secret']);
if (!$data || empty($data['user_id'])) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(['error' => 'invalid signed_request']);
exit;
}
$uid = preg_replace('/\D/', '', $data['user_id']); // เหลือแต่ตัวเลข
$code = bin2hex(random_bytes(8)); // รหัสยืนยัน 16 ตัวอักษร
@mkdir(__DIR__ . '/_deletions', 0755, true);
@file_put_contents(__DIR__ . '/_deletions/' . $code . '.json', json_encode([
'user_id' => $uid,
'requested_at' => date('c'),
'status' => 'received',
]));
// ต่อจากนี้คือหน้าที่ของระบบหลังบ้าน: ลบข้อมูลของ $uid ออกจากฐานข้อมูลจริง
header('Content-Type: application/json');
echo json_encode([
'url' => 'https://example.com/deletion-status.php?code=' . $code,
'confirmation_code' => $code,
]);
สิ่งที่ตอบกลับไปต้องหน้าตาแบบนี้เป๊ะ ชื่อคีย์ผิดตัวเดียวคือตก
{
"url": "https://example.com/deletion-status.php?code=9f3c1a7b2e5d4c86",
"confirmation_code": "9f3c1a7b2e5d4c86"
}
หน้าสถานะที่อยู่ปลายลิงก์นั้นไม่ต้องสวย แต่ต้องเปิดได้จริงด้วยรหัสที่ส่งไป และต้องบอกสถานะเป็นภาษาคนอ่าน ผมกรองรหัสด้วยนิพจน์ปกติให้เหลือแต่อักขระฐานสิบหกก่อนเอาไปประกอบเป็นชื่อไฟล์เสมอ เพราะค่านั้นมาจากที่อยู่เว็บ
<?php
// deletion-status.php — หน้าที่ผู้ใช้เปิดจากลิงก์ที่เราส่งกลับไป
header('Content-Type: text/html; charset=utf-8');
// กรองให้เหลือแต่ฐานสิบหก กันการไต่โฟลเดอร์ผ่านชื่อไฟล์
$code = preg_replace('/[^a-f0-9]/', '', $_GET['code'] ?? '');
$f = __DIR__ . '/_deletions/' . $code . '.json';
echo '<meta charset=utf-8><title>สถานะคำขอลบข้อมูล</title>';
if ($code !== '' && is_file($f)) {
echo '<h2>คำขอลบข้อมูล</h2>'
. '<p>รหัสยืนยัน: <b>' . htmlspecialchars($code) . '</b></p>'
. '<p>สถานะ: ได้รับคำขอแล้ว ระบบจะลบข้อมูลที่เกี่ยวข้องกับบัญชีของคุณ</p>'
. '<p>สอบถามเพิ่มเติม [email protected]</p>';
} else {
echo '<h2>ไม่พบคำขอ</h2><p>รหัสไม่ถูกต้องหรือหมดอายุ · [email protected]</p>';
}
ตัวที่สามสั้นที่สุด คือรับคำขอตอนผู้ใช้ถอนสิทธิ์แอป หน้าที่ของเราคือบันทึกไว้ ล้างโทเคนของผู้ใช้คนนั้นออกจากระบบ แล้วตอบ 200
<?php
// deauthorize.php — ผู้ใช้ถอดแอปออกจากบัญชีของตัวเอง
$cfg = require __DIR__ . '/fb_config.php';
require __DIR__ . '/_fbhelp.php';
$data = fb_parse_signed_request($_POST['signed_request'] ?? '', $cfg['app_secret']);
if ($data && !empty($data['user_id'])) {
@file_put_contents(__DIR__ . '/_deauth.log',
date('c') . ' user_id=' . preg_replace('/\D/', '', $data['user_id']) . "\n",
FILE_APPEND);
// ต่อจากนี้: ลบโทเคนและสิทธิ์ของผู้ใช้คนนี้ออกจากระบบจริง
}
http_response_code(200);
echo 'ok';
ก่อนส่งเข้ารีวิว ผมทดสอบทั้งสามตัวด้วยคำสั่งบรรทัดเดียว ให้แน่ใจว่ารหัสสถานะที่ได้เป็นไปตามที่ควรเป็น
# ยิงคำขอลบข้อมูลแบบไม่มีลายเซ็น ต้องได้ 400 ไม่ใช่ 500 และไม่ใช่ 200
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://example.com/data-deletion.php
# หน้าสถานะด้วยรหัสมั่ว ต้องได้ 200 พร้อมข้อความว่าไม่พบคำขอ ไม่ใช่หน้าขาว
curl -s https://example.com/deletion-status.php?code=zzzz | head -c 200
# ถอนสิทธิ์แบบไม่มีลายเซ็น ต้องได้ 200 เปล่าๆ ไม่ใช่ 500
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://example.com/deauthorize.php
แลกโทเคนสั้นเป็นโทเคนเพจ แบบทำครั้งเดียวจบ
ขั้นตอนนี้เป็นจุดที่คนงงกันมากที่สุด เพราะโทเคนมีหลายชนิดและอายุไม่เท่ากัน ลำดับที่ถูกต้องมีสามขั้น
- เจ้าของเพจกดอนุญาตแอป แล้วเราได้ โทเคนผู้ใช้แบบสั้น มา อายุประมาณสองชั่วโมง ใช้ทำงานจริงไม่ได้
- เอาโทเคนสั้นไปแลกเป็น โทเคนผู้ใช้แบบยาว ที่ปลายทาง oauth/access_token โดยส่งรหัสแอปและค่าลับแอปไปด้วย ได้อายุประมาณหกสิบวัน
- เอาโทเคนผู้ใช้แบบยาวไปเรียก me/accounts ซึ่งคืนรายการเพจที่ผู้ใช้คนนั้นเป็นผู้ดูแล พร้อม โทเคนเพจ ของแต่ละเพจมาให้ โทเคนเพจที่ออกจากโทเคนผู้ใช้แบบยาวคือตัวที่ไม่มีวันหมดอายุ
ถ้าข้ามขั้นที่สอง แล้วเอาโทเคนสั้นไปเรียก me/accounts เลย โทเคนเพจที่ได้จะหมดอายุตามโทเคนสั้น ซึ่งแปลว่าระบบจะพังในอีกสองชั่วโมงข้างหน้าโดยที่เราไม่รู้ตัว นี่คือสาเหตุอันดับหนึ่งของอาการที่ว่าเมื่อวานยังใช้ได้อยู่เลย
<?php
// 1) โทเคนผู้ใช้แบบสั้น -> แบบยาว
$ll = fb_api('GET', 'oauth/access_token', [
'grant_type' => 'fb_exchange_token',
'client_id' => $cfg['app_id'],
'client_secret' => $cfg['app_secret'],
'fb_exchange_token' => $short_user_token,
]);
if (empty($ll['access_token'])) {
exit('แลกโทเคนยาวไม่สำเร็จ: ' . json_encode($ll, JSON_UNESCAPED_UNICODE));
}
// 2) โทเคนผู้ใช้แบบยาว -> รายการเพจ พร้อมโทเคนเพจ
$accts = fb_api('GET', 'me/accounts', ['fields' => 'id,name,access_token'], $ll['access_token']);
if (empty($accts['data'])) {
exit('ไม่พบเพจที่เป็นผู้ดูแล หรือขอบเขตสิทธิ์ไม่พอ');
}
$page = $accts['data'][0]; // ถ้ามีหลายเพจ ให้เลือกด้วยรหัสเพจแทนการหยิบตัวแรก
// 3) สมัครรับเหตุการณ์ของเพจเข้าแอป
fb_api('POST', $page['id'] . '/subscribed_apps',
['subscribed_fields' => 'messages,messaging_postbacks,feed'],
$page['access_token']);
ตัวเรียก Graph API ผมทำเป็นฟังก์ชันเดียวใช้ร่วมกันทั้งโปรเจกต์ สำคัญที่มันคืน array เสมอ ไม่โยนข้อผิดพลาดออกมา เพราะผู้เรียกต้องอ่านคีย์ error ได้ตลอด
<?php
function fb_api($method, $path, $params = [], $token = null) {
$ver = 'v25.0'; // ตรึงรุ่นไว้ อย่าใช้เส้นทางที่ไม่ระบุรุ่น
$url = 'https://graph.facebook.com/' . $ver . '/' . ltrim($path, '/');
if ($token) $params['access_token'] = $token;
$ch = curl_init();
if (strtoupper($method) === 'GET') {
curl_setopt($ch, CURLOPT_URL, $url . '?' . http_build_query($params));
} else {
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$res = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($res === false) return ['error' => ['message' => $err ?: 'curl failed']];
$j = json_decode($res, true);
return is_array($j) ? $j : ['error' => ['message' => 'bad json', 'raw' => $res]];
}
การตรึงหมายเลขรุ่นของส่วนต่อประสานเป็นเรื่องที่ผมยืนยันว่าต้องทำ เพราะแพลตฟอร์มปลดระวางรุ่นเก่าเป็นรอบๆ ถ้าไม่ระบุรุ่น วันที่เขาเปลี่ยนรุ่นปริยาย ระบบเราจะเปลี่ยนพฤติกรรมเองโดยที่เราไม่ได้แก้อะไรเลย
โทเคนเพจต้องไม่อยู่ในที่ที่เว็บเสิร์ฟได้
โทเคนเพจที่ไม่มีวันหมดอายุคือของที่มีค่าที่สุดในระบบทั้งก้อน ใครได้ไปคือโพสต์ในนามเพจของลูกค้าได้ และอ่านข้อความในกล่องข้อความของเพจได้ทั้งหมด
อย่าเก็บโทเคนไว้ในโฟลเดอร์ที่เว็บเสิร์ฟ แม้จะตั้งชื่อไฟล์ให้เดายากแค่ไหนก็ตาม เพราะวันที่เว็บเซิร์ฟเวอร์ตีความนามสกุลผิด หรือวันที่มีคนเปิดดูรายชื่อไฟล์ในโฟลเดอร์ได้ ของทั้งหมดหลุดพร้อมกัน ที่ถูกคือเก็บไว้ในโฟลเดอร์ที่อยู่เหนือรากเว็บขึ้นไปหนึ่งชั้น และตั้งสิทธิ์ไฟล์ให้เจ้าของอ่านเขียนได้คนเดียว ถ้าใช้โฮสต์ร่วมที่ย้ายไฟล์ออกไปไม่ได้จริงๆ ให้ปิดโฟลเดอร์ด้วยไฟล์กำหนดค่าของเว็บเซิร์ฟเวอร์ แล้วยังต้องยอมรับว่าชั้นป้องกันบางกว่า
<?php
// fb_config.php — ค่าลับทั้งหมดอ่านจากสภาพแวดล้อม ไม่มีค่าจริงอยู่ในซอร์สที่ commit
return [
'app_id' => getenv('FB_APP_ID'),
'app_secret' => getenv('FB_APP_SECRET'),
'verify_token' => getenv('FB_VERIFY_TOKEN'), // ใช้ตอนจับมือกับ webhook
'api_secret' => getenv('FB_INTERNAL_BEARER') // กันปลายทางภายในของเราเอง
];
// ที่เก็บโทเคนเพจ: อยู่เหนือรากเว็บขึ้นไปหนึ่งชั้น
function fb_token_file() { return dirname(__DIR__) . '/private/page_token.json'; }
ตอนเขียนไฟล์โทเคนครั้งแรก ผมสั่งตั้งสิทธิ์ไฟล์เป็น 0600 ทันทีในบรรทัดถัดจากการเขียน ไม่รอให้ใครมาตั้งให้ทีหลัง เพราะช่วงเวลาระหว่างที่ไฟล์เกิดกับที่สิทธิ์ถูกแก้ คือช่วงที่เพื่อนบ้านบนโฮสต์ร่วมอ่านได้
เหตุการณ์ที่ยิงเข้ามา ต้องตรวจลายเซ็นอีกชั้น
ปลายทางรับเหตุการณ์ทำงานสองแบบในไฟล์เดียว แบบ GET คือการจับมือตอนตั้งค่าครั้งแรก เราต้องเทียบค่าที่ตั้งไว้แล้วส่งค่าท้าทายกลับไปดิบๆ ส่วนแบบ POST คือเหตุการณ์จริง ซึ่งมาพร้อมลายเซ็นในส่วนหัว
<?php
// webhook.php
$cfg = require __DIR__ . '/fb_config.php';
require __DIR__ . '/_fbhelp.php';
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') {
$mode = $_GET['hub_mode'] ?? '';
$tok = $_GET['hub_verify_token'] ?? '';
if ($mode === 'subscribe' && hash_equals($cfg['verify_token'], $tok)) {
header('Content-Type: text/plain');
echo $_GET['hub_challenge'] ?? ''; // ต้องส่งค่าท้าทายกลับดิบๆ ถึงจะผ่าน
exit;
}
http_response_code(403);
exit('verify failed');
}
// POST: เหตุการณ์จริง — ต้องเซ็นจาก "เนื้อความดิบ" ก่อน json_decode
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
$ok = false;
if (strpos($sig, 'sha256=') === 0) {
$expected = 'sha256=' . hash_hmac('sha256', $raw, $cfg['app_secret']);
$ok = hash_equals($expected, $sig);
}
if (!$ok) { http_response_code(403); exit('bad signature'); }
@file_put_contents(__DIR__ . '/_events.log',
json_encode(['at' => date('c'), 'body' => json_decode($raw, true)], JSON_UNESCAPED_UNICODE) . "\n",
FILE_APPEND | LOCK_EX);
http_response_code(200);
echo 'EVENT_RECEIVED';
ตอนทดสอบระยะแรก ผมเขียนบันทึกเหตุการณ์ลงไฟล์ไว้ก่อนโดยยังไม่ปฏิเสธคำขอที่ลายเซ็นไม่ผ่าน เพื่อจะได้เห็นว่าอะไรเข้ามาบ้าง แต่พอขึ้นใช้งานจริงต้องเปลี่ยนเป็นปฏิเสธทันทีเสมอ ไม่งั้นใครก็ยัดเหตุการณ์ปลอมเข้าระบบเราได้ ถ้าเดาที่อยู่ปลายทางถูก
สรุป
งานนี้ส่วนที่ยากไม่ใช่การเรียกส่วนต่อประสาน เพราะเอกสารเขาชัดและตัวอย่างมีครบ ส่วนที่ยากคือของสามอย่างที่ไม่เกี่ยวกับฟีเจอร์ของเราเลย คือปลายทางบังคับสามตัว การตรวจลายเซ็นให้ถูกวิธี และการเก็บโทเคนให้มิดชิด
ส่วนตัวผมสรุปเป็นสามข้อสั้นๆ ข้อแรกทำปลายทางบังคับให้ครบก่อนกดส่งรีวิว เขียนสามไฟล์ใช้เวลาไม่ถึงชั่วโมง แต่รีวิวตกหนึ่งรอบเสียเวลาเป็นสัปดาห์ ข้อสองต้องแลกเป็นโทเคนผู้ใช้แบบยาวก่อนเสมอ แล้วค่อยไปขอโทเคนเพจ ไม่งั้นระบบตายในสองชั่วโมง ข้อสามโทเคนเพจอยู่นอกรากเว็บเท่านั้น เพราะมันคือกุญแจของเพจลูกค้า ไม่ใช่ของเรา
ใครจะเลือกใช้บริการของผู้ให้บริการที่ผ่านรีวิวมาแล้วแทนการทำเองก็ตามสะดวกครับ ถ้าสิ่งที่ต้องการคือตอบข้อความอัตโนมัติเฉยๆ ทางนั้นคุ้มกว่าจริงๆ
ปล. เรื่องที่ผมยังไม่ได้เขียนในรอบนี้คือการรับมือตอนโทเคนเพจใช้ไม่ได้กลางทาง เช่นเจ้าของเพจถอดสิทธิ์ผู้ดูแลของเราออก ตอนนี้ระบบแค่บันทึกข้อผิดพลาดไว้เฉยๆ ซึ่งแปลว่าอาจเงียบไปหลายชั่วโมงกว่าจะมีคนสังเกต ที่ถูกควรตรวจรหัสข้อผิดพลาดแล้วแจ้งเตือนทันที เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย