iT邦幫忙

第 11 屆 iThome 鐵人賽

1
Software Development

後端基礎PHP+Mysql & Laravel 30日養成計畫系列 第 35

Day 35 Laravel 實作 LINE Bot 小幫手筆記

前陣子和工作室的夥伴們一起參加了在台中Monospace舉辦的GDG 黑客松,度過了非常精實的兩天一夜(幾乎沒什麼睡都在趕工),不過收穫蠻多的,想在此分享一下我們這組的作品和參賽心得,順便複習一下學到的新東西。
當時在選定主題前,遇到一位有在研究聊天機器人的大大,問我們想不想跟他一起玩玩看,於是我們就決定要在這兩天內做出一隻能根據對話內容來自動查詢ptt二手手機拍賣文章的LINE Bot。
主要運用到的技術有:Line Bot SDK、Dialogfolw、Guzzle和爬蟲,但我這篇只介紹Line Bot的部份。

第一步:創立Channel

登入你的line帳號:https://developers.line.biz/zh-hant/
選擇Message API,看到這個畫面後,把該填的欄位填完。
https://ithelp.ithome.com.tw/upload/images/20200106/20120024Npt9x8adzD.png
填完就會看到你剛創見的Channel 了,點進剛剛建立的Channel。
https://ithelp.ithome.com.tw/upload/images/20200106/20120024xxMMAsLMCq.png
https://ithelp.ithome.com.tw/upload/images/20200106/20120024QhuclG12wU.png

Basic Setting這邊可以找到的Channel ID、Channel secret等等,Messaging API則可以看到Channel的QR Code、Channel access token和設定你的API URL(這邊還沒有域名的可以先使用ngrok測試)。
https://ithelp.ithome.com.tw/upload/images/20200106/201200241AXjHAVLUt.png
然後就能開始寫程式了。

第二步:開始寫程式

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裡。

流程:

  1. 取得line的使用者id和訊息內容
  2. 根據訊息內容回覆對應的問題(此步驟需要串接dialogFlow進行意圖判斷)
  3. 根據使用者的回答儲存搜尋關鍵字:需要蒐集到買/賣手機、地點、手機型號/廠牌、價格區間
  4. 將上述蒐集到的五個關鍵字用guzzle發送request讓爬蟲爬取符合條件的手機拍賣文章
  5. 回傳爬到的文章給使用者

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);
  }

記錄request和response的log

由於測試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": "*"
}

上一篇
Day 34 Laravel Factory:填充假資料
下一篇
Day 36 Laravel Eloquent Relations
系列文
後端基礎PHP+Mysql & Laravel 30日養成計畫36
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言