こんにちは、開発部のレンです。
日本人たちは日本語が上手いですね。
自分の日本語能力を改善したいですが、暇の時でほぼゲームをやっているので、なかなか勉強する時間がなかったです。草
毎日電車で通勤してて、電車の中でゲームできないので、その時間ちょこちょこ勉強するようにしたいと思いました。
そのため、Messenger botを作ってみました。
Facebook Messenger でbotに声をかけると、新しい日本語の文法を返ってくれる感じです。
デモ:
msbot.gif

botの作り方を説明させていただきます

準備

  1. Facebook developer account Facebook for Developers
  2. heroku account heroku
  3. Nodejsのナレッジ
  4. Headless Chrome: puppeteerのナレッジ puppeteer

全体の流れ

Untitled Diagram (3).png

facebook messenger botの設定流れ

スクリーンショット 2019-12-24 午後4.25.07.png

facebookのドキュメントはすごくわかりやすいので、上の6つのステップをすすめていただくと、botが作成できます。
Botの挙動は「Step 3. Webhookを設定する」ステップで決まるので、
この記事は「Webhookを設定する」ステップを詳しく話したいと思います。

Webhook設定ステップ

新しいNode.jsプロジェクトの作成

mkdir messenger-webhook // プロジェクトディレクトリを作成 
cd messenger-webhook // 新しいディレクトリに移動 
touch index.js // 空のindex.jsファイルを作成。
npm init // package.jsonを作成。すべての質問に対してデフォルトをそのまま使用します。
npm install express body-parser --save // express.js httpサーバーフレームワークモジュールをインストールし、// それらをpackage.jsonファイルのdependenciesセクションに追加。
npm install puppeteer --save // web crawlerを実装するため、pupperteerを追加

注意
日本語がうまくcrawlできなかったら、 fonts-ipafont-gothic fonts-ipafont-minchoもインストールしてください
apt-get install fonts-ipafont-gothic fonts-ipafont-mincho

Webhookエンドポイントの追加

エンドユーザーが送ったメッセージはここに届きます

// Creates the endpoint for our webhook 
app.post('/webhook', (req, res) => {  
 
  let body = req.body;

  // Checks this is an event from a page subscription
  if (body.object === 'page') {

    // Iterates over each entry - there may be multiple if batched
    body.entry.forEach(function(entry) {

      // Gets the message. entry.messaging is an array, but 
      // will only ever contain one message, so we get index 0
      let webhook_event = entry.messaging[0];
      console.log(webhook_event);
      
    });

    // Returns a '200 OK' response to all requests
    res.status(200).send('EVENT_RECEIVED');
  } else {
    // Returns a '404 Not Found' if event is not from a page subscription
    res.sendStatus(404);
  }

});

webhook_eventの中に、エンドユーザーが入力したテキストがあります。
webhook_event例:

{ 
  sender: { id: '247802069227****' },
  recipient: { id: '48593652514****' },
  timestamp: 1577172836757,
  message:
   { mid:
      'm_UcB2ELDcx8frRDMMDl92x9b5ZSaR20LWFnrR1GAlt0DE48CLDlsdcVbC8y8AGPixdWO2SbBqBIQ5N44f****',
     text: 'Hello world' }
}

日本語のBotの場合、ユーザーが nextを入力していただくときだけに日本語を返ってくるので、ロジックは以下のようになります

const crawler = require('./crawler');

app.post('/webhook', (req, res) => {

  let body = req.body;

  if (body.object === 'page') {

    body.entry.forEach(function(entry) {

      let webhook_event = entry.messaging[0];
      console.log(webhook_event);

      // Get the sender PSID
      let sender_psid = webhook_event.sender.id;
      console.log('Sender PSID: ' + sender_psid);

      // Handles messages events
      if (webhook_event.message) {
        handleMessage(sender_psid, webhook_event.message); // ここです!
      } 

    });
  }

});

// Handles messages events
function handleMessage(sender_psid, received_message) {
  let response;

  // Check if the message contains text
  if (!received_message.text) {
    return;
  }
  if (received_message.text.toLowerCase() != "next") {
    callSendAPI(sender_psid, {"text": "Type [next] to get a new grammar."});
  }
  else if (received_message.text && received_message.text.toLowerCase() == "next") {
    // 'next'の場合、crawlerを走ります
    (async () => {
      crawler.crawl_grammar(sender_psid, callSendAPI)
      })();
  }
}

https://github.com/DucNguyenVan/bikae/blob/master/index.js

puppeteerを利用し、crawler.crawl_grammarを作る

async function crawl_grammar(sender_psid, callback){
  const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
  const page = await browser.newPage();
  await page.setRequestInterception(true);
  page.on('request', interceptedRequest => {
    if (!interceptedRequest.url().includes('bikae.net'))
      interceptedRequest.abort();
    else
      interceptedRequest.continue();
  });
  // 日本語の文法があるページにアクセスする
  await page.goto('https://bikae.net/ngu-phap/tong-hop-ngu-phap-n2/');
  const grammar_href = await page.evaluate(() => {
    var grammar_selectors = '.entry-content span a';
    // 文法該当リンクを全部取得する
    var sum = document.querySelectorAll(grammar_selectors).length
    const rd_idx = (Math.random(0,sum/100)*100).toFixed();
    // 各文法リンクの中に、一つの文法をrandomで選ぶ、
    return document.querySelectorAll(grammar_selectors)[rd_idx].href;
  })
  // 選ばれたリンクにアクセスし、タイトルをGETする
  await page.goto(grammar_href);
  const grammar = await page.evaluate(() => {
    return document.querySelector('.entry-header .entry-title').textContent
  })
  await browser.close();
  // タイトル - 文法の該当リンクを返す:
  // ~ に基づいて  https://bikae.net/ngu-phap/ngu-phap-n2-ni-motoduite/
  response = {
    "text": `${grammar} \n ${grammar_href}`
  }
  callback(sender_psid, response)
};

https://github.com/DucNguyenVan/bikae/blob/master/crawler.js

残りやりたいこと

nextを入力しないで、定期時間で新しい日本語の文法を送る機能を実装しようと思ってましたが、
facebookのbussiness planではないとできないようですので、断念しました

最後

こちらのbotについて、source codeが公開しましたので、
使ってみたい方はどうぞ