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 收到使用者的訊息類型在單聊大致上分成六種:TextImageVideoAudioLocationSticker

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

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

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

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

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

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

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

NestJS 基礎架構說明

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

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

在 NestJS 中,ModuleControllerService 均以 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 的三個核心概念——ModuleControllerProvider,並將這些概念融入原有架構中,進一步提升了整體設計的品質。

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


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

尚未有邦友留言

立即登入留言