iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Modern Web

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

Day 11:LINE Bot Text Emoji 與 Sticker 回覆

  • 分享至 

  • xImage
  •  

2025 鐵人賽背景圖

本日程式碼的範例連結

前言

目前我們的 LINE Bot 主要使用 Text 方式回覆訊息。今天將探討 Text 類型的 Emoji 應用,以及 Sticker 回覆方式,基於 Day 8 版本進行改寫。

除了基礎的六種回覆類型外,LINE Bot 還提供了三種進階訊息回覆機制:

  • ImageMap:可點擊區域的圖片訊息
  • Template:結構化的範本訊息
  • Flex:高度客製化的彈性版面訊息

Text Message(v1) 搭配 Emoji 回覆方式

我們將先實作 v1 版本,再介紹 v2 版本的優勢。透過比較兩個版本的差異,了解為什麼需要升級到 v2,讓 LINE Bot 在回覆訊息時有更好的控制能力。

在開始之前,我們先對先前的版本進行重構。原先 line-webhook.module 負責太多職責,現在將其職責聚焦在接收 LINE 平台事件。

我們將把訊息格式處理獨立成專門的模組,這樣的架構有以下優勢:

  • 職責分離更清楚
  • 未來使用 Push 方式發送訊息時,也能複用訊息處理邏輯
  • 透過訊息處理模組的介面,可以更好地限制和驗證使用者參數

Step 1:輸入指令建立獨立訊息處理模組

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

Step 2:新增自定義 textMessageReq 型別

官方提供 LINE Emoji 清單

LINE Text Message 不論是 v1 或 v2 版本,都支援發送以下兩種類型的表情符號:

  • LINE Emoji:LINE 官方提供的表情符號包
  • Unicode Emoji:標準 Unicode 表情符號(如 😀、❤️、🎉 等)

Text Message v1 使用官方提供的 LINE Emoji 時,必須透過 emoji 物件來指定:

  • productId:用於指定使用哪一個 LINE Emoji
  • emojiId:用於指定該包中的特定表情符號
  • index:指定表情符號在文字中的插入位置

我們可以在 line-message資料夾中新增 types 資料夾,用於存放自定義的型別定義。

line-message/types/text-message.ts

假設我們這邊定義三個主題包的 Emoji 001 ~ 009 可以使用的情境!否則每次都要查詢productId

// 假設我們只有使用到 001 ~ 009
type Emojis = ['001', '002', '003', '004', '005', '006', '007', '008', '009'];

/**
 * LINE Emoji Project IDs
 * - 表情符號系列(可愛臉部表情):670e0cce840a8236ddd4ee4c
 * - 派對帽、蛋糕、禮物等慶祝主題:5ac2213e040ab15980c9b447
 * - 彩色英文字母 A~I:5ac21a8c040ab15980c9b43f
 */

type ProjectIds = [
  '670e0cce840a8236ddd4ee4c',
  '5ac2213e040ab15980c9b447',
  '5ac21a8c040ab15980c9b43f',
];

export interface TextMessageReq {
  text: string;
  emoji?: {
    index: number;
    productId: ProjectIds[number];
    emojiId: Emojis[number];
  };
}

函數採用 TextMessageReq 進行封裝,在使用時,IDE 會自動提供提示,顯示所有可用的輸入選項。如需了解特定參數 ID 的詳細定義,可直接看 textMessageReq 型別定義中的 JSDoc 說明文檔,以了解完整的參數說明。

TextMessageReq IDE 提醒

Step 3:封裝 LINE Text Message 訊息處理流程

方便後續可以加上預設值、驗證、客製化處理等機制!

Text(v1) 版本當中,可以把 $ 字符替換成對應的官方 Emoji 。雖然這是可選欄位,但我們可以調整函式,讓使用者只需插入一個 Emoji 就能完成。

Emoji 插入位置超出原始字串長度時,我們使用 ~ 字符補足字串長度作為占位符,確保索引位置有效,避免發送訊息時出現錯誤。

line-message/line-message.service.ts

// 略
export class LineMessageService {
    createTextMessage(textMessageReq: TextMessageReq): TextMessage {
    const { text, emoji } = textMessageReq;

    let modifiedText = text;

    // 如果有 Emoji 幫我插入指定位置
    if (emoji) {
      const emojiIndex = emoji.index;
      const placeholderChar = '~';
      const textArr = Array.from(text.padStart(emojiIndex, placeholderChar));
      textArr.splice(emojiIndex, 0, '$ ');
      modifiedText = textArr.join('');
    }

    const textMessage: TextMessage = {
      type: MessageType.Text,
      text: modifiedText,
      ...(emoji && {
        emojis: [
          {
            index: emoji.index,
            productId: emoji.productId,
            emojiId: emoji.emojiId,
          },
        ],
      }),
    };

    return textMessage;
  }
}

Step 4:將 line-message.service 匯出

匯出 LineMessageService,讓載入 LineMessageModule 的模組可以使用此服務

line-message/line-message.module.ts

// 略

@Module({
  providers: [LineMessageService],
  exports: [LineMessageService],
})
export class LineMessageModule {}

Step 5:匯入 line-message.service

line-webhook/line-webhook.module.ts

// 略

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

Step 6:初始化 line-message 服務

注入模組後可以透過初始化的 lineMessageService 實例操作創建的方法

import { LineMessageService } from 'src/line-message/line-message.service';

export class LineWebhookService {
  constructor(
    private readonly lineMessageService: LineMessageService,
  ) {}
}

Step 7:調整 MessageEventHandlerMap 型別

Message 型別是 LINE 發送訊息所有型別的聯合型別

確保最終產生結果的型別符合 LINE 發送訊息所需的 Message 格式。未來這個格式除了可以應用在 LINE Reply Message 之外,也可以搭配 LINE Push Message 的方式使用。

line-webhook/line-webhook.type.ts

export type MessageEventHandlerMap = {
  [K in EventMessage['type']]: (
    event: Extract<EventMessage, { type: K }>,
  ) => Extract<Message, { type: K }>;
};

Step 8:改寫 messageEventHandleMap 使其搭配 line-message.service 使用

結合 line-message.service 處理 textMessage(v1) 格式,並根據是否需要 Emoji 自動切換處理:

  • 不需要 Emoji 時,只需提供 text 回覆文字
  • 需要 Emoji 時可指定插入位置(目前限制只能插入一個 Emoji)

line-webhook/line-webhook.service.ts

private async handleMessageEvent(event: MessageEvent): Promise<void> {
     const messageEventHandlerMap = {
      text: (message) =>
        this.lineMessageService.createTextMessage({
          text: message.text,
          emoji: {
            index: 0,
            productId: '5ac21c4e031a6752fb806d5b',
            emojiId: '006',
          },
        }),
    } satisfies Partial<MessageEventHandlerMap>;
    
    let replyMessage;
    const handler = messageEventHandlerMap[event.message.type];
    // 這邊記得移除 await !!!
    if (handler) replyMessage = handler(event.message);

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

成果展現(有給 emoji 的情形)

LINE textMessage v1 行動裝置呈現

TextMessage(v2) Emoji 使用方式比較

  • LINE TextMessage(v2) 官方說明

Emoji 為例,置放方式不再像 v1 需要指定 index,而是直接使用 {laugh} 置換字串特定位置,使用上更加方便。

v2 相對於 v1 有三大特色:

  1. 支援動態內容插入
  2. 使用 {} 語法包圍變數( tag 的效果在群組可以發揮效果)
    • {laugh} 代表 emoji
    • {user1} 代表 tag 特定用戶
    • {everyone} 代表 tag 所有人
  3. 可與 emojimentions 混合在字串中使用

line-message/line-message.service

  createTextMessageV2(textMessageReq: TextMessageReq): TextMessageV2 {
    const { text, emoji } = textMessageReq;

    const modifiedText = `{laugh} ${text} {laugh}`; // Emoji 改使用 {laugh} 替換

    const textMessage: TextMessageV2 = {
      type: MessageType.TextV2,
      text: modifiedText,
      ...(emoji && {
        substitution: {
          laugh: {
            type: 'emoji',
            productId: emoji.productId,
            emojiId: emoji.emojiId,
          },
        },
      }),
    };

    return textMessage;
  }

sticker 貼圖訊息 - 型別定義 & 函式定義

  • LINE 官方列出的貼圖包及貼圖說明
  • LINE Sticker Message 官方說明

這部分的處理跟文字和 Emoji 相似,但 Emoji 有 Unicode Emoji 可以選擇,而貼圖透過 API 只能使用 LINE 官方列出的貼圖包編號(packageId)貼圖編號(stickerId)

Step 1:新增自定義 StickerMessageReq 型別

處理思路與文字 Emoji 相同,都是希望能限制使用的貼圖包跟貼圖區間,並搭配 IDE 型別提示加速開發

line-message/types/sticker-message.ts

export const stickerIds = [
  {
    packageId: '446',
    stickerIds: ['1988', '1989', '1990', '1991', '1992'],
  },
  {
    packageId: '789',
    stickerIds: ['10855', '10856', '10857', '10858', '10859'],
  },
] as const;

export type StickerMap = {
  [K in (typeof stickerIds)[number]['packageId']]: Extract<
    (typeof stickerIds)[number],
    { packageId: K }
  >['stickerIds'][number][];
};

export type StickerMessageReq = {
  [K in keyof StickerMap]: {
    packageId: K;
    stickerId: StickerMap[K][number];
  };
}[keyof StickerMap];

Step 2:封裝 LINE Sticker Message 訊息處理流程

使用者只需要決定要發送哪個貼圖包及貼圖編號

line-webhook/line-webhook.service

private async handleMessageEvent(event: MessageEvent): Promise<void> {
    const messageEventHandlerMap = {
      sticker: () =>
        this.lineMessageService.createStickerMessage({
          packageId: '6359',
          stickerId: '11069850',
        }),
    } satisfies Partial<MessageEventHandlerMap>;
}

選擇不同的貼圖包出現的 stickerId 提示也會不同,感覺開發使用上會方便許多!!
LINE Sticky貼圖型別提示

成果展現

LINE Bot 貼圖回覆成果展示

本日結語

畫了兩天的圖文選單,回歸到 LINE Message 的回覆上逐一說明。雖然 LINE Bot 文字回覆搭配 Emoji 跟貼圖回覆在實務上都很少見,送訊息通常會想要更多效果,一般會使用 Flex MessageImageMap Message 更為常見,但是能更深入理解 LINE Bot 還是很開心!

今天的部分,主要也是想了很久,思考在要發送貼圖有沒有什麼辦法可以有提示!!想了很久,或許這反而花更多時間,但是目前這個方式還算滿意,如果你有更棒的想法也歡迎分享。


上一篇
Day 10:LINE Rich Menu Switch Action 提升使用者體驗
系列文
Line Bot × NestJS:30 天開發日記11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言