[LINE bot 好好玩 30 天玩轉 LINE API] 第 8 天:事件處理真簡單

2020 11th 鐵人賽 LINE

本文同步刊載於 iT 邦幫忙第11屆鐵人賽:[LINE bot 好好玩 30 天玩轉 LINE API] 第 8 天:事件處理真簡單

前言

在前面章節我們只有處理特定的 event type 與 event message type (如下),其他的 event type 我們沒有做特別的處理,只有做忽略的動作,這是什麼意思呢?意思是所有的文字訊息我們程式都會處理,而不是文字訊息的都會丟掉,可是平常在聊天的時候有很多的訊息,像是圖片、影片、聲音、貼圖等 ……,那我們的 Bot 難道不能處理嗎?其實是可以的,那要怎麼處理呢?今天就來教大家怎麼處理其他的 event 吧!

迷:你是不是也沒講把之前漏掉的 text message 也講解一下

if (event.type !== 'message' || event.message.type !== 'text') {
    // ignore non-text-message event
    return Promise.resolve(null);
}

Event 處理中心

在這個範例我們把整個 Event 做一個新的處理,讓整體結構變得比較乾淨。

  • 首先是第 2 行,先做一個過濾此行是因應 Developer 的 Webhook URL Verify 的功能
  • 第 5 行進來的 User ID 先做一個 Log 的紀錄
  • 第 7 行用 event type 先做一個分類的動作
  • 第 12 行用 handleText 的 function 來處理 text
  • 14、16、18、20 與 22 都做差不多的事情

下面分別會介紹這些 event 的處理方法

function handleEvent(event) {
  if (event.replyToken && event.replyToken.match(/^(.)\1*$/)) {
    return console.log('Test hook recieved: ' + JSON.stringify(event.message));
  }
  console.log(`User ID: ${event.source.userId}`);

  switch (event.type) {
    case 'message':
      const message = event.message;
      switch (message.type) {
        case 'text':
          return handleText(message, event.replyToken, event.source);
        case 'image':
          return handleImage(message, event.replyToken);
        case 'video':
          return handleVideo(message, event.replyToken);
        case 'audio':
          return handleAudio(message, event.replyToken);
        case 'location':
          return handleLocation(message, event.replyToken, event.source);
        case 'sticker':
          return handleSticker(message, event.replyToken);
        default:
          throw new Error(`Unknown message: ${JSON.stringify(message)}`);
      }
    default:
      throw new Error(`Unknown event: ${JSON.stringify(event)}`);
  }
}

Text Message Event

我們把之前範例的測試指令 測試1 做一個整理,在前幾章我們都是用 if 來判斷,這樣其實看起來閱讀性可能比較差,所以就來整理一下用 switch case 來處理,如下:

詳細的程式碼可以查看 GitHub

function handleText(message, replyToken, source) {
  switch (message.text) {
    case '測試1':
        return client.replyMessage(replyToken, [
          {
            type: 'sticker',
            packageId: '1',
            stickerId: '1'
          }
        ]);

    default:
      console.log(`Echo message to ${replyToken}: ${message.text}`);
      const echo = {
        type: 'text',
        text: message.text
      };
      return client.replyMessage(replyToken, echo);
  }
}

Image Message Event

在今天的章節最精華的應該就是這段了 ?,我們開始處理圖片的部分,首先是存檔我們使用 client.getMessageContent 抓到 stream,然後把這個 stream 存起來,在這邊用了一個叫做 pipe 的方法。

pipe

什麼是 pipe 呢?
我們先看看 Wiki 怎麼說

管道(英語:Pipeline)是一系列將標準輸入輸出連結起來的行程,其中每一個行程的輸出被直接作為下一個行程的輸入。 每一個連結都由匿名管道實現

看了看可能不太理解那我說一個大家可能有看過的是 篩選蛤,工人會把蛤蠣從一個一個不一樣大小的篩網,把蛤蠣做一個分類,不過在這邊的重點不是要說分類這件事情,是要說今天我們會把沒有篩過的蛤蠣 (原資料) 放到篩網 (pipe) 得到一個比較小蛤蠣,之後把剛剛的蛤蠣放到更小的篩網得到我們要的東西,其實這就是一個 pipe 的概念,但是在 pipe 裡面是一個經過的過程,不一定會比較小,有可能後面會變大,這樣大家可能可以比較好了解

中文有時候我們叫它資料流,感覺起來像是水流。

那以下面的範例來看就是 stream -> pipe -> writable

詳細的程式碼可以查看 GitHub

function downloadContent(messageId, downloadPath) {
  return client.getMessageContent(messageId)
    .then((stream) => new Promise((resolve, reject) => {
      const writable = fs.createWriteStream(downloadPath);
      stream.pipe(writable);
      stream.on('end', () => resolve(downloadPath));
      stream.on('error', reject);
    }));
}

Image Message

講完了圖片存檔那我們來進入 handle 來看一下這個 function 主體,首先這個範例我們只想要處理 line 傳過來的圖片,所以第 3 行我們限制 line 不然這邊還有 external 可以用,external 表示 liff 傳過來的圖片,這邊暫時不處理,因為這個範例的需要,我們做一個回傳圖片需要的 object,如此就完成 echo 圖片的動作拉 ~

function handleImage(message, replyToken) {
  let getContent;
  if (message.contentProvider.type === "line") {
    const downloadPath = path.join(process.cwd(), 'public', 'downloaded', `${message.id}.jpg`);

    getContent = downloadContent(message.id, downloadPath)
      .then((downloadPath) => {
        return {
          originalContentUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
          previewImageUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
        };
      });
  } else if (message.contentProvider.type === "external") {
    getContent = Promise.resolve(message.contentProvider);
  }

  return getContent
    .then(({ originalContentUrl, previewImageUrl }) => {
      return client.replyMessage(
        replyToken,
        {
          type: 'image',
          originalContentUrl,
          previewImageUrl,
        }
      );
    });
}

詳細的程式碼可以查看 GitHub

Video Message Event

基本上與 Image 差不多,比較特別的地方是 previewImageUrl 我用了一個 static 的 url 來完成這件事情,比較適合的方法是把下載下來的影片做一個截圖可能會比較適合不過這邊沒有做展示,就留給讀者去完成拉 ~

詳細的程式碼可以查看 GitHub

function handleVideo(message, replyToken) {
  let getContent;
  if (message.contentProvider.type === "line") {
    const downloadPath = path.join(process.cwd(), 'public', 'downloaded', `${message.id}.mp4`);

    getContent = downloadContent(message.id, downloadPath)
      .then((downloadPath) => {
        return {
          originalContentUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
          previewImageUrl: lineImgURL,
        }
      });
  } else if (message.contentProvider.type === "external") {
    getContent = Promise.resolve(message.contentProvider);
  }

  return getContent
    .then(({ originalContentUrl, previewImageUrl }) => {
      return client.replyMessage(
        replyToken,
        {
          type: 'video',
          originalContentUrl,
          previewImageUrl,
        }
      );
    });
}

Audio Message Event

Audio 部分沒有 previewImageUrl,取而代之的是 duration,這是一個比較特別的地方。

function handleAudio(message, replyToken) {
  let getContent;
  if (message.contentProvider.type === "line") {
    const downloadPath = path.join(process.cwd(), 'public', 'downloaded', `${message.id}.m4a`);

    getContent = downloadContent(message.id, downloadPath)
      .then((downloadPath) => {
        return {
          originalContentUrl: baseURL + '/downloaded/' + path.basename(downloadPath),
        };
      });
  } else {
    getContent = Promise.resolve(message.contentProvider);
  }

  return getContent
    .then(({ originalContentUrl }) => {
      return client.replyMessage(
        replyToken,
        {
          type: 'audio',
          originalContentUrl,
          duration: message.duration,
        }
      );
    });
}

詳細的程式碼可以查看 GitHub

Location Message Event

這邊比較特別的是 title,如果不是特別的地標,這個資料會是沒有的,這個地方在處理上要小心

function handleLocation(message, replyToken) {
  return client.replyMessage(
    replyToken,
    {
      type: 'location',
      title: message.title,
      address: message.address,
      latitude: message.latitude,
      longitude: message.longitude,
    }
  );
}

詳細的程式碼可以查看 GitHub

Sticker Message Event

與前面章節介紹的依樣可以回傳的貼圖很有限,只有文件定義的貼圖列表而已,所以基本上只會收到對應的 packageId 與 stickerId,而詳細來說貼圖是什麼,也不太知道 XD
所以感覺這個 Event 也不太需要實作它

function handleSticker(message, replyToken) {
  return client.replyMessage(
    replyToken,
    {
      type: 'sticker',
      packageId: message.packageId,
      stickerId: message.stickerId,
    }
  );
}

詳細的程式碼可以查看 GitHub

結語

LINE 的 Event 很多要完全實作也不太容易,所以其實還是把有需要的東西實作就好,沒有實作的部分直接回傳 200 就可以了,主要還是先看需求再考慮要實作哪些感覺才是一個比較好的做法!