@gakr-gakr/line 0.1.0

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.
Files changed (66) hide show
  1. package/api.ts +11 -0
  2. package/autobot.plugin.json +15 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +5 -0
  5. package/index.ts +54 -0
  6. package/package.json +60 -0
  7. package/runtime-api.ts +182 -0
  8. package/secret-contract-api.ts +4 -0
  9. package/setup-api.ts +2 -0
  10. package/setup-entry.ts +9 -0
  11. package/src/account-helpers.ts +16 -0
  12. package/src/accounts.ts +187 -0
  13. package/src/actions.ts +61 -0
  14. package/src/auto-reply-delivery.ts +200 -0
  15. package/src/bindings.ts +65 -0
  16. package/src/bot-access.ts +30 -0
  17. package/src/bot-handlers.ts +620 -0
  18. package/src/bot-message-context.ts +586 -0
  19. package/src/bot.ts +70 -0
  20. package/src/card-command.ts +347 -0
  21. package/src/channel-access-token.ts +14 -0
  22. package/src/channel-api.ts +17 -0
  23. package/src/channel-shared.ts +48 -0
  24. package/src/channel.runtime.ts +3 -0
  25. package/src/channel.setup.ts +11 -0
  26. package/src/channel.ts +155 -0
  27. package/src/config-adapter.ts +29 -0
  28. package/src/config-schema.ts +81 -0
  29. package/src/download.ts +34 -0
  30. package/src/flex-templates/basic-cards.ts +395 -0
  31. package/src/flex-templates/common.ts +20 -0
  32. package/src/flex-templates/media-control-cards.ts +555 -0
  33. package/src/flex-templates/message.ts +13 -0
  34. package/src/flex-templates/schedule-cards.ts +467 -0
  35. package/src/flex-templates/types.ts +22 -0
  36. package/src/flex-templates.ts +32 -0
  37. package/src/gateway.ts +129 -0
  38. package/src/group-keys.ts +65 -0
  39. package/src/group-policy.ts +22 -0
  40. package/src/markdown-to-line.ts +416 -0
  41. package/src/monitor-durable.ts +37 -0
  42. package/src/monitor.runtime.ts +1 -0
  43. package/src/monitor.ts +507 -0
  44. package/src/outbound-media.ts +120 -0
  45. package/src/outbound.runtime.ts +12 -0
  46. package/src/outbound.ts +427 -0
  47. package/src/probe.runtime.ts +1 -0
  48. package/src/probe.ts +34 -0
  49. package/src/quick-reply-fallback.ts +10 -0
  50. package/src/reply-chunks.ts +110 -0
  51. package/src/reply-payload-transform.ts +317 -0
  52. package/src/rich-menu.ts +326 -0
  53. package/src/runtime.ts +32 -0
  54. package/src/send-receipt.ts +32 -0
  55. package/src/send.ts +531 -0
  56. package/src/setup-core.ts +149 -0
  57. package/src/setup-runtime-api.ts +9 -0
  58. package/src/setup-surface.ts +229 -0
  59. package/src/signature.ts +24 -0
  60. package/src/status.ts +37 -0
  61. package/src/template-messages.ts +333 -0
  62. package/src/types.ts +130 -0
  63. package/src/webhook-node.ts +155 -0
  64. package/src/webhook-utils.ts +10 -0
  65. package/src/webhook.ts +135 -0
  66. package/tsconfig.json +16 -0
@@ -0,0 +1,347 @@
1
+ import type { AutoBotPluginApi } from "autobot/plugin-sdk/core";
2
+ import type { ReplyPayload } from "autobot/plugin-sdk/reply-runtime";
3
+ import { normalizeLowercaseStringOrEmpty } from "autobot/plugin-sdk/string-coerce-runtime";
4
+ import {
5
+ createActionCard,
6
+ createImageCard,
7
+ createInfoCard,
8
+ createListCard,
9
+ createReceiptCard,
10
+ type CardAction,
11
+ type ListItem,
12
+ } from "./flex-templates.js";
13
+ import type { LineChannelData } from "./types.js";
14
+
15
+ const CARD_USAGE = `Usage: /card <type> "title" "body" [options]
16
+
17
+ Types:
18
+ info "Title" "Body" ["Footer"]
19
+ image "Title" "Caption" --url <image-url>
20
+ action "Title" "Body" --actions "Btn1|url1,Btn2|text2"
21
+ list "Title" "Item1|Desc1,Item2|Desc2"
22
+ receipt "Title" "Item1:$10,Item2:$20" --total "$30"
23
+ confirm "Question?" --yes "Yes|data" --no "No|data"
24
+ buttons "Title" "Text" --actions "Btn1|url1,Btn2|data2"
25
+
26
+ Examples:
27
+ /card info "Welcome" "Thanks for joining!"
28
+ /card image "Product" "Check it out" --url https://example.com/img.jpg
29
+ /card action "Menu" "Choose an option" --actions "Order|/order,Help|/help"`;
30
+
31
+ function buildLineReply(lineData: LineChannelData): ReplyPayload {
32
+ return {
33
+ channelData: {
34
+ line: lineData,
35
+ },
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Parse action string format: "Label|data,Label2|data2"
41
+ * Data can be a URL (uri action) or plain text (message action) or key=value (postback)
42
+ */
43
+ function parseActions(actionsStr: string | undefined): CardAction[] {
44
+ if (!actionsStr) {
45
+ return [];
46
+ }
47
+
48
+ const results: CardAction[] = [];
49
+
50
+ for (const part of actionsStr.split(",")) {
51
+ const [label, data] = part
52
+ .trim()
53
+ .split("|")
54
+ .map((s) => s.trim());
55
+ if (!label) {
56
+ continue;
57
+ }
58
+
59
+ const actionData = data || label;
60
+
61
+ if (actionData.startsWith("http://") || actionData.startsWith("https://")) {
62
+ results.push({
63
+ label,
64
+ action: { type: "uri", label: label.slice(0, 20), uri: actionData },
65
+ });
66
+ } else if (actionData.includes("=")) {
67
+ results.push({
68
+ label,
69
+ action: {
70
+ type: "postback",
71
+ label: label.slice(0, 20),
72
+ data: actionData.slice(0, 300),
73
+ displayText: label,
74
+ },
75
+ });
76
+ } else {
77
+ results.push({
78
+ label,
79
+ action: { type: "message", label: label.slice(0, 20), text: actionData },
80
+ });
81
+ }
82
+ }
83
+
84
+ return results;
85
+ }
86
+
87
+ /**
88
+ * Parse list items format: "Item1|Subtitle1,Item2|Subtitle2"
89
+ */
90
+ function parseListItems(itemsStr: string): ListItem[] {
91
+ return itemsStr
92
+ .split(",")
93
+ .map((part) => {
94
+ const [title, subtitle] = part
95
+ .trim()
96
+ .split("|")
97
+ .map((s) => s.trim());
98
+ return { title: title || "", subtitle };
99
+ })
100
+ .filter((item) => item.title);
101
+ }
102
+
103
+ /**
104
+ * Parse receipt items format: "Item1:$10,Item2:$20"
105
+ */
106
+ function parseReceiptItems(itemsStr: string): Array<{ name: string; value: string }> {
107
+ return itemsStr
108
+ .split(",")
109
+ .map((part) => {
110
+ const colonIndex = part.lastIndexOf(":");
111
+ if (colonIndex === -1) {
112
+ return { name: part.trim(), value: "" };
113
+ }
114
+ return {
115
+ name: part.slice(0, colonIndex).trim(),
116
+ value: part.slice(colonIndex + 1).trim(),
117
+ };
118
+ })
119
+ .filter((item) => item.name);
120
+ }
121
+
122
+ /**
123
+ * Parse quoted arguments from command string
124
+ * Supports: /card type "arg1" "arg2" "arg3" --flag value
125
+ */
126
+ function parseCardArgs(argsStr: string): {
127
+ type: string;
128
+ args: string[];
129
+ flags: Record<string, string>;
130
+ } {
131
+ const result: { type: string; args: string[]; flags: Record<string, string> } = {
132
+ type: "",
133
+ args: [],
134
+ flags: {},
135
+ };
136
+
137
+ // Extract type (first word)
138
+ const typeMatch = argsStr.match(/^(\w+)/);
139
+ if (typeMatch) {
140
+ result.type = normalizeLowercaseStringOrEmpty(typeMatch[1]);
141
+ argsStr = argsStr.slice(typeMatch[0].length).trim();
142
+ }
143
+
144
+ // Extract quoted arguments
145
+ const quotedRegex = /"([^"]*?)"/g;
146
+ let match;
147
+ while ((match = quotedRegex.exec(argsStr)) !== null) {
148
+ result.args.push(match[1]);
149
+ }
150
+
151
+ // Extract flags (--key value or --key "value")
152
+ const flagRegex = /--(\w+)\s+(?:"([^"]*?)"|(\S+))/g;
153
+ while ((match = flagRegex.exec(argsStr)) !== null) {
154
+ result.flags[match[1]] = match[2] ?? match[3];
155
+ }
156
+
157
+ return result;
158
+ }
159
+
160
+ export function registerLineCardCommand(api: AutoBotPluginApi): void {
161
+ api.registerCommand({
162
+ name: "card",
163
+ description: "Send a rich card message (LINE).",
164
+ acceptsArgs: true,
165
+ requireAuth: false,
166
+ handler: async (ctx) => {
167
+ const argsStr = ctx.args?.trim() ?? "";
168
+ if (!argsStr) {
169
+ return { text: CARD_USAGE };
170
+ }
171
+
172
+ const parsed = parseCardArgs(argsStr);
173
+ const { type, args, flags } = parsed;
174
+
175
+ if (!type) {
176
+ return { text: CARD_USAGE };
177
+ }
178
+
179
+ // Only LINE supports rich cards; fallback to text elsewhere.
180
+ if (ctx.channel !== "line") {
181
+ const fallbackText = args.join(" - ");
182
+ return { text: `[${type} card] ${fallbackText}`.trim() };
183
+ }
184
+
185
+ try {
186
+ switch (type) {
187
+ case "info": {
188
+ const [title = "Info", body = "", footer] = args;
189
+ const bubble = createInfoCard(title, body, footer);
190
+ return buildLineReply({
191
+ flexMessage: {
192
+ altText: `${title}: ${body}`.slice(0, 400),
193
+ contents: bubble,
194
+ },
195
+ });
196
+ }
197
+
198
+ case "image": {
199
+ const [title = "Image", caption = ""] = args;
200
+ const imageUrl = flags.url || flags.image;
201
+ if (!imageUrl) {
202
+ return { text: "Error: Image card requires --url <image-url>" };
203
+ }
204
+ const bubble = createImageCard(imageUrl, title, caption);
205
+ return buildLineReply({
206
+ flexMessage: {
207
+ altText: `${title}: ${caption}`.slice(0, 400),
208
+ contents: bubble,
209
+ },
210
+ });
211
+ }
212
+
213
+ case "action": {
214
+ const [title = "Actions", body = ""] = args;
215
+ const actions = parseActions(flags.actions);
216
+ if (actions.length === 0) {
217
+ return { text: 'Error: Action card requires --actions "Label1|data1,Label2|data2"' };
218
+ }
219
+ const bubble = createActionCard(title, body, actions, {
220
+ imageUrl: flags.url || flags.image,
221
+ });
222
+ return buildLineReply({
223
+ flexMessage: {
224
+ altText: `${title}: ${body}`.slice(0, 400),
225
+ contents: bubble,
226
+ },
227
+ });
228
+ }
229
+
230
+ case "list": {
231
+ const [title = "List", itemsStr = ""] = args;
232
+ const items = parseListItems(itemsStr || flags.items || "");
233
+ if (items.length === 0) {
234
+ return {
235
+ text: 'Error: List card requires items. Usage: /card list "Title" "Item1|Desc1,Item2|Desc2"',
236
+ };
237
+ }
238
+ const bubble = createListCard(title, items);
239
+ return buildLineReply({
240
+ flexMessage: {
241
+ altText: `${title}: ${items.map((i) => i.title).join(", ")}`.slice(0, 400),
242
+ contents: bubble,
243
+ },
244
+ });
245
+ }
246
+
247
+ case "receipt": {
248
+ const [title = "Receipt", itemsStr = ""] = args;
249
+ const items = parseReceiptItems(itemsStr || flags.items || "");
250
+ const total = flags.total ? { label: "Total", value: flags.total } : undefined;
251
+ const footer = flags.footer;
252
+
253
+ if (items.length === 0) {
254
+ return {
255
+ text: 'Error: Receipt card requires items. Usage: /card receipt "Title" "Item1:$10,Item2:$20" --total "$30"',
256
+ };
257
+ }
258
+
259
+ const bubble = createReceiptCard({ title, items, total, footer });
260
+ return buildLineReply({
261
+ flexMessage: {
262
+ altText: `${title}: ${items.map((i) => `${i.name} ${i.value}`).join(", ")}`.slice(
263
+ 0,
264
+ 400,
265
+ ),
266
+ contents: bubble,
267
+ },
268
+ });
269
+ }
270
+
271
+ case "confirm": {
272
+ const [question = "Confirm?"] = args;
273
+ const yesStr = flags.yes || "Yes|yes";
274
+ const noStr = flags.no || "No|no";
275
+
276
+ const [yesLabel, yesData] = yesStr.split("|").map((s) => s.trim());
277
+ const [noLabel, noData] = noStr.split("|").map((s) => s.trim());
278
+
279
+ return buildLineReply({
280
+ templateMessage: {
281
+ type: "confirm",
282
+ text: question,
283
+ confirmLabel: yesLabel || "Yes",
284
+ confirmData: yesData || "yes",
285
+ cancelLabel: noLabel || "No",
286
+ cancelData: noData || "no",
287
+ altText: question,
288
+ },
289
+ });
290
+ }
291
+
292
+ case "buttons": {
293
+ const [title = "Menu", text = "Choose an option"] = args;
294
+ const actionsStr = flags.actions || "";
295
+ const actionParts = parseActions(actionsStr);
296
+
297
+ if (actionParts.length === 0) {
298
+ return { text: 'Error: Buttons card requires --actions "Label1|data1,Label2|data2"' };
299
+ }
300
+
301
+ const templateActions: Array<{
302
+ type: "message" | "uri" | "postback";
303
+ label: string;
304
+ data?: string;
305
+ uri?: string;
306
+ }> = actionParts.map((a) => {
307
+ const action = a.action;
308
+ const label = action.label ?? a.label;
309
+ if (action.type === "uri") {
310
+ return { type: "uri" as const, label, uri: (action as { uri: string }).uri };
311
+ }
312
+ if (action.type === "postback") {
313
+ return {
314
+ type: "postback" as const,
315
+ label,
316
+ data: (action as { data: string }).data,
317
+ };
318
+ }
319
+ return {
320
+ type: "message" as const,
321
+ label,
322
+ data: (action as { text: string }).text,
323
+ };
324
+ });
325
+
326
+ return buildLineReply({
327
+ templateMessage: {
328
+ type: "buttons",
329
+ title,
330
+ text,
331
+ thumbnailImageUrl: flags.url || flags.image,
332
+ actions: templateActions,
333
+ },
334
+ });
335
+ }
336
+
337
+ default:
338
+ return {
339
+ text: `Unknown card type: "${type}". Available types: info, image, action, list, receipt, confirm, buttons`,
340
+ };
341
+ }
342
+ } catch (err) {
343
+ return { text: `Error creating card: ${String(err)}` };
344
+ }
345
+ },
346
+ });
347
+ }
@@ -0,0 +1,14 @@
1
+ export function resolveLineChannelAccessToken(
2
+ explicit: string | undefined,
3
+ params: { accountId: string; channelAccessToken: string },
4
+ ): string {
5
+ if (explicit?.trim()) {
6
+ return explicit.trim();
7
+ }
8
+ if (!params.channelAccessToken) {
9
+ throw new Error(
10
+ `LINE channel access token missing for account "${params.accountId}" (set channels.line.channelAccessToken or LINE_CHANNEL_ACCESS_TOKEN).`,
11
+ );
12
+ }
13
+ return params.channelAccessToken.trim();
14
+ }
@@ -0,0 +1,17 @@
1
+ export { clearAccountEntryFields } from "autobot/plugin-sdk/core";
2
+ import { DEFAULT_ACCOUNT_ID } from "autobot/plugin-sdk/account-id";
3
+ import type { AutoBotConfig } from "autobot/plugin-sdk/account-resolution";
4
+ import type { ChannelPlugin } from "autobot/plugin-sdk/core";
5
+ import { listLineAccountIds, resolveDefaultLineAccountId, resolveLineAccount } from "./accounts.js";
6
+ import { resolveExactLineGroupConfigKey } from "./group-keys.js";
7
+ import type { LineConfig, ResolvedLineAccount } from "./types.js";
8
+
9
+ export {
10
+ DEFAULT_ACCOUNT_ID,
11
+ listLineAccountIds,
12
+ resolveDefaultLineAccountId,
13
+ resolveExactLineGroupConfigKey,
14
+ resolveLineAccount,
15
+ };
16
+
17
+ export type { ChannelPlugin, LineConfig, AutoBotConfig, ResolvedLineAccount };
@@ -0,0 +1,48 @@
1
+ import { describeWebhookAccountSnapshot } from "autobot/plugin-sdk/account-helpers";
2
+ import { hasLineCredentials } from "./account-helpers.js";
3
+ import { type ChannelPlugin, type ResolvedLineAccount } from "./channel-api.js";
4
+ import { lineConfigAdapter } from "./config-adapter.js";
5
+ import { LineChannelConfigSchema } from "./config-schema.js";
6
+
7
+ const lineChannelMeta = {
8
+ id: "line",
9
+ label: "LINE",
10
+ selectionLabel: "LINE (Messaging API)",
11
+ detailLabel: "LINE Bot",
12
+ docsPath: "/channels/line",
13
+ docsLabel: "line",
14
+ blurb: "LINE Messaging API bot for Japan/Taiwan/Thailand markets.",
15
+ systemImage: "message.fill",
16
+ } as const;
17
+
18
+ export const lineChannelPluginCommon = {
19
+ meta: {
20
+ ...lineChannelMeta,
21
+ quickstartAllowFrom: true,
22
+ },
23
+ capabilities: {
24
+ chatTypes: ["direct", "group"],
25
+ reactions: false,
26
+ threads: false,
27
+ media: true,
28
+ nativeCommands: false,
29
+ blockStreaming: true,
30
+ },
31
+ reload: { configPrefixes: ["channels.line"] },
32
+ configSchema: LineChannelConfigSchema,
33
+ config: {
34
+ ...lineConfigAdapter,
35
+ isConfigured: (account: ResolvedLineAccount) => hasLineCredentials(account),
36
+ describeAccount: (account: ResolvedLineAccount) =>
37
+ describeWebhookAccountSnapshot({
38
+ account,
39
+ configured: hasLineCredentials(account),
40
+ extra: {
41
+ tokenSource: account.tokenSource ?? undefined,
42
+ },
43
+ }),
44
+ },
45
+ } satisfies Pick<
46
+ ChannelPlugin<ResolvedLineAccount>,
47
+ "meta" | "capabilities" | "reload" | "configSchema" | "config"
48
+ >;
@@ -0,0 +1,3 @@
1
+ export { monitorLineProvider } from "./monitor.js";
2
+ export { probeLineBot } from "./probe.js";
3
+ export { pushMessageLine } from "./send.js";
@@ -0,0 +1,11 @@
1
+ import { type ChannelPlugin, type ResolvedLineAccount } from "./channel-api.js";
2
+ import { lineChannelPluginCommon } from "./channel-shared.js";
3
+ import { lineSetupAdapter } from "./setup-core.js";
4
+ import { lineSetupWizard } from "./setup-surface.js";
5
+
6
+ export const lineSetupPlugin: ChannelPlugin<ResolvedLineAccount> = {
7
+ id: "line",
8
+ ...lineChannelPluginCommon,
9
+ setupWizard: lineSetupWizard,
10
+ setup: lineSetupAdapter,
11
+ };
package/src/channel.ts ADDED
@@ -0,0 +1,155 @@
1
+ import { createChatChannelPlugin } from "autobot/plugin-sdk/channel-core";
2
+ import { createPairingPrefixStripper } from "autobot/plugin-sdk/channel-pairing";
3
+ import { createRestrictSendersChannelSecurity } from "autobot/plugin-sdk/channel-policy";
4
+ import { createEmptyChannelDirectoryAdapter } from "autobot/plugin-sdk/directory-runtime";
5
+ import { createLazyRuntimeModule } from "autobot/plugin-sdk/lazy-runtime";
6
+ import { resolveLineAccount } from "./accounts.js";
7
+ import { lineBindingsAdapter } from "./bindings.js";
8
+ import { type ChannelPlugin, type ResolvedLineAccount } from "./channel-api.js";
9
+ import { lineChannelPluginCommon } from "./channel-shared.js";
10
+ import { lineGatewayAdapter } from "./gateway.js";
11
+ import { resolveLineGroupRequireMention } from "./group-policy.js";
12
+ import { lineMessageAdapter, lineOutboundAdapter } from "./outbound.js";
13
+ import { hasLineDirectives, parseLineDirectives } from "./reply-payload-transform.js";
14
+ import { getLineRuntime } from "./runtime.js";
15
+ import { lineSetupAdapter } from "./setup-core.js";
16
+ import { lineSetupWizard } from "./setup-surface.js";
17
+ import { lineStatusAdapter } from "./status.js";
18
+
19
+ const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
20
+
21
+ const lineSecurityAdapter = createRestrictSendersChannelSecurity<ResolvedLineAccount>({
22
+ channelKey: "line",
23
+ resolveDmPolicy: (account) => account.config.dmPolicy,
24
+ resolveDmAllowFrom: (account) => account.config.allowFrom,
25
+ resolveGroupPolicy: (account) => account.config.groupPolicy,
26
+ surface: "LINE groups",
27
+ openScope: "any member in groups",
28
+ groupPolicyPath: "channels.line.groupPolicy",
29
+ groupAllowFromPath: "channels.line.groupAllowFrom",
30
+ mentionGated: false,
31
+ policyPathSuffix: "dmPolicy",
32
+ approveHint: "autobot pairing approve line <code>",
33
+ normalizeDmEntry: (raw) => raw.replace(/^line:(?:user:)?/i, ""),
34
+ });
35
+
36
+ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = createChatChannelPlugin({
37
+ base: {
38
+ id: "line",
39
+ ...lineChannelPluginCommon,
40
+ setupWizard: lineSetupWizard,
41
+ groups: {
42
+ resolveRequireMention: resolveLineGroupRequireMention,
43
+ },
44
+ messaging: {
45
+ targetPrefixes: ["line"],
46
+ normalizeTarget: (target) => {
47
+ const trimmed = target.trim();
48
+ if (!trimmed) {
49
+ return undefined;
50
+ }
51
+ return trimmed.replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
52
+ },
53
+ resolveInboundConversation: lineBindingsAdapter.resolveInboundConversation,
54
+ transformReplyPayload: ({ payload }) => {
55
+ if (!payload.text || !hasLineDirectives(payload.text)) {
56
+ return payload;
57
+ }
58
+ return parseLineDirectives(payload);
59
+ },
60
+ targetResolver: {
61
+ looksLikeId: (id) => {
62
+ const trimmed = id?.trim();
63
+ if (!trimmed) {
64
+ return false;
65
+ }
66
+ return /^[UCR][a-f0-9]{32}$/i.test(trimmed) || /^line:/i.test(trimmed);
67
+ },
68
+ hint: "<userId|groupId|roomId>",
69
+ },
70
+ },
71
+ directory: createEmptyChannelDirectoryAdapter(),
72
+ setup: lineSetupAdapter,
73
+ status: lineStatusAdapter,
74
+ gateway: lineGatewayAdapter,
75
+ message: lineMessageAdapter,
76
+ bindings: lineBindingsAdapter,
77
+ conversationBindings: {
78
+ defaultTopLevelPlacement: "current",
79
+ },
80
+ agentPrompt: {
81
+ messageToolHints: () => [
82
+ "",
83
+ "### LINE Rich Messages",
84
+ "LINE supports rich visual messages. Use these directives in your reply when appropriate:",
85
+ "",
86
+ "**Quick Replies** (bottom button suggestions):",
87
+ " [[quick_replies: Option 1, Option 2, Option 3]]",
88
+ "",
89
+ "**Location** (map pin):",
90
+ " [[location: Place Name | Address | latitude | longitude]]",
91
+ "",
92
+ "**Confirm Dialog** (yes/no prompt):",
93
+ " [[confirm: Question text? | Yes Label | No Label]]",
94
+ "",
95
+ "**Button Menu** (title + text + buttons):",
96
+ " [[buttons: Title | Description | Btn1:action1, Btn2:https://url.com]]",
97
+ "",
98
+ "**Media Player Card** (music status):",
99
+ " [[media_player: Song Title | Artist Name | Source | https://albumart.url | playing]]",
100
+ " - Status: 'playing' or 'paused' (optional)",
101
+ "",
102
+ "**Event Card** (calendar events, meetings):",
103
+ " [[event: Event Title | Date | Time | Location | Description]]",
104
+ " - Time, Location, Description are optional",
105
+ "",
106
+ "**Agenda Card** (multiple events/schedule):",
107
+ " [[agenda: Schedule Title | Event1:9:00 AM, Event2:12:00 PM, Event3:3:00 PM]]",
108
+ "",
109
+ "**Device Control Card** (smart devices, TVs, etc.):",
110
+ " [[device: Device Name | Device Type | Status | Control1:data1, Control2:data2]]",
111
+ "",
112
+ "**Apple TV Remote** (full D-pad + transport):",
113
+ " [[appletv_remote: Apple TV | Playing]]",
114
+ "",
115
+ "**Auto-converted**: Markdown tables become Flex cards, code blocks become styled cards.",
116
+ "",
117
+ "When to use rich messages:",
118
+ "- Use [[quick_replies:...]] when offering 2-4 clear options",
119
+ "- Use [[confirm:...]] for yes/no decisions",
120
+ "- Use [[buttons:...]] for menus with actions/links",
121
+ "- Use [[location:...]] when sharing a place",
122
+ "- Use [[media_player:...]] when showing what's playing",
123
+ "- Use [[event:...]] for calendar event details",
124
+ "- Use [[agenda:...]] for a day's schedule or event list",
125
+ "- Use [[device:...]] for smart device status/controls",
126
+ "- Tables/code in your response auto-convert to visual cards",
127
+ ],
128
+ },
129
+ },
130
+ pairing: {
131
+ text: {
132
+ idLabel: "lineUserId",
133
+ message: "AutoBot: your access has been approved.",
134
+ normalizeAllowEntry: createPairingPrefixStripper(/^line:(?:user:)?/i),
135
+ notify: async ({ cfg, id, message }) => {
136
+ const account = (getLineRuntime().channel.line?.resolveLineAccount ?? resolveLineAccount)({
137
+ cfg,
138
+ });
139
+ if (!account.channelAccessToken) {
140
+ throw new Error("LINE channel access token not configured");
141
+ }
142
+ const pushMessageLine =
143
+ getLineRuntime().channel.line?.pushMessageLine ??
144
+ (await loadLineChannelRuntime()).pushMessageLine;
145
+ await pushMessageLine(id, message, {
146
+ cfg,
147
+ accountId: account.accountId,
148
+ channelAccessToken: account.channelAccessToken,
149
+ });
150
+ },
151
+ },
152
+ },
153
+ security: lineSecurityAdapter,
154
+ outbound: lineOutboundAdapter,
155
+ });
@@ -0,0 +1,29 @@
1
+ import { createScopedChannelConfigAdapter } from "autobot/plugin-sdk/channel-config-helpers";
2
+ import {
3
+ listLineAccountIds,
4
+ resolveDefaultLineAccountId,
5
+ resolveLineAccount,
6
+ type ResolvedLineAccount,
7
+ } from "./channel-api.js";
8
+
9
+ function normalizeLineAllowFrom(entry: string): string {
10
+ return entry.replace(/^line:(?:user:)?/i, "");
11
+ }
12
+
13
+ export const lineConfigAdapter = createScopedChannelConfigAdapter<
14
+ ResolvedLineAccount,
15
+ ResolvedLineAccount
16
+ >({
17
+ sectionKey: "line",
18
+ listAccountIds: listLineAccountIds,
19
+ resolveAccount: (cfg, accountId) =>
20
+ resolveLineAccount({ cfg, accountId: accountId ?? undefined }),
21
+ defaultAccountId: resolveDefaultLineAccountId,
22
+ clearBaseFields: ["channelSecret", "tokenFile", "secretFile"],
23
+ resolveAllowFrom: (account) => account.config.allowFrom,
24
+ formatAllowFrom: (allowFrom) =>
25
+ allowFrom
26
+ .map((entry) => String(entry).trim())
27
+ .filter(Boolean)
28
+ .map(normalizeLineAllowFrom),
29
+ });