前陣子和工作室的夥伴們一起參加了在台中Monospace舉辦的GDG 黑客松,度過了非常精實的兩天一夜(幾乎沒什麼睡都在趕工),不過收穫蠻多的,想在此分享一下我們這組的作品和參賽心得,順便複習一下學到的新東西。
當時在選定主題前,遇到一位有在研究聊天機器人的大大,問我們想不想跟他一起玩玩看,於是我們就決定要在這兩天內做出一隻能根據對話內容來自動查詢ptt二手手機拍賣文章的LINE Bot。
主要運用到的技術有:Line Bot SDK、Dialogfolw、Guzzle和爬蟲,但我這篇只介紹Line Bot的部份。
登入你的line帳號:https://developers.line.biz/zh-hant/
選擇Message API,看到這個畫面後,把該填的欄位填完。
填完就會看到你剛創見的Channel 了,點進剛剛建立的Channel。
Basic Setting這邊可以找到的Channel ID、Channel secret等等,Messaging API則可以看到Channel的QR Code、Channel access token和設定你的API URL(這邊還沒有域名的可以先使用ngrok測試)。
然後就能開始寫程式了。
cd進專案目錄下,安裝linebot sdk
$composer require linecorp/line-bot-sdk
安裝串接dialogFlow會用到的bcmath
$sudo apt-get install -y php7.3-bcmath
在專案目錄下加入dialogFlow設定檔
"type": "service_account",
"project_id": "你的dialogflow id",
"private_key_id": "你的dialogflow private key id",
"private_key": "你的dialogflow private key"
"client_email": "你的dialogflow信箱",
"client_id": "你的client id(linebot)",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
然後記得先把LINEBOT_TOKEN和LINEBOT_SECRET放到.env裡。
流程:
Part 1:取得client端資料(使用者id、訊息內容等)
class BotController extends Controller
{
function chat(Request $request)
{
//實體化linebot物件
$httpClient = new CurlHTTPClient(env('LINEBOT_TOKEN'));
$bot = new LINEBot($httpClient, ['channelSecret' => env('LINEBOT_SECRET')]);
//取得使用者id和訊息內容
$text = $request->events[0]['message']['text'];
$user_id = $request->events[0]['source']['userId'];
//透過dialogFlow判斷訊息意圖
$dialog = $this->dialog($text);
$noLimit = !strpos($dialog->content(), 'Vague response');
//將以上拿到的資訊寫進log裡,debug用
Log::info($text);
Log::debug($dialog->content());
Part 2:控制流程
根據使用者的訊息內容來回覆對應訊息(由於一天內趕出來的,code實在有點糙,請見諒)
if (count(Mobile::where('userId', $user_id)->get()) > 0) {
$status = Mobile::where('userId', $user_id)->first()->status;
if ((strpos($dialog->content(), 'Go back to the previous step') && $status != 1)) {
Log::debug("status = " . $status);
$status -= 1;
$user = Mobile::where('userId', $user_id)->first();
$user->update(['text' => $text, 'status' => $status]);
Log::debug("status = " . $status);
}
if ($status == 5) {
$users = Mobile::where('userId', $user_id)->get();
$records = Record::where('userId', $user_id)->get();
$reply = '感謝您的使用';
foreach ($users as $user) {
$user->delete();
}
foreach ($records as $record) {
$record->delete();
}
} elseif ($status == 4) {
if (is_numeric($text) == 0 && $noLimit == 1) {
$reply = "請問您想搜尋的價格為?";
} else {
//將搜尋關鍵字發送到爬蟲那端爬取符合條件的文章(詳細內容見Part 3)
}
} else if ($status == 3) {
if (!strpos($dialog->content(), 'step3-reply labels - custom') && $noLimit == 1) {
$reply = "請問您想要找什麼樣的手機? ex:iphone 6s";
} else {
if (strpos($dialog->content(), 'step3-reply labels - custom')) {
Record::create([
'userId' => $user_id,
'key' => 'title_like',
'value' => $text,
]);
}
$reply = "請問您想搜尋的價格為?";
$user = Mobile::where('userId', $user_id)->first();
$user->update(['text' => $text, 'status' => 4]);
}
} else if ($status == 2) {
if (!strpos($dialog->content(), 'step2-reply county') && $noLimit == 1) {
$reply = "請問您要搜尋的縣市為? ex:台北/台中/台南";
} else {
if (strpos($dialog->content(), 'step2-reply county')) {
Record::create([
'userId' => $user_id,
'key' => 'county_like',
'value' => $text,
]);
}
$reply = "請問您想要找什麼樣的手機? ex:iphone 6s";
$user = Mobile::where('userId', $user_id)->first();
$user->update(['text' => $text, 'status' => 3]);
}
} else if ($status == 1) {
if (!strpos($dialog->content(), 'step1-ask')) {
$reply = "您好,很高興為您服務。請問您想要購買或賣出手機?";
$reply = new TextMessageBuilder($reply);
$bot->replyMessage($request->events[0]['replyToken'], $reply);
return;
} elseif (strpos($dialog->content(), 'step1-ask transaction_buy')) {
$text = "sell";
} elseif (strpos($dialog->content(), 'step1-ask transaction_sale')) {
$text = "buy";
}
$reply = "請問您要搜尋的縣市為? ex:台北/台中/台南";
$user = Mobile::where('userId', $user_id)->first();
$user->update(['text' => $text, 'status' => 2]);
$reply = new TextMessageBuilder($reply);
$bot->replyMessage($request->events[0]['replyToken'], $reply);
Record::create([
'userId' => $user_id,
'key' => 'type',
'value' => $text,
]);
}
} else {
$reply = "您好,很高興為您服務。請問您想要購買或賣出手機?";
$status = 1;
Mobile::create([
'userId' => $user_id,
'text' => $text,
'status' => $status,
]);
}
$reply = new TextMessageBuilder($reply);
$response = $bot->replyMessage($request->events[0]['replyToken'], $reply);
if ($response->isSucceeded()) {
return;
}
}
}
Part 3:爬取符合條件的拍賣文章,並回傳給使用者
if (!is_numeric($text) == 0) {
$price_gte = (int)$text - 2000;
$price_lte = (int)$text + 2000;
Record::create([
'userId' => $user_id,
'key' => 'price_lte',
'value' => $price_lte,
]);
Record::create([
'userId' => $user_id,
'key' => 'price_gte',
'value' => $price_gte,
]);
}
$param = [];
$keywords = Record::where('userId',$user_id)->get()->toArray();
Log::debug($keywords);
foreach ($keywords as $rec) {
$param[$rec['key']] = $rec['value'];
}
$param['_limit'] = 5;
$http = new Client();
$response = $http->request('GET',
'https://ptt-crawler-gdg.herokuapp.com/posts',
['query' => $param]);
$getbody = json_decode($response->getBody()->getContents());
$getbody = array_map(function ($resp) {
try {
return [
'title' => $resp->title,
'url' => $resp->url,
'price' => $resp->price,
];
} catch (ErrorException $e) {
return [
'title' => '',
'url' => '',
'price' => '',
];
}
}, $getbody);
$getbody = array_filter($getbody, function ($p) {
return $p['title'] != '' && $p['price'] != '' && $p['url'] != '';
});
if (count($getbody) > 0) {
$msg = new MultiMessageBuilder();
foreach ($getbody as $reply) {
$_msg = new TextMessageBuilder($reply['title'] . "\n$" . $reply['price'] . ' ' . "\n" . $reply['url']);
$msg->add($_msg);
}
$bot->replyMessage($request->events[0]['replyToken'], $msg);
} else {
$reply = "查無結果";
$reply = new TextMessageBuilder($reply);
$bot->replyMessage($request->events[0]['replyToken'], $reply);
}
$user = Mobile::where('userId', $user_id)->first();
$user->update(['text' => $text, 'status' => 5]);
return response()->json([$getbody]);
串接DialogFlow:
public function dialog($text)
{
$credentials = [env('GOOGLE_APPLICATION_CREDENTIALS')];
$projectName = 'linebot-xvhlqg';
$sessionsClient = new SessionsClient($credentials);
$session = $sessionsClient->sessionName($projectName, uniqid());
$languageCode = 'zh-tw';
// create text input
$textInput = new TextInput();
$textInput->setText($text);
$textInput->setLanguageCode($languageCode);
// create query input
$queryInput = new QueryInput();
$queryInput->setText($textInput);
// get response and relevant info
$response = $sessionsClient->detectIntent($session, $queryInput);
$queryResult = $response->getQueryResult();
$intent = $queryResult->getIntent();
$displayName = $intent->getDisplayName();
return response()->json($displayName);
}
由於測試linebot很難透過postman,要直接在line的聊天室做測試,紀錄request和response和查看錯誤訊息等,就要透過laravel的log功能。
清空log:
cd進儲存log的資料夾,
echo "" > laravel-2019-12-24.log
接著把你想在log中看到的東西寫進去,例如:
$text = $request->events[0]['message']['text'];
Log::info($text);
部署上gcp後踩的第一個坑:
clone完專案後,安裝composer時:
Problem 1
- Installation request for linecorp/line-bot-sdk 4.0.1 -> satisfiable by linecorp/line-bot-sdk[4.0.1].
- linecorp/line-bot-sdk 4.0.1 requires ext-curl * -> the requested PHP extension curl is missing from your system.
安裝composer時,會到你專案底下的composer.json看這個專案需要安裝什麼套件,執行composer install就會一起幫你把這些套件都一併裝好。
而他在裝line-bot-sdk時需要php-curl,然而php-curl並不在composer的管轄範圍內,他是屬於php的套件,所以在composer install前要先自行安裝好。
Solution:
要先裝php-curl,且版本要和instance裝的一致。
若下這個指令
composer require php-http/curl-client
會報以下錯誤:
Problem 1
- The requested PHP extension ext-http * is missing from your system.
Install or enable PHP's http extension.
去laravel專案下的composer.json把這行刪掉就行了
"require": {
"ext-http": "*"
}