iT邦幫忙

2025 iThome 鐵人賽

DAY 4
2
Modern Web

Line Bot × NestJS:30 天開發日記系列 第 4 篇

Day 4:掌握 LINE Bot 七種訊息類型與 NestJS 架構優化

  • 分享至 

  • xImage
  •  

2025 鐵人賽背景圖

前言

目前我們都只使用 LINE Message Event 中的純文字(text)格式進行回覆,但除了純文字之外,LINE Bot 後端伺服器其實可以接收多種不同類型的訊息。今天就讓我們一起來深入了解這些訊息類型吧!

本文將著重於實現以下核心功能:

  • 解析 LINE 各種 Message Types 訊息回覆
  • NestJS 架構優化:將先前結構重構為模組化設計

本日程式碼的範例連結


後端伺服器能透過 Webhook 收到使用者的訊息類型在單聊大致上分成六種:Text、Image、Video、Audio、Location、Sticker。

這邊埋個小彩蛋,後端伺服器其實也能透過 LINE 平台讀取檔案,比如:PDF 檔案。不過這項功能僅限在群聊環境下使用。

  • 單聊:使用者與官方帳號一對一的溝通。
  • 群聊:讓官方帳號變成群組的一員(這部分需要在 LINE Official Account 後台開啟設定)

後端伺服器支援回傳的訊息類型說明

官方文件已詳細說明各訊息類型的長度限制與檔案大小規範,本表格主要聚焦於各類型間的差異特點。其中最特殊的是 File 類型,雖然在群聊環境下可以接收檔案,但後端伺服器卻無法透過 LINE Messaging API 將檔案回傳給用戶。

前面我們介紹了後端伺服器可以接收七種訊息類型,接下來探討這七種類型是否也能透過後端伺服器回傳給用戶:

訊息類型 情境描述 支援回傳 回傳限制
Text 用戶發送文字訊息時觸發 是 無
Sticker 用戶發送貼圖或表情貼時觸發 是 僅能使用平台提供的貼圖包
Image 用戶發送圖片、照片、截圖等內容時觸發 是 僅接受 JPEG or PNG 格式
Video 用戶發送影片檔案時觸發 是 僅接受 mp4 格式
Audio 用戶發送語音訊息或音檔時觸發 是 僅接受 mp3 或 m4a 格式
File 用戶發送文件檔案時觸發 否 LINE Bot API 不支援檔案回傳
Location 用戶分享地理位置時觸發 是 無

除了上述基本的訊息類型外,後端伺服器實際上還能回傳更多樣化的內容格式。我們將在接下來的章節中,完整介紹 LINE Messaging API 所支援的所有回傳訊息類型。

NestJS 基礎架構說明

在開始今天的實作之前,我們需要將 NestJS 的結構做調整。

先前直接寫在app.controller.ts的方式,主要是希望讓所有人都能用最低的成本,使用 NestJS 架設 LINE Bot Server。接著我們將會透過 Nest 官方的架構說明進行相對應的調整。

在 NestJS 中,Module、Controller 及 Service 均以 Class 為設計核心,透過裝飾器(Decorator)來標註其角色與功能。

等等等....一來感覺就壓力滿滿,試著使用餐廳情境來描述 NestJS 的結構 🍽️

Modules

負責將相關的功能(如 Controller、Provider 等)組織起來,形成獨立且可重用的程式碼單元。

我們可以將 Module 想像成「各個負責不同職能的部門」。每個部門都有明確的職責與分工,例如:

  • 負責烹飪料理的「廚房部門」
  • 負責接待與服務顧客的「外場部門」
  • 負責帳務與經營收支的「財務部門」

這些部門的存在,是為了將性質相近的任務歸類在一起,使每個部門都能專注處理特定類型的事務,確保責任分工明確。

Nest Module 結構

Controllers

主要功能:負責處理傳入的請求(Request)及回覆響應(Response)的資料

Controller 的主要職責是理解並接收使用者的請求,然後將這些請求導向給對應的服務進行處理。

舉例來說,當顧客告訴接待員:「我想要一份白醬義大利麵,內用。」接待員會接收到這項指令,接著將這個需求轉達給廚房的廚師處理。

Nest Controller 結構

Providers

主要功能:Provider 負責提供各種可重複使用的服務,包含多種類型:

  • Value Provider:提供靜態常數或設定值
  • Service Provider:處理複雜的業務邏輯
  • Factory Provider:動態建立和配置服務實例

它們透過依賴注入 (Dependency Injection, DI) 的機制,在不同的模組之間建立功能連結,

可以將它們想像成餐廳裡那些「擁有專業技能的幕後工作者」:他們雖然不會直接面對客人,卻是讓餐廳順利運作的核心。這就像廚師不必自己去研究如何製作所有醬料,只需從專門的醬料提供者那裡獲取現成的醬料服務即可。

Nest Provider 結構


以下透過五個步驟將先前撰寫的結構進行調整,使其更符合單一模組的特性。

Step 1:創立新的 Module、Controller、Service 和 type 檔案

介紹完概念就要把原先寫的程式碼找到他們的家

設置專門處理 Line webhook event 的模組。

輸入以下指令產生 module:

nest g module line-webhook
nest g controller line-webhook --no-spec
nest g service line-webhook --no-spec

在 line-webhook 資料夾底下,新增 line-webhook.types.ts,負責統一管理 LINE Webhook 相關的型別定義。

Step 2:將原先 eventHandler 業務邏輯搬到 service

service 負責將我們統整的業務邏輯進行統一的管理

當需要處理 Message 事件時,交由 handleMessageEvent 函式處理,每個函式都獨立處理一種 WebhookEvent。

line-webhook.service.ts

// 略
@Injectable()
export class LineWebhookService {
  // 略
  async processWebhook(body: WebhookRequestBody): Promise<string> {
    const { events } = body;
    
    // 處理 follow、message、unfollow 三種事件
    const webhookEventHandlerMap = {
      message: (event) => this.handleMessageEvent(event),
      follow: (event) => this.handleFollowEvent(event),
      unfollow: (event) => this.handleUnfollowEvent(event),
    } satisfies Partial<WebhookEventHandlerMap>;

    for (const event of events) {
      const handler = webhookEventHandlerMap[event.type];
      if (handler) await handler(event);
    }

    return 'Webhook processed successfully';
  }

  /**
   * 用戶發送訊息時觸發
   * @param event 訊息事件
   */
  private async handleMessageEvent(event: MessageEvent): Promise<void> {
      // ... 略
  }
}

Step 3:將原先 Controller 設置調整至 line-webhook.controller

所有 Line 平台傳遞的事件都透過 line-webhook.controller 進行請求的處理

line-webhook.controller

//...略
@Controller('webhook')
export class LineWebhookController {
  constructor(private readonly lineWebhookService: LineWebhookService) {}

  @Post()
  async handleWebhook(@Body() body: WebhookRequestBody): Promise<string> {
    return this.lineWebhookService.processWebhook(body);
  }
}

Step 4:封裝 LINE 配置為 NestJS Provider

建立 LINE 配置的集中化管理機制,為後續在各個 Service 中創建 LINE SDK 實例提供統一的配置來源

line.config.ts

import { ClientConfig } from '@line/bot-sdk';
import { ConfigService } from '@nestjs/config';

// 註冊名稱
export const LINE_CONFIG = 'LINE_CONFIG';

// 註冊的常數(這邊讀取的是.env 環境變數裡面的內容)
const lineConfig = (configService: ConfigService): ClientConfig => ({
  channelAccessToken: configService.getOrThrow<string>(
    'LINE_CHANNEL_ACCESS_TOKEN',
  ),
  channelSecret: configService.getOrThrow<string>('LINE_CHANNEL_SECRET'),
});

// 匯出成 NestJS Provider 供依賴注入系統使用
export const LineConfigProvider = {
  provide: LINE_CONFIG,
  useFactory: (configService: ConfigService) => lineConfig(configService),
  inject: [ConfigService],
};

Step 4:將 Line Config 提供給 line-webhook.module

將 LINE Config Provider 註冊到模組中,為後續在 LineWebhookService 中注入配置並創建 LINE SDK 實例做準備

line-webhook.module

//...略
import { LineConfigProvider } from 'config/line.config';

@Module({
  controllers: [LineWebhookController],
  providers: [LineWebhookService, LineConfigProvider],
})
export class LineWebhookModule {}

Step 5:移除 app.controller.ts 完全交由 line-webhook.module 處理

將 /webhook端點的請求都透過 LineWebhookModule 處理

app.module.ts

//...略
@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true }), LineWebhookModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // 針對 webhook 路由使用 LINE 中介軟體驗證
    consumer.apply(LineMiddleware).forRoutes('webhook');
  }
}

Step 6:新增 messageEventHandlerMap 函式處理不同訊息類型

採用與 LINE Webhook 事件處理相同的模式,針對 Message Event 內的六種訊息類型建立對應的處理函式,根據訊息類型回傳不同的回應內容。其中圖片、音檔與影片訊息會根據內容來源(contentProvider)區分為 line 和 external 兩種類型進行不同處理。

line-webhook-service.ts

  private async handleMessageEvent(event: MessageEvent): Promise<void> {
    const messageEventHandlerMap = {
      text: (message) => `📝 收到文字訊息:${message.text}`,
      sticker: (message) =>
        `🎭 收到貼圖訊息 => 貼圖包編號:${message.stickerId}-貼圖編號:${message.packageId}}`,
      image: (message) =>
        `🖼️ 收到圖片訊息 => 訊息編號:${message.id}-圖片來源:${message.contentProvider.type}`,
      video: (message) =>
        `🎬 收到影片訊息 => 訊息編號:${message.id}-影片來源:${message.contentProvider.type}`,
      audio: (message) =>
        `🎵 收到音檔訊息 => 訊息編號:${message.id}-時長:${message.duration} ms-音頻來源:${message.contentProvider.type}`,
      location: (message) =>
        `📍 收到位置訊息 => 地址:${message.address}-精度:${message.longitude}-緯度:${message.latitude}`,
    } satisfies Partial<MessageEventHandlerMap>;

    let replyMessage = '✨ 感謝你的訊息,我們已經收到了!';
    const handler = messageEventHandlerMap[event.message.type];
    if (handler) replyMessage = handler(event.message);

    await this.lineClient.replyMessage({
      replyToken: event.replyToken,
      messages: [{ type: 'text', text: replyMessage }],
    });
  }

LINE 媒體訊息(圖片、音檔與影片訊息)的內容來源類型說明

Line 類型


External 類型

  • 定義:由liff.sendMessages()發送的靜態資源,這部分可參照官方說明
  • 存取方式:可以透過originalContentUrl屬性值存取。

至此,我們已經具備了處理 MessageEvent 事件六種基礎訊息的處理函數!

本日結語

今天我們探討了 LINE Bot 可接收的七種訊息類型,並對 Day 3 建立的基礎架構進行了優化與調整。此外,我們深入理解了 NestJS 的三個核心概念——Module、Controller 和 Provider,並將這些概念融入原有架構中,進一步提升了整體設計的品質。

儘管對於 NestJS 還充滿著各種未知,但是我會嘗試在一個又一個的實作當中,慢慢在實踐中學習,希望能以更輕鬆的方式讓大家認識 LINE Bot 的開發世界。


上一篇
Day 3:Webhook 簽章驗證與訊息回覆策略(Push & Reply)
下一篇
Day 5:整合 Pino Logger 提升 LINE Bot 專案的可維護性
系列文
Line Bot × NestJS:30 天開發日記 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言