@jcheesepkg/nanobot 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"line.d.mts","names":[],"sources":["../../src/channels/line.ts"],"mappings":";;;;;;UAcU,eAAA;EACR,WAAA;EACA,MAAA,EAAQ,SAAA;AAAA;AAAA,UAGA,UAAA;EACR,IAAA;EACA,MAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGQ,SAAA;EACR,IAAA;EACA,SAAA;EACA,MAAA,EAAQ,UAAA;EACR,UAAA;EACA,IAAA;EACA,cAAA;EACA,eAAA;IAAmB,YAAA;EAAA;EACnB,OAAA,GAAU,WAAA;AAAA;AAAA,UAKF,SAAA;EACR,KAAA;EACA,MAAA;EACA,SAAA;EACA,OAAA;AAAA;AAAA,UAKQ,aAAA;EACR,KAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAA,UAGQ,WAAA;EACR,UAAA,EAAY,aAAA;AAAA;AAAA,UAKJ,mBAAA;EACR,IAAA;EACA,kBAAA;EACA,eAAA;AAAA;AAAA,UAKQ,YAAA;EACR,EAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,UAKQ,WAAA;EACR,IAAA;EACA,EAAA;EACA,UAAA;EACA,eAAA;EAGA,IAAA;EACA,MAAA,GAAS,SAAA;EACT,OAAA,GAAU,WAAA;EAGV,eAAA,GAAkB,mBAAA;EAClB,QAAA,GAAW,YAAA;EAGX,QAAA;EAGA,QAAA;EACA,QAAA;EAGA,KAAA;EACA,OAAA;EACA,QAAA;EACA,SAAA;EAGA,SAAA;EACA,SAAA;EACA,mBAAA;EACA,QAAA;AAAA;AAAA,iBAOc,mBAAA,CACd,aAAA,UACA,OAAA,UACA,SAAA;;;AAvDe;;;;;cA8EJ,WAAA,SAAoB,WAAA;EAAA,SACtB,IAAA;EAAA,QACD,UAAA;EAAA,QACA,SAAA;cAEI,MAAA,EAAQ,UAAA,EAAY,GAAA,EAAK,UAAA,EAAY,SAAA;EAO3C,KAAA,CAAA,GAAS,OAAA;EAKT,IAAA,CAAA,GAAQ,OAAA;EAMR,IAAA,CAAK,GAAA,EAAK,eAAA,GAAkB,OAAA;EA/ExB;EAuFJ,KAAA,CAAM,UAAA,UAAoB,IAAA,WAAe,OAAA;EAnFpC;EAwGL,WAAA,CAAY,EAAA,UAAY,IAAA,WAAe,OAAA;EAxGtB;;;;EAkIjB,aAAA,CAAc,IAAA,EAAM,eAAA,GAAkB,OAAA;EAAA,QA2B9B,cAAA;EAlKd;EAAA,QA8OQ,gBAAA;EA7OR;EAAA,QAgQc,kBAAA;EA7Pd;EAAA,QAsRQ,kBAAA;EArRR;EAAA,QA6RQ,kBAAA;EA1RR;EAAA,QAkSc,iBAAA;EA9Rd;EAAA,QA0SQ,qBAAA;EAtSR;EAAA,QAiTQ,oBAAA;EA/SR;;;;EAAA,QA2Uc,eAAA;EArUN;;AAOV;;;EAPU,QA+WM,mBAAA;EAvWd;;;;EAwZA,eAAA,CAAgB,OAAA,UAAiB,SAAA;EAAA,QAUzB,iBAAA;EAzYe;EAAA,QAoZf,YAAA;EA/YY;EAAA,QA0ZZ,cAAA;EAnZO;EAAA,QAwaP,cAAA;AAAA"}
1
+ {"version":3,"file":"line.d.mts","names":[],"sources":["../../src/channels/line.ts"],"mappings":";;;;;;UAcU,eAAA;EACR,WAAA;EACA,MAAA,EAAQ,SAAA;AAAA;AAAA,UAGA,UAAA;EACR,IAAA;EACA,MAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGQ,SAAA;EACR,IAAA;EACA,SAAA;EACA,MAAA,EAAQ,UAAA;EACR,UAAA;EACA,IAAA;EACA,cAAA;EACA,eAAA;IAAmB,YAAA;EAAA;EACnB,OAAA,GAAU,WAAA;AAAA;AAAA,UAKF,SAAA;EACR,KAAA;EACA,MAAA;EACA,SAAA;EACA,OAAA;AAAA;AAAA,UAKQ,aAAA;EACR,KAAA;EACA,MAAA;EACA,IAAA;EACA,MAAA;EACA,MAAA;AAAA;AAAA,UAGQ,WAAA;EACR,UAAA,EAAY,aAAA;AAAA;AAAA,UAKJ,mBAAA;EACR,IAAA;EACA,kBAAA;EACA,eAAA;AAAA;AAAA,UAKQ,YAAA;EACR,EAAA;EACA,KAAA;EACA,KAAA;AAAA;AAAA,UAKQ,WAAA;EACR,IAAA;EACA,EAAA;EACA,UAAA;EACA,eAAA;EAGA,IAAA;EACA,MAAA,GAAS,SAAA;EACT,OAAA,GAAU,WAAA;EAGV,eAAA,GAAkB,mBAAA;EAClB,QAAA,GAAW,YAAA;EAGX,QAAA;EAGA,QAAA;EACA,QAAA;EAGA,KAAA;EACA,OAAA;EACA,QAAA;EACA,SAAA;EAGA,SAAA;EACA,SAAA;EACA,mBAAA;EACA,QAAA;AAAA;AAAA,iBAOc,mBAAA,CACd,aAAA,UACA,OAAA,UACA,SAAA;;;AAvDe;;;;;cA8EJ,WAAA,SAAoB,WAAA;EAAA,SACtB,IAAA;EAAA,QACD,UAAA;EAAA,QACA,SAAA;cAEI,MAAA,EAAQ,UAAA,EAAY,GAAA,EAAK,UAAA,EAAY,SAAA;EAO3C,KAAA,CAAA,GAAS,OAAA;EAKT,IAAA,CAAA,GAAQ,OAAA;EAMR,IAAA,CAAK,GAAA,EAAK,eAAA,GAAkB,OAAA;EA/ExB;EAuFJ,KAAA,CAAM,UAAA,UAAoB,IAAA,WAAe,OAAA;EAnFpC;EAwGL,WAAA,CAAY,EAAA,UAAY,IAAA,WAAe,OAAA;EAxGtB;;;;EAkIjB,aAAA,CAAc,IAAA,EAAM,eAAA,GAAkB,OAAA;EAAA,QA2B9B,cAAA;EAlKd;EAAA,QA8OQ,gBAAA;EA7OR;EAAA,QAgQc,kBAAA;EA7Pd;EAAA,QAsRQ,kBAAA;EArRR;EAAA,QA6RQ,kBAAA;EA1RR;EAAA,QAkSc,iBAAA;EA9Rd;EAAA,QA0SQ,qBAAA;EAtSR;EAAA,QAiTQ,oBAAA;EA/SR;;;;EAAA,QA2Uc,eAAA;EArUN;;AAOV;;;EAPU,QA+WM,mBAAA;EAvWd;;;;EAwZA,eAAA,CAAgB,OAAA,UAAiB,SAAA;EAAA,QAUzB,iBAAA;EAzYe;EAAA,QAwZf,YAAA;EAnZY;EAAA,QA8ZZ,cAAA;EAvZO;EAAA,QA4aP,cAAA;AAAA"}
@@ -266,7 +266,9 @@ var LineChannel = class extends BaseChannel {
266
266
  return verifyLineSignature(this.lineConfig.channelSecret, rawBody, signature);
267
267
  }
268
268
  returnFlexMessage(message) {
269
+ console.log("message", message);
269
270
  const parsed = JSON.parse(message);
271
+ console.log("parsed", parsed);
270
272
  return [{
271
273
  type: "flex",
272
274
  altText: this.extractAltText(parsed),
@@ -1 +1 @@
1
- {"version":3,"file":"line.mjs","names":[],"sources":["../../src/channels/line.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { messagingApi } from \"@line/bot-sdk\";\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport { BaseChannel } from \"./base.js\";\nimport type { LineConfig } from \"../config/schema.js\";\n\n// ---------------------------------------------------------------------------\n// LINE Messaging API types (webhook events)\n// ---------------------------------------------------------------------------\n\ninterface LineWebhookBody {\n destination: string;\n events: LineEvent[];\n}\n\ninterface LineSource {\n type: \"user\" | \"group\" | \"room\";\n userId?: string;\n groupId?: string;\n roomId?: string;\n}\n\ninterface LineEvent {\n type: string;\n timestamp: number;\n source: LineSource;\n replyToken?: string;\n mode: \"active\" | \"standby\";\n webhookEventId: string;\n deliveryContext: { isRedelivery: boolean };\n message?: LineMessage;\n}\n\n// -- Emoji in text messages ------------------------------------------------\n\ninterface LineEmoji {\n index: number;\n length: number;\n productId: string;\n emojiId: string;\n}\n\n// -- Mention in text messages ----------------------------------------------\n\ninterface LineMentionee {\n index: number;\n length: number;\n type: \"user\" | \"all\";\n userId?: string;\n isSelf?: boolean;\n}\n\ninterface LineMention {\n mentionees: LineMentionee[];\n}\n\n// -- Content provider (image, video, audio) --------------------------------\n\ninterface LineContentProvider {\n type: \"line\" | \"external\";\n originalContentUrl?: string;\n previewImageUrl?: string;\n}\n\n// -- Image set (multiple images sent simultaneously) -----------------------\n\ninterface LineImageSet {\n id: string;\n index?: number;\n total?: number;\n}\n\n// -- Union message type ----------------------------------------------------\n\ninterface LineMessage {\n type: string;\n id: string;\n quoteToken?: string;\n quotedMessageId?: string;\n\n // Text\n text?: string;\n emojis?: LineEmoji[];\n mention?: LineMention;\n\n // Image\n contentProvider?: LineContentProvider;\n imageSet?: LineImageSet;\n\n // Video / Audio\n duration?: number;\n\n // File\n fileName?: string;\n fileSize?: number;\n\n // Location\n title?: string;\n address?: string;\n latitude?: number;\n longitude?: number;\n\n // Sticker\n packageId?: string;\n stickerId?: string;\n stickerResourceType?: string;\n keywords?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Signature verification\n// ---------------------------------------------------------------------------\n\nexport function verifyLineSignature(\n channelSecret: string,\n rawBody: string,\n signature: string,\n): boolean {\n const digest = createHmac(\"sha256\", channelSecret)\n .update(rawBody)\n .digest(\"base64\");\n try {\n return timingSafeEqual(Buffer.from(digest), Buffer.from(signature));\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// LINE Channel\n// ---------------------------------------------------------------------------\n\n/**\n * LINE Messaging API channel.\n *\n * Unlike Telegram (long-polling), LINE uses webhooks -- the HTTP server pushes\n * events into this channel via `handleWebhook()`. The channel is registered in\n * the gateway Hono server which forwards POST /webhook/line to it.\n */\nexport class LineChannel extends BaseChannel {\n readonly name = \"line\";\n private lineConfig: LineConfig;\n private workspace: string | null;\n\n constructor(config: LineConfig, bus: MessageBus, workspace?: string) {\n super(config, bus);\n this.lineConfig = config;\n this.workspace = workspace ?? null;\n }\n\n // LINE is webhook-driven; start/stop are no-ops.\n async start(): Promise<void> {\n this._running = true;\n console.log(\"LINE channel ready (webhook mode)\");\n }\n\n async stop(): Promise<void> {\n this._running = false;\n }\n\n // ------ Outbound: reply or push ------\n\n async send(msg: OutboundMessage): Promise<void> {\n // msg.chatId is the LINE userId (or groupId/roomId)\n // Reply tokens expire quickly, so we always use the push API\n // for outbound messages routed through the bus.\n await this.pushMessage(msg.chatId, msg.content);\n }\n\n /** Send a reply using a replyToken (fast, free, single-use). */\n async reply(replyToken: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/reply\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ replyToken, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE reply failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE reply error:\", err);\n }\n }\n\n /** Send a push message (works any time, consumes monthly quota). */\n async pushMessage(to: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/push\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ to, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE push failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE push error:\", err);\n }\n }\n\n // ------ Inbound: webhook events ------\n\n /**\n * Process a raw webhook request.\n * Called by the gateway Hono route after signature verification.\n */\n async handleWebhook(body: LineWebhookBody): Promise<void> {\n for (const event of body.events) {\n if (event.mode !== \"active\") continue;\n\n if (event.type === \"message\" && event.message) {\n await this.onMessageEvent(event);\n } else if (event.type === \"follow\") {\n const userId = event.source.userId;\n if (userId) {\n console.log(`LINE: new follower ${userId}`);\n if (event.replyToken) {\n await this.reply(\n event.replyToken,\n \"Hi! I'm nanobot. Send me a message and I'll respond!\",\n );\n }\n }\n } else if (event.type === \"unfollow\") {\n console.log(`LINE: unfollowed by ${event.source.userId ?? \"unknown\"}`);\n } else if (event.type === \"join\") {\n console.log(`LINE: joined ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n } else if (event.type === \"leave\") {\n console.log(`LINE: left ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n }\n }\n }\n\n private async onMessageEvent(event: LineEvent): Promise<void> {\n const message = event.message!;\n const source = event.source;\n\n // Determine senderId and chatId\n const senderId = source.userId ?? \"unknown\";\n const chatId =\n source.type === \"group\"\n ? (source.groupId ?? senderId)\n : source.type === \"room\"\n ? (source.roomId ?? senderId)\n : senderId;\n\n // Build content and optional media\n let content: string;\n const media: string[] = [];\n\n switch (message.type) {\n case \"text\":\n content = this.buildTextContent(message);\n break;\n\n case \"image\":\n content = await this.handleImageMessage(message, media);\n break;\n\n case \"video\":\n content = this.handleVideoMessage(message);\n break;\n\n case \"audio\":\n content = this.handleAudioMessage(message);\n break;\n\n case \"file\":\n content = await this.handleFileMessage(message);\n break;\n\n case \"location\":\n content = this.handleLocationMessage(message);\n break;\n\n case \"sticker\":\n content = this.handleStickerMessage(message);\n break;\n\n default:\n content = `[${message.type} message]`;\n break;\n }\n\n // Prepend quoted message context if this message quotes a previous one\n if (message.quotedMessageId) {\n content = `[Quoting message ${message.quotedMessageId}]\\n${content}`;\n }\n\n // Store the replyToken + rich metadata so downstream can use them\n await this.handleMessage({\n senderId,\n chatId,\n content,\n media,\n metadata: {\n messageId: message.id,\n replyToken: event.replyToken,\n sourceType: source.type,\n quoteToken: message.quoteToken,\n ...(message.quotedMessageId && { quotedMessageId: message.quotedMessageId }),\n ...(message.mention && { mention: message.mention }),\n },\n });\n }\n\n // ------ Message type handlers ------\n\n /** Build content string for a text message, including mention/emoji info. */\n private buildTextContent(message: LineMessage): string {\n let content = message.text ?? \"\";\n\n // If the text contains mentions, annotate them\n if (message.mention && message.mention.mentionees.length > 0) {\n const mentionInfo = message.mention.mentionees\n .map((m) => {\n const who = m.type === \"all\" ? \"@All\" : (m.userId ?? \"user\");\n const isSelf = m.isSelf ? \" (mentioning bot)\" : \"\";\n return `${who}${isSelf}`;\n })\n .join(\", \");\n content += `\\n[Mentions: ${mentionInfo}]`;\n }\n\n return content;\n }\n\n /** Handle image message: download from Content API if provider is LINE. */\n private async handleImageMessage(\n message: LineMessage,\n media: string[],\n ): Promise<string> {\n const provider = message.contentProvider;\n\n if (provider?.type === \"external\" && provider.originalContentUrl) {\n // External image: use the URL directly\n media.push(provider.originalContentUrl);\n return \"[User sent an image]\";\n }\n\n // LINE-hosted image: download via Content API\n const imagePath = await this.downloadContent(message.id, \"jpg\");\n if (imagePath) {\n media.push(imagePath);\n const setInfo = message.imageSet\n ? ` (${message.imageSet.index ?? \"?\"}/${message.imageSet.total ?? \"?\"})`\n : \"\";\n return `[User sent an image${setInfo}]`;\n }\n return \"[Image: failed to download]\";\n }\n\n /** Handle video message. */\n private handleVideoMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Video message${duration} -- video analysis not yet supported]`;\n }\n\n /** Handle audio message. */\n private handleAudioMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Audio message${duration} -- transcription not yet supported]`;\n }\n\n /** Handle file message: download from Content API to workspace. */\n private async handleFileMessage(message: LineMessage): Promise<string> {\n const fileName = message.fileName ?? `file_${message.id}`;\n const fileSize = this.formatFileSize(message.fileSize);\n\n const savedPath = await this.downloadToWorkspace(message.id, fileName);\n if (savedPath) {\n return `[File received: ${fileName} (${fileSize}) — saved to ${savedPath}]`;\n }\n return `[File: ${fileName} (${fileSize}) — download failed]`;\n }\n\n /** Handle location message with all available fields. */\n private handleLocationMessage(message: LineMessage): string {\n const parts: string[] = [];\n if (message.title) parts.push(message.title);\n if (message.address) parts.push(message.address);\n if (message.latitude != null && message.longitude != null) {\n parts.push(`(${message.latitude}, ${message.longitude})`);\n }\n return `[Location: ${parts.join(\" | \")}]`;\n }\n\n /** Handle sticker message with resource type and keywords. */\n private handleStickerMessage(message: LineMessage): string {\n const parts: string[] = [\"Sticker\"];\n\n // Include sticker text for message stickers\n if (message.text) {\n parts.push(`\"${message.text}\"`);\n }\n\n // Include keywords if available (gives the AI context about the sticker)\n if (message.keywords && message.keywords.length > 0) {\n parts.push(`(${message.keywords.join(\", \")})`);\n }\n\n // Resource type for logging\n const resType = message.stickerResourceType;\n if (resType && resType !== \"STATIC\") {\n parts.push(`[${resType.toLowerCase()}]`);\n }\n\n return `[${parts.join(\" \")}]`;\n }\n\n // ------ Content download ------\n\n /**\n * Download message content (image, video, audio, file) from LINE Content API.\n * Returns the local file path, or null on failure.\n */\n private async downloadContent(\n messageId: string,\n ext: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Save to temp dir\n const dir = join(tmpdir(), \"nanobot-line-media\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const filePath = join(dir, `${messageId}.${ext}`);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: downloaded content ${messageId} (${buffer.length} bytes)`);\n return filePath;\n } catch (err) {\n console.error(\"LINE content download error:\", err);\n return null;\n }\n }\n\n /**\n * Download message content to the workspace uploads directory.\n * Falls back to tmpdir if no workspace is configured.\n * Returns the saved file path, or null on failure.\n */\n private async downloadToWorkspace(\n messageId: string,\n fileName: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Determine save directory\n const dir = this.workspace\n ? join(this.workspace, \"uploads\")\n : join(tmpdir(), \"nanobot-line-uploads\");\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Avoid filename collisions by prepending timestamp\n const safeName = `${Date.now()}_${fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\")}`;\n const filePath = join(dir, safeName);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`);\n return filePath;\n } catch (err) {\n console.error(\"LINE file download error:\", err);\n return null;\n }\n }\n\n // ------ Signature ------\n\n /**\n * Verify webhook signature.\n * Exposed as a helper so the gateway route can call it.\n */\n verifySignature(rawBody: string, signature: string): boolean {\n return verifyLineSignature(\n this.lineConfig.channelSecret,\n rawBody,\n signature,\n );\n }\n\n // ------ Helpers ------\n\n private returnFlexMessage(message: string) {\n const parsed = JSON.parse(message);\n\n return [{\n type: \"flex\",\n altText: this.extractAltText(parsed),\n contents: parsed,\n }] satisfies messagingApi.Message[];\n }\n\n /** Parse text into LINE message. If valid Flex JSON, send as flex; otherwise text. */\n private parseMessage(text: string): messagingApi.Message[] {\n const trimmed = text.trim();\n \n try {\n return this.returnFlexMessage(trimmed);\n } catch {\n return [{ type: \"text\", text: trimmed || \"(empty)\" }];\n }\n }\n\n /** Extract alt text from flex content. */\n private extractAltText(contents: Record<string, unknown>): string {\n const extractText = (obj: unknown, depth = 0): string | null => {\n if (depth > 5) return null;\n if (typeof obj === \"string\") return obj.slice(0, 100);\n if (!obj || typeof obj !== \"object\") return null;\n\n const record = obj as Record<string, unknown>;\n if (record.text && typeof record.text === \"string\") return record.text.slice(0, 100);\n if (record.title && typeof record.title === \"string\") return record.title.slice(0, 100);\n\n for (const key of [\"contents\", \"body\", \"header\", \"hero\"]) {\n const result = extractText(record[key], depth + 1);\n if (result) return result;\n }\n return null;\n };\n\n return extractText(contents) || \"Flex Message\";\n }\n\n /** Format file size in human-readable form. */\n private formatFileSize(bytes?: number): string {\n if (bytes == null) return \"unknown size\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"line.mjs","names":[],"sources":["../../src/channels/line.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { messagingApi } from \"@line/bot-sdk\";\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport { BaseChannel } from \"./base.js\";\nimport type { LineConfig } from \"../config/schema.js\";\n\n// ---------------------------------------------------------------------------\n// LINE Messaging API types (webhook events)\n// ---------------------------------------------------------------------------\n\ninterface LineWebhookBody {\n destination: string;\n events: LineEvent[];\n}\n\ninterface LineSource {\n type: \"user\" | \"group\" | \"room\";\n userId?: string;\n groupId?: string;\n roomId?: string;\n}\n\ninterface LineEvent {\n type: string;\n timestamp: number;\n source: LineSource;\n replyToken?: string;\n mode: \"active\" | \"standby\";\n webhookEventId: string;\n deliveryContext: { isRedelivery: boolean };\n message?: LineMessage;\n}\n\n// -- Emoji in text messages ------------------------------------------------\n\ninterface LineEmoji {\n index: number;\n length: number;\n productId: string;\n emojiId: string;\n}\n\n// -- Mention in text messages ----------------------------------------------\n\ninterface LineMentionee {\n index: number;\n length: number;\n type: \"user\" | \"all\";\n userId?: string;\n isSelf?: boolean;\n}\n\ninterface LineMention {\n mentionees: LineMentionee[];\n}\n\n// -- Content provider (image, video, audio) --------------------------------\n\ninterface LineContentProvider {\n type: \"line\" | \"external\";\n originalContentUrl?: string;\n previewImageUrl?: string;\n}\n\n// -- Image set (multiple images sent simultaneously) -----------------------\n\ninterface LineImageSet {\n id: string;\n index?: number;\n total?: number;\n}\n\n// -- Union message type ----------------------------------------------------\n\ninterface LineMessage {\n type: string;\n id: string;\n quoteToken?: string;\n quotedMessageId?: string;\n\n // Text\n text?: string;\n emojis?: LineEmoji[];\n mention?: LineMention;\n\n // Image\n contentProvider?: LineContentProvider;\n imageSet?: LineImageSet;\n\n // Video / Audio\n duration?: number;\n\n // File\n fileName?: string;\n fileSize?: number;\n\n // Location\n title?: string;\n address?: string;\n latitude?: number;\n longitude?: number;\n\n // Sticker\n packageId?: string;\n stickerId?: string;\n stickerResourceType?: string;\n keywords?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Signature verification\n// ---------------------------------------------------------------------------\n\nexport function verifyLineSignature(\n channelSecret: string,\n rawBody: string,\n signature: string,\n): boolean {\n const digest = createHmac(\"sha256\", channelSecret)\n .update(rawBody)\n .digest(\"base64\");\n try {\n return timingSafeEqual(Buffer.from(digest), Buffer.from(signature));\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// LINE Channel\n// ---------------------------------------------------------------------------\n\n/**\n * LINE Messaging API channel.\n *\n * Unlike Telegram (long-polling), LINE uses webhooks -- the HTTP server pushes\n * events into this channel via `handleWebhook()`. The channel is registered in\n * the gateway Hono server which forwards POST /webhook/line to it.\n */\nexport class LineChannel extends BaseChannel {\n readonly name = \"line\";\n private lineConfig: LineConfig;\n private workspace: string | null;\n\n constructor(config: LineConfig, bus: MessageBus, workspace?: string) {\n super(config, bus);\n this.lineConfig = config;\n this.workspace = workspace ?? null;\n }\n\n // LINE is webhook-driven; start/stop are no-ops.\n async start(): Promise<void> {\n this._running = true;\n console.log(\"LINE channel ready (webhook mode)\");\n }\n\n async stop(): Promise<void> {\n this._running = false;\n }\n\n // ------ Outbound: reply or push ------\n\n async send(msg: OutboundMessage): Promise<void> {\n // msg.chatId is the LINE userId (or groupId/roomId)\n // Reply tokens expire quickly, so we always use the push API\n // for outbound messages routed through the bus.\n await this.pushMessage(msg.chatId, msg.content);\n }\n\n /** Send a reply using a replyToken (fast, free, single-use). */\n async reply(replyToken: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/reply\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ replyToken, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE reply failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE reply error:\", err);\n }\n }\n\n /** Send a push message (works any time, consumes monthly quota). */\n async pushMessage(to: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/push\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ to, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE push failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE push error:\", err);\n }\n }\n\n // ------ Inbound: webhook events ------\n\n /**\n * Process a raw webhook request.\n * Called by the gateway Hono route after signature verification.\n */\n async handleWebhook(body: LineWebhookBody): Promise<void> {\n for (const event of body.events) {\n if (event.mode !== \"active\") continue;\n\n if (event.type === \"message\" && event.message) {\n await this.onMessageEvent(event);\n } else if (event.type === \"follow\") {\n const userId = event.source.userId;\n if (userId) {\n console.log(`LINE: new follower ${userId}`);\n if (event.replyToken) {\n await this.reply(\n event.replyToken,\n \"Hi! I'm nanobot. Send me a message and I'll respond!\",\n );\n }\n }\n } else if (event.type === \"unfollow\") {\n console.log(`LINE: unfollowed by ${event.source.userId ?? \"unknown\"}`);\n } else if (event.type === \"join\") {\n console.log(`LINE: joined ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n } else if (event.type === \"leave\") {\n console.log(`LINE: left ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n }\n }\n }\n\n private async onMessageEvent(event: LineEvent): Promise<void> {\n const message = event.message!;\n const source = event.source;\n\n // Determine senderId and chatId\n const senderId = source.userId ?? \"unknown\";\n const chatId =\n source.type === \"group\"\n ? (source.groupId ?? senderId)\n : source.type === \"room\"\n ? (source.roomId ?? senderId)\n : senderId;\n\n // Build content and optional media\n let content: string;\n const media: string[] = [];\n\n switch (message.type) {\n case \"text\":\n content = this.buildTextContent(message);\n break;\n\n case \"image\":\n content = await this.handleImageMessage(message, media);\n break;\n\n case \"video\":\n content = this.handleVideoMessage(message);\n break;\n\n case \"audio\":\n content = this.handleAudioMessage(message);\n break;\n\n case \"file\":\n content = await this.handleFileMessage(message);\n break;\n\n case \"location\":\n content = this.handleLocationMessage(message);\n break;\n\n case \"sticker\":\n content = this.handleStickerMessage(message);\n break;\n\n default:\n content = `[${message.type} message]`;\n break;\n }\n\n // Prepend quoted message context if this message quotes a previous one\n if (message.quotedMessageId) {\n content = `[Quoting message ${message.quotedMessageId}]\\n${content}`;\n }\n\n // Store the replyToken + rich metadata so downstream can use them\n await this.handleMessage({\n senderId,\n chatId,\n content,\n media,\n metadata: {\n messageId: message.id,\n replyToken: event.replyToken,\n sourceType: source.type,\n quoteToken: message.quoteToken,\n ...(message.quotedMessageId && { quotedMessageId: message.quotedMessageId }),\n ...(message.mention && { mention: message.mention }),\n },\n });\n }\n\n // ------ Message type handlers ------\n\n /** Build content string for a text message, including mention/emoji info. */\n private buildTextContent(message: LineMessage): string {\n let content = message.text ?? \"\";\n\n // If the text contains mentions, annotate them\n if (message.mention && message.mention.mentionees.length > 0) {\n const mentionInfo = message.mention.mentionees\n .map((m) => {\n const who = m.type === \"all\" ? \"@All\" : (m.userId ?? \"user\");\n const isSelf = m.isSelf ? \" (mentioning bot)\" : \"\";\n return `${who}${isSelf}`;\n })\n .join(\", \");\n content += `\\n[Mentions: ${mentionInfo}]`;\n }\n\n return content;\n }\n\n /** Handle image message: download from Content API if provider is LINE. */\n private async handleImageMessage(\n message: LineMessage,\n media: string[],\n ): Promise<string> {\n const provider = message.contentProvider;\n\n if (provider?.type === \"external\" && provider.originalContentUrl) {\n // External image: use the URL directly\n media.push(provider.originalContentUrl);\n return \"[User sent an image]\";\n }\n\n // LINE-hosted image: download via Content API\n const imagePath = await this.downloadContent(message.id, \"jpg\");\n if (imagePath) {\n media.push(imagePath);\n const setInfo = message.imageSet\n ? ` (${message.imageSet.index ?? \"?\"}/${message.imageSet.total ?? \"?\"})`\n : \"\";\n return `[User sent an image${setInfo}]`;\n }\n return \"[Image: failed to download]\";\n }\n\n /** Handle video message. */\n private handleVideoMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Video message${duration} -- video analysis not yet supported]`;\n }\n\n /** Handle audio message. */\n private handleAudioMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Audio message${duration} -- transcription not yet supported]`;\n }\n\n /** Handle file message: download from Content API to workspace. */\n private async handleFileMessage(message: LineMessage): Promise<string> {\n const fileName = message.fileName ?? `file_${message.id}`;\n const fileSize = this.formatFileSize(message.fileSize);\n\n const savedPath = await this.downloadToWorkspace(message.id, fileName);\n if (savedPath) {\n return `[File received: ${fileName} (${fileSize}) — saved to ${savedPath}]`;\n }\n return `[File: ${fileName} (${fileSize}) — download failed]`;\n }\n\n /** Handle location message with all available fields. */\n private handleLocationMessage(message: LineMessage): string {\n const parts: string[] = [];\n if (message.title) parts.push(message.title);\n if (message.address) parts.push(message.address);\n if (message.latitude != null && message.longitude != null) {\n parts.push(`(${message.latitude}, ${message.longitude})`);\n }\n return `[Location: ${parts.join(\" | \")}]`;\n }\n\n /** Handle sticker message with resource type and keywords. */\n private handleStickerMessage(message: LineMessage): string {\n const parts: string[] = [\"Sticker\"];\n\n // Include sticker text for message stickers\n if (message.text) {\n parts.push(`\"${message.text}\"`);\n }\n\n // Include keywords if available (gives the AI context about the sticker)\n if (message.keywords && message.keywords.length > 0) {\n parts.push(`(${message.keywords.join(\", \")})`);\n }\n\n // Resource type for logging\n const resType = message.stickerResourceType;\n if (resType && resType !== \"STATIC\") {\n parts.push(`[${resType.toLowerCase()}]`);\n }\n\n return `[${parts.join(\" \")}]`;\n }\n\n // ------ Content download ------\n\n /**\n * Download message content (image, video, audio, file) from LINE Content API.\n * Returns the local file path, or null on failure.\n */\n private async downloadContent(\n messageId: string,\n ext: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Save to temp dir\n const dir = join(tmpdir(), \"nanobot-line-media\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const filePath = join(dir, `${messageId}.${ext}`);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: downloaded content ${messageId} (${buffer.length} bytes)`);\n return filePath;\n } catch (err) {\n console.error(\"LINE content download error:\", err);\n return null;\n }\n }\n\n /**\n * Download message content to the workspace uploads directory.\n * Falls back to tmpdir if no workspace is configured.\n * Returns the saved file path, or null on failure.\n */\n private async downloadToWorkspace(\n messageId: string,\n fileName: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Determine save directory\n const dir = this.workspace\n ? join(this.workspace, \"uploads\")\n : join(tmpdir(), \"nanobot-line-uploads\");\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Avoid filename collisions by prepending timestamp\n const safeName = `${Date.now()}_${fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\")}`;\n const filePath = join(dir, safeName);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`);\n return filePath;\n } catch (err) {\n console.error(\"LINE file download error:\", err);\n return null;\n }\n }\n\n // ------ Signature ------\n\n /**\n * Verify webhook signature.\n * Exposed as a helper so the gateway route can call it.\n */\n verifySignature(rawBody: string, signature: string): boolean {\n return verifyLineSignature(\n this.lineConfig.channelSecret,\n rawBody,\n signature,\n );\n }\n\n // ------ Helpers ------\n\n private returnFlexMessage(message: string) {\n console.log(\"message\", message);\n \n const parsed = JSON.parse(message);\n \n console.log(\"parsed\", parsed);\n\n return [{\n type: \"flex\",\n altText: this.extractAltText(parsed),\n contents: parsed,\n }] satisfies messagingApi.Message[];\n }\n\n /** Parse text into LINE message. If valid Flex JSON, send as flex; otherwise text. */\n private parseMessage(text: string): messagingApi.Message[] {\n const trimmed = text.trim();\n \n try {\n return this.returnFlexMessage(trimmed);\n } catch {\n return [{ type: \"text\", text: trimmed || \"(empty)\" }];\n }\n }\n\n /** Extract alt text from flex content. */\n private extractAltText(contents: Record<string, unknown>): string {\n const extractText = (obj: unknown, depth = 0): string | null => {\n if (depth > 5) return null;\n if (typeof obj === \"string\") return obj.slice(0, 100);\n if (!obj || typeof obj !== \"object\") return null;\n\n const record = obj as Record<string, unknown>;\n if (record.text && typeof record.text === \"string\") return record.text.slice(0, 100);\n if (record.title && typeof record.title === \"string\") return record.title.slice(0, 100);\n\n for (const key of [\"contents\", \"body\", \"header\", \"hero\"]) {\n const result = extractText(record[key], depth + 1);\n if (result) return result;\n }\n return null;\n };\n\n return extractText(contents) || \"Flex Message\";\n }\n\n /** Format file size in human-readable form. */\n private formatFileSize(bytes?: number): string {\n if (bytes == null) return \"unknown size\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n }\n}\n"],"mappings":"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"}
@@ -556,15 +556,15 @@ declare const ToolsConfigSchema: z.ZodObject<{
556
556
  export?: string | undefined;
557
557
  }>, "many">>;
558
558
  }, "strip", z.ZodTypeAny, {
559
+ exec: {
560
+ timeout: number;
561
+ };
559
562
  web: {
560
563
  search: {
561
564
  apiKey: string;
562
565
  maxResults: number;
563
566
  };
564
567
  };
565
- exec: {
566
- timeout: number;
567
- };
568
568
  restrictToWorkspace: boolean;
569
569
  enabled?: string[] | undefined;
570
570
  disabled?: string[] | undefined;
@@ -574,6 +574,9 @@ declare const ToolsConfigSchema: z.ZodObject<{
574
574
  export?: string | undefined;
575
575
  }[] | undefined;
576
576
  }, {
577
+ exec?: {
578
+ timeout?: number | undefined;
579
+ } | undefined;
577
580
  enabled?: string[] | undefined;
578
581
  web?: {
579
582
  search?: {
@@ -581,9 +584,6 @@ declare const ToolsConfigSchema: z.ZodObject<{
581
584
  maxResults?: number | undefined;
582
585
  } | undefined;
583
586
  } | undefined;
584
- exec?: {
585
- timeout?: number | undefined;
586
- } | undefined;
587
587
  restrictToWorkspace?: boolean | undefined;
588
588
  disabled?: string[] | undefined;
589
589
  custom?: {
@@ -1015,15 +1015,15 @@ declare const ConfigSchema: z.ZodObject<{
1015
1015
  export?: string | undefined;
1016
1016
  }>, "many">>;
1017
1017
  }, "strip", z.ZodTypeAny, {
1018
+ exec: {
1019
+ timeout: number;
1020
+ };
1018
1021
  web: {
1019
1022
  search: {
1020
1023
  apiKey: string;
1021
1024
  maxResults: number;
1022
1025
  };
1023
1026
  };
1024
- exec: {
1025
- timeout: number;
1026
- };
1027
1027
  restrictToWorkspace: boolean;
1028
1028
  enabled?: string[] | undefined;
1029
1029
  disabled?: string[] | undefined;
@@ -1033,6 +1033,9 @@ declare const ConfigSchema: z.ZodObject<{
1033
1033
  export?: string | undefined;
1034
1034
  }[] | undefined;
1035
1035
  }, {
1036
+ exec?: {
1037
+ timeout?: number | undefined;
1038
+ } | undefined;
1036
1039
  enabled?: string[] | undefined;
1037
1040
  web?: {
1038
1041
  search?: {
@@ -1040,9 +1043,6 @@ declare const ConfigSchema: z.ZodObject<{
1040
1043
  maxResults?: number | undefined;
1041
1044
  } | undefined;
1042
1045
  } | undefined;
1043
- exec?: {
1044
- timeout?: number | undefined;
1045
- } | undefined;
1046
1046
  restrictToWorkspace?: boolean | undefined;
1047
1047
  disabled?: string[] | undefined;
1048
1048
  custom?: {
@@ -1139,15 +1139,15 @@ declare const ConfigSchema: z.ZodObject<{
1139
1139
  port: number;
1140
1140
  };
1141
1141
  tools: {
1142
+ exec: {
1143
+ timeout: number;
1144
+ };
1142
1145
  web: {
1143
1146
  search: {
1144
1147
  apiKey: string;
1145
1148
  maxResults: number;
1146
1149
  };
1147
1150
  };
1148
- exec: {
1149
- timeout: number;
1150
- };
1151
1151
  restrictToWorkspace: boolean;
1152
1152
  enabled?: string[] | undefined;
1153
1153
  disabled?: string[] | undefined;
@@ -1245,6 +1245,9 @@ declare const ConfigSchema: z.ZodObject<{
1245
1245
  port?: number | undefined;
1246
1246
  } | undefined;
1247
1247
  tools?: {
1248
+ exec?: {
1249
+ timeout?: number | undefined;
1250
+ } | undefined;
1248
1251
  enabled?: string[] | undefined;
1249
1252
  web?: {
1250
1253
  search?: {
@@ -1252,9 +1255,6 @@ declare const ConfigSchema: z.ZodObject<{
1252
1255
  maxResults?: number | undefined;
1253
1256
  } | undefined;
1254
1257
  } | undefined;
1255
- exec?: {
1256
- timeout?: number | undefined;
1257
- } | undefined;
1258
1258
  restrictToWorkspace?: boolean | undefined;
1259
1259
  disabled?: string[] | undefined;
1260
1260
  custom?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcheesepkg/nanobot",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Lightweight AI assistant - TypeScript port",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -42,7 +42,7 @@ Day of week:
42
42
  - 水 = 燃えないゴミ
43
43
  - 土 = ペットボトル (varies by ward)
44
44
 
45
- ## Flex Template
45
+ ## Flex Message Template
46
46
 
47
47
  **Output raw JSON directly. No code blocks, no markdown. Just the JSON object:**
48
48
 
@@ -84,3 +84,7 @@ Read from `memory/MEMORY.md`:
84
84
  ## Evening (22:00)
85
85
 
86
86
  Tomorrow's weather + clothing + morning events.
87
+
88
+ ## Notes
89
+
90
+ - ALWAYS output in the Flex Message format.
@@ -40,7 +40,7 @@ Fortune messages:
40
40
  | ★★☆☆☆ | 注意深く。深呼吸して |
41
41
  | ★☆☆☆☆ | 慎重に。無理は禁物 🐢 |
42
42
 
43
- ## Flex Message
43
+ ## Flex Message Template
44
44
 
45
45
  **Output raw JSON directly. No code blocks, no markdown. Just the JSON object:**
46
46
 
@@ -110,3 +110,4 @@ Match bot tone:
110
110
 
111
111
  - Same sign = same fortune all day (deterministic)
112
112
  - Keep it fun, not serious
113
+ - ALWAYS output in the Flex Message format.
@@ -66,4 +66,4 @@ Text-based weekly:
66
66
  ## Motivation
67
67
 
68
68
  - Streak celebration: "🔥 7日連続すごい!"
69
- - Gentle nudge: "今日まだ瞑想してないよ〜"
69
+ - Gentle nudge: "今日まだ瞑想してないよ〜"
@@ -20,7 +20,7 @@ echo "$(date -Iseconds)|1" >> data/hydration.log
20
20
  grep "$(date +%Y-%m-%d)" data/hydration.log | wc -l
21
21
  ```
22
22
 
23
- ## Reminder Flex
23
+ ## Flex Message Template
24
24
 
25
25
  **Output raw JSON directly. No code blocks, no markdown. Just the JSON object:**
26
26
 
@@ -63,3 +63,7 @@ Archive at midnight:
63
63
  ```bash
64
64
  [ -f data/hydration.log ] && mv data/hydration.log data/hydration-$(date +%Y-%m-%d).log
65
65
  ```
66
+
67
+ ## Notes
68
+
69
+ - ALWAYS output in the Flex Message format.