| import type { messagingApi } from "@line/bot-sdk"; |
| import type { ReplyPayload } from "../auto-reply/types.js"; |
| import type { FlexContainer } from "./flex-templates.js"; |
| import type { ProcessedLineMessage } from "./markdown-to-line.js"; |
| import type { SendLineReplyChunksParams } from "./reply-chunks.js"; |
| import type { LineChannelData, LineTemplateMessagePayload } from "./types.js"; |
|
|
| export type LineAutoReplyDeps = { |
| buildTemplateMessageFromPayload: ( |
| payload: LineTemplateMessagePayload, |
| ) => messagingApi.TemplateMessage | null; |
| processLineMessage: (text: string) => ProcessedLineMessage; |
| chunkMarkdownText: (text: string, limit: number) => string[]; |
| sendLineReplyChunks: (params: SendLineReplyChunksParams) => Promise<{ replyTokenUsed: boolean }>; |
| createQuickReplyItems: (labels: string[]) => messagingApi.QuickReply; |
| pushMessagesLine: ( |
| to: string, |
| messages: messagingApi.Message[], |
| opts?: { accountId?: string }, |
| ) => Promise<unknown>; |
| createFlexMessage: (altText: string, contents: FlexContainer) => messagingApi.FlexMessage; |
| createImageMessage: ( |
| originalContentUrl: string, |
| previewImageUrl?: string, |
| ) => messagingApi.ImageMessage; |
| createLocationMessage: (location: { |
| title: string; |
| address: string; |
| latitude: number; |
| longitude: number; |
| }) => messagingApi.LocationMessage; |
| } & Pick< |
| SendLineReplyChunksParams, |
| | "replyMessageLine" |
| | "pushMessageLine" |
| | "pushTextMessageWithQuickReplies" |
| | "createTextMessageWithQuickReplies" |
| | "onReplyError" |
| >; |
|
|
| export async function deliverLineAutoReply(params: { |
| payload: ReplyPayload; |
| lineData: LineChannelData; |
| to: string; |
| replyToken?: string | null; |
| replyTokenUsed: boolean; |
| accountId?: string; |
| textLimit: number; |
| deps: LineAutoReplyDeps; |
| }): Promise<{ replyTokenUsed: boolean }> { |
| const { payload, lineData, replyToken, accountId, to, textLimit, deps } = params; |
| let replyTokenUsed = params.replyTokenUsed; |
|
|
| const pushLineMessages = async (messages: messagingApi.Message[]): Promise<void> => { |
| if (messages.length === 0) { |
| return; |
| } |
| for (let i = 0; i < messages.length; i += 5) { |
| await deps.pushMessagesLine(to, messages.slice(i, i + 5), { |
| accountId, |
| }); |
| } |
| }; |
|
|
| const sendLineMessages = async ( |
| messages: messagingApi.Message[], |
| allowReplyToken: boolean, |
| ): Promise<void> => { |
| if (messages.length === 0) { |
| return; |
| } |
|
|
| let remaining = messages; |
| if (allowReplyToken && replyToken && !replyTokenUsed) { |
| const replyBatch = remaining.slice(0, 5); |
| try { |
| await deps.replyMessageLine(replyToken, replyBatch, { |
| accountId, |
| }); |
| } catch (err) { |
| deps.onReplyError?.(err); |
| await pushLineMessages(replyBatch); |
| } |
| replyTokenUsed = true; |
| remaining = remaining.slice(replyBatch.length); |
| } |
|
|
| if (remaining.length > 0) { |
| await pushLineMessages(remaining); |
| } |
| }; |
|
|
| const richMessages: messagingApi.Message[] = []; |
| const hasQuickReplies = Boolean(lineData.quickReplies?.length); |
|
|
| if (lineData.flexMessage) { |
| richMessages.push( |
| deps.createFlexMessage( |
| lineData.flexMessage.altText.slice(0, 400), |
| lineData.flexMessage.contents as FlexContainer, |
| ), |
| ); |
| } |
|
|
| if (lineData.templateMessage) { |
| const templateMsg = deps.buildTemplateMessageFromPayload(lineData.templateMessage); |
| if (templateMsg) { |
| richMessages.push(templateMsg); |
| } |
| } |
|
|
| if (lineData.location) { |
| richMessages.push(deps.createLocationMessage(lineData.location)); |
| } |
|
|
| const processed = payload.text |
| ? deps.processLineMessage(payload.text) |
| : { text: "", flexMessages: [] }; |
|
|
| for (const flexMsg of processed.flexMessages) { |
| richMessages.push(deps.createFlexMessage(flexMsg.altText.slice(0, 400), flexMsg.contents)); |
| } |
|
|
| const chunks = processed.text ? deps.chunkMarkdownText(processed.text, textLimit) : []; |
|
|
| const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); |
| const mediaMessages = mediaUrls |
| .map((url) => url?.trim()) |
| .filter((url): url is string => Boolean(url)) |
| .map((url) => deps.createImageMessage(url)); |
|
|
| if (chunks.length > 0) { |
| const hasRichOrMedia = richMessages.length > 0 || mediaMessages.length > 0; |
| if (hasQuickReplies && hasRichOrMedia) { |
| try { |
| await sendLineMessages([...richMessages, ...mediaMessages], false); |
| } catch (err) { |
| deps.onReplyError?.(err); |
| } |
| } |
| const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({ |
| to, |
| chunks, |
| quickReplies: lineData.quickReplies, |
| replyToken, |
| replyTokenUsed, |
| accountId, |
| replyMessageLine: deps.replyMessageLine, |
| pushMessageLine: deps.pushMessageLine, |
| pushTextMessageWithQuickReplies: deps.pushTextMessageWithQuickReplies, |
| createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies, |
| }); |
| replyTokenUsed = nextReplyTokenUsed; |
| if (!hasQuickReplies || !hasRichOrMedia) { |
| await sendLineMessages(richMessages, false); |
| if (mediaMessages.length > 0) { |
| await sendLineMessages(mediaMessages, false); |
| } |
| } |
| } else { |
| const combined = [...richMessages, ...mediaMessages]; |
| if (hasQuickReplies && combined.length > 0) { |
| const quickReply = deps.createQuickReplyItems(lineData.quickReplies!); |
| const targetIndex = |
| replyToken && !replyTokenUsed ? Math.min(4, combined.length - 1) : combined.length - 1; |
| const target = combined[targetIndex] as messagingApi.Message & { |
| quickReply?: messagingApi.QuickReply; |
| }; |
| combined[targetIndex] = { ...target, quickReply }; |
| } |
| await sendLineMessages(combined, true); |
| } |
|
|
| return { replyTokenUsed }; |
| } |
|
|