import type { messagingApi } from "@line/bot-sdk"; type TemplateMessage = messagingApi.TemplateMessage; type ConfirmTemplate = messagingApi.ConfirmTemplate; type ButtonsTemplate = messagingApi.ButtonsTemplate; type CarouselTemplate = messagingApi.CarouselTemplate; type CarouselColumn = messagingApi.CarouselColumn; type ImageCarouselTemplate = messagingApi.ImageCarouselTemplate; type ImageCarouselColumn = messagingApi.ImageCarouselColumn; type Action = messagingApi.Action; /** * Create a confirm template (yes/no style dialog) */ export function createConfirmTemplate( text: string, confirmAction: Action, cancelAction: Action, altText?: string, ): TemplateMessage { const template: ConfirmTemplate = { type: "confirm", text: text.slice(0, 240), // LINE limit actions: [confirmAction, cancelAction], }; return { type: "template", altText: altText?.slice(0, 400) ?? text.slice(0, 400), template, }; } /** * Create a button template with title, text, and action buttons */ export function createButtonTemplate( title: string, text: string, actions: Action[], options?: { thumbnailImageUrl?: string; imageAspectRatio?: "rectangle" | "square"; imageSize?: "cover" | "contain"; imageBackgroundColor?: string; defaultAction?: Action; altText?: string; }, ): TemplateMessage { const hasThumbnail = Boolean(options?.thumbnailImageUrl?.trim()); const textLimit = hasThumbnail ? 160 : 60; const template: ButtonsTemplate = { type: "buttons", title: title.slice(0, 40), // LINE limit text: text.slice(0, textLimit), // LINE limit (60 if no thumbnail, 160 with thumbnail) actions: actions.slice(0, 4), // LINE limit: max 4 actions thumbnailImageUrl: options?.thumbnailImageUrl, imageAspectRatio: options?.imageAspectRatio ?? "rectangle", imageSize: options?.imageSize ?? "cover", imageBackgroundColor: options?.imageBackgroundColor, defaultAction: options?.defaultAction, }; return { type: "template", altText: options?.altText?.slice(0, 400) ?? `${title}: ${text}`.slice(0, 400), template, }; } /** * Create a carousel template with multiple columns */ export function createTemplateCarousel( columns: CarouselColumn[], options?: { imageAspectRatio?: "rectangle" | "square"; imageSize?: "cover" | "contain"; altText?: string; }, ): TemplateMessage { const template: CarouselTemplate = { type: "carousel", columns: columns.slice(0, 10), // LINE limit: max 10 columns imageAspectRatio: options?.imageAspectRatio ?? "rectangle", imageSize: options?.imageSize ?? "cover", }; return { type: "template", altText: options?.altText?.slice(0, 400) ?? "View carousel", template, }; } /** * Create a carousel column for use with createTemplateCarousel */ export function createCarouselColumn(params: { title?: string; text: string; actions: Action[]; thumbnailImageUrl?: string; imageBackgroundColor?: string; defaultAction?: Action; }): CarouselColumn { return { title: params.title?.slice(0, 40), text: params.text.slice(0, 120), // LINE limit actions: params.actions.slice(0, 3), // LINE limit: max 3 actions per column thumbnailImageUrl: params.thumbnailImageUrl, imageBackgroundColor: params.imageBackgroundColor, defaultAction: params.defaultAction, }; } /** * Create an image carousel template (simpler, image-focused carousel) */ export function createImageCarousel( columns: ImageCarouselColumn[], altText?: string, ): TemplateMessage { const template: ImageCarouselTemplate = { type: "image_carousel", columns: columns.slice(0, 10), // LINE limit: max 10 columns }; return { type: "template", altText: altText?.slice(0, 400) ?? "View images", template, }; } /** * Create an image carousel column for use with createImageCarousel */ export function createImageCarouselColumn(imageUrl: string, action: Action): ImageCarouselColumn { return { imageUrl, action, }; } // ============================================================================ // Action Helpers (same as rich-menu but re-exported for convenience) // ============================================================================ /** * Create a message action (sends text when tapped) */ export function messageAction(label: string, text?: string): Action { return { type: "message", label: label.slice(0, 20), text: text ?? label, }; } /** * Create a URI action (opens a URL when tapped) */ export function uriAction(label: string, uri: string): Action { return { type: "uri", label: label.slice(0, 20), uri, }; } /** * Create a postback action (sends data to webhook when tapped) */ export function postbackAction(label: string, data: string, displayText?: string): Action { return { type: "postback", label: label.slice(0, 20), data: data.slice(0, 300), displayText: displayText?.slice(0, 300), }; } /** * Create a datetime picker action */ export function datetimePickerAction( label: string, data: string, mode: "date" | "time" | "datetime", options?: { initial?: string; max?: string; min?: string; }, ): Action { return { type: "datetimepicker", label: label.slice(0, 20), data: data.slice(0, 300), mode, initial: options?.initial, max: options?.max, min: options?.min, }; } // ============================================================================ // Convenience Builders // ============================================================================ /** * Create a simple yes/no confirmation dialog */ export function createYesNoConfirm( question: string, options?: { yesText?: string; noText?: string; yesData?: string; noData?: string; altText?: string; }, ): TemplateMessage { const yesAction: Action = options?.yesData ? postbackAction(options.yesText ?? "Yes", options.yesData, options.yesText ?? "Yes") : messageAction(options?.yesText ?? "Yes"); const noAction: Action = options?.noData ? postbackAction(options.noText ?? "No", options.noData, options.noText ?? "No") : messageAction(options?.noText ?? "No"); return createConfirmTemplate(question, yesAction, noAction, options?.altText); } /** * Create a button menu with simple text buttons */ export function createButtonMenu( title: string, text: string, buttons: Array<{ label: string; text?: string }>, options?: { thumbnailImageUrl?: string; altText?: string; }, ): TemplateMessage { const actions = buttons.slice(0, 4).map((btn) => messageAction(btn.label, btn.text)); return createButtonTemplate(title, text, actions, { thumbnailImageUrl: options?.thumbnailImageUrl, altText: options?.altText, }); } /** * Create a button menu with URL links */ export function createLinkMenu( title: string, text: string, links: Array<{ label: string; url: string }>, options?: { thumbnailImageUrl?: string; altText?: string; }, ): TemplateMessage { const actions = links.slice(0, 4).map((link) => uriAction(link.label, link.url)); return createButtonTemplate(title, text, actions, { thumbnailImageUrl: options?.thumbnailImageUrl, altText: options?.altText, }); } /** * Create a simple product/item carousel */ export function createProductCarousel( products: Array<{ title: string; description: string; imageUrl?: string; price?: string; actionLabel?: string; actionUrl?: string; actionData?: string; }>, altText?: string, ): TemplateMessage { const columns = products.slice(0, 10).map((product) => { const actions: Action[] = []; // Add main action if (product.actionUrl) { actions.push(uriAction(product.actionLabel ?? "View", product.actionUrl)); } else if (product.actionData) { actions.push(postbackAction(product.actionLabel ?? "Select", product.actionData)); } else { actions.push(messageAction(product.actionLabel ?? "Select", product.title)); } return createCarouselColumn({ title: product.title, text: product.price ? `${product.description}\n${product.price}`.slice(0, 120) : product.description, thumbnailImageUrl: product.imageUrl, actions, }); }); return createTemplateCarousel(columns, { altText }); } // ============================================================================ // ReplyPayload Conversion // ============================================================================ import type { LineTemplateMessagePayload } from "./types.js"; /** * Convert a TemplateMessagePayload from ReplyPayload to a LINE TemplateMessage */ export function buildTemplateMessageFromPayload( payload: LineTemplateMessagePayload, ): TemplateMessage | null { switch (payload.type) { case "confirm": { const confirmAction = payload.confirmData.startsWith("http") ? uriAction(payload.confirmLabel, payload.confirmData) : payload.confirmData.includes("=") ? postbackAction(payload.confirmLabel, payload.confirmData, payload.confirmLabel) : messageAction(payload.confirmLabel, payload.confirmData); const cancelAction = payload.cancelData.startsWith("http") ? uriAction(payload.cancelLabel, payload.cancelData) : payload.cancelData.includes("=") ? postbackAction(payload.cancelLabel, payload.cancelData, payload.cancelLabel) : messageAction(payload.cancelLabel, payload.cancelData); return createConfirmTemplate(payload.text, confirmAction, cancelAction, payload.altText); } case "buttons": { const actions: Action[] = payload.actions.slice(0, 4).map((action) => { if (action.type === "uri" && action.uri) { return uriAction(action.label, action.uri); } if (action.type === "postback" && action.data) { return postbackAction(action.label, action.data, action.label); } // Default to message action return messageAction(action.label, action.data ?? action.label); }); return createButtonTemplate(payload.title, payload.text, actions, { thumbnailImageUrl: payload.thumbnailImageUrl, altText: payload.altText, }); } case "carousel": { const columns: CarouselColumn[] = payload.columns.slice(0, 10).map((col) => { const colActions: Action[] = col.actions.slice(0, 3).map((action) => { if (action.type === "uri" && action.uri) { return uriAction(action.label, action.uri); } if (action.type === "postback" && action.data) { return postbackAction(action.label, action.data, action.label); } return messageAction(action.label, action.data ?? action.label); }); return createCarouselColumn({ title: col.title, text: col.text, thumbnailImageUrl: col.thumbnailImageUrl, actions: colActions, }); }); return createTemplateCarousel(columns, { altText: payload.altText }); } default: return null; } } // Re-export types export type { TemplateMessage, ConfirmTemplate, ButtonsTemplate, CarouselTemplate, CarouselColumn, ImageCarouselTemplate, ImageCarouselColumn, Action, };