@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
package/src/send.ts ADDED
@@ -0,0 +1,531 @@
1
+ import { messagingApi } from "@line/bot-sdk";
2
+ import { recordChannelActivity } from "autobot/plugin-sdk/channel-activity-runtime";
3
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
4
+ import { requireRuntimeConfig } from "autobot/plugin-sdk/plugin-config-runtime";
5
+ import { logVerbose } from "autobot/plugin-sdk/runtime-env";
6
+ import { resolveLineAccount } from "./accounts.js";
7
+ import { resolveLineChannelAccessToken } from "./channel-access-token.js";
8
+ import { validateLineMediaUrl } from "./outbound-media.js";
9
+ import { createLineSendReceipt } from "./send-receipt.js";
10
+ import type { LineSendResult } from "./types.js";
11
+
12
+ type Message = messagingApi.Message;
13
+ type TextMessage = messagingApi.TextMessage;
14
+ type ImageMessage = messagingApi.ImageMessage;
15
+ type VideoMessage = messagingApi.VideoMessage & { trackingId?: string };
16
+ type AudioMessage = messagingApi.AudioMessage;
17
+ type LocationMessage = messagingApi.LocationMessage;
18
+ type FlexMessage = messagingApi.FlexMessage;
19
+ type FlexContainer = messagingApi.FlexContainer;
20
+ type TemplateMessage = messagingApi.TemplateMessage;
21
+ type QuickReply = messagingApi.QuickReply;
22
+ type QuickReplyItem = messagingApi.QuickReplyItem;
23
+
24
+ const userProfileCache = new Map<
25
+ string,
26
+ { displayName: string; pictureUrl?: string; fetchedAt: number }
27
+ >();
28
+ const PROFILE_CACHE_TTL_MS = 5 * 60 * 1000;
29
+
30
+ interface LineSendOpts {
31
+ cfg: AutoBotConfig;
32
+ channelAccessToken?: string;
33
+ accountId?: string;
34
+ verbose?: boolean;
35
+ mediaUrl?: string;
36
+ mediaKind?: "image" | "video" | "audio";
37
+ previewImageUrl?: string;
38
+ durationMs?: number;
39
+ trackingId?: string;
40
+ replyToken?: string;
41
+ }
42
+
43
+ type LineClientOpts = Pick<LineSendOpts, "cfg" | "channelAccessToken" | "accountId">;
44
+ type LinePushOpts = Pick<LineSendOpts, "cfg" | "channelAccessToken" | "accountId" | "verbose">;
45
+
46
+ interface LinePushBehavior {
47
+ errorContext?: string;
48
+ verboseMessage?: (chatId: string, messageCount: number) => string;
49
+ }
50
+
51
+ interface LineReplyBehavior {
52
+ verboseMessage?: (messageCount: number) => string;
53
+ }
54
+
55
+ function normalizeTarget(to: string): string {
56
+ const trimmed = to.trim();
57
+ if (!trimmed) {
58
+ throw new Error("Recipient is required for LINE sends");
59
+ }
60
+
61
+ const normalized = trimmed
62
+ .replace(/^line:group:/i, "")
63
+ .replace(/^line:room:/i, "")
64
+ .replace(/^line:user:/i, "")
65
+ .replace(/^line:/i, "");
66
+
67
+ if (!normalized) {
68
+ throw new Error("Recipient is required for LINE sends");
69
+ }
70
+
71
+ // Real LINE chat ids are a capital C/U/R followed by 32 lowercase hex chars
72
+ // (33 chars total) and are case-sensitive — push returns HTTP 400 otherwise.
73
+ // Reject values that match the LINE id shape but lost their leading capital
74
+ // so the failure is surfaced as a permanent error (recovery moves the entry
75
+ // to failed/ immediately instead of silently retrying 5 times). Short test
76
+ // fixtures (e.g. "U123") are left alone. autobot/autobot#81628
77
+ if (normalized.length >= 33 && !/^[CUR]/.test(normalized)) {
78
+ throw new Error(
79
+ `Recipient is not a valid LINE id (case-sensitive; expected leading capital C/U/R): ${normalized.slice(0, 4)}…`,
80
+ );
81
+ }
82
+
83
+ return normalized;
84
+ }
85
+
86
+ function isLineUserChatId(chatId: string): boolean {
87
+ return /^U/i.test(chatId);
88
+ }
89
+
90
+ function createLineMessagingClient(opts: LineClientOpts): {
91
+ account: ReturnType<typeof resolveLineAccount>;
92
+ client: messagingApi.MessagingApiClient;
93
+ } {
94
+ const cfg = requireRuntimeConfig(opts.cfg, "LINE send");
95
+ const account = resolveLineAccount({
96
+ cfg,
97
+ accountId: opts.accountId,
98
+ });
99
+ const token = resolveLineChannelAccessToken(opts.channelAccessToken, account);
100
+ const client = new messagingApi.MessagingApiClient({
101
+ channelAccessToken: token,
102
+ });
103
+ return { account, client };
104
+ }
105
+
106
+ function createLinePushContext(
107
+ to: string,
108
+ opts: LineClientOpts,
109
+ ): {
110
+ account: ReturnType<typeof resolveLineAccount>;
111
+ client: messagingApi.MessagingApiClient;
112
+ chatId: string;
113
+ } {
114
+ const { account, client } = createLineMessagingClient(opts);
115
+ const chatId = normalizeTarget(to);
116
+ return { account, client, chatId };
117
+ }
118
+
119
+ function createTextMessage(text: string): TextMessage {
120
+ return { type: "text", text };
121
+ }
122
+
123
+ export function createImageMessage(
124
+ originalContentUrl: string,
125
+ previewImageUrl?: string,
126
+ ): ImageMessage {
127
+ return {
128
+ type: "image",
129
+ originalContentUrl,
130
+ previewImageUrl: previewImageUrl ?? originalContentUrl,
131
+ };
132
+ }
133
+
134
+ export function createVideoMessage(
135
+ originalContentUrl: string,
136
+ previewImageUrl: string,
137
+ trackingId?: string,
138
+ ): VideoMessage {
139
+ return {
140
+ type: "video",
141
+ originalContentUrl,
142
+ previewImageUrl,
143
+ ...(trackingId ? { trackingId } : {}),
144
+ };
145
+ }
146
+
147
+ export function createAudioMessage(originalContentUrl: string, durationMs: number): AudioMessage {
148
+ return {
149
+ type: "audio",
150
+ originalContentUrl,
151
+ duration: durationMs,
152
+ };
153
+ }
154
+
155
+ export function createLocationMessage(location: {
156
+ title: string;
157
+ address: string;
158
+ latitude: number;
159
+ longitude: number;
160
+ }): LocationMessage {
161
+ return {
162
+ type: "location",
163
+ title: location.title.slice(0, 100),
164
+ address: location.address.slice(0, 100),
165
+ latitude: location.latitude,
166
+ longitude: location.longitude,
167
+ };
168
+ }
169
+
170
+ function logLineHttpError(err: unknown, context: string): void {
171
+ if (!err || typeof err !== "object") {
172
+ return;
173
+ }
174
+ const { status, statusText, body } = err as {
175
+ status?: number;
176
+ statusText?: string;
177
+ body?: string;
178
+ };
179
+ if (typeof body === "string") {
180
+ const summary = status ? `${status} ${statusText ?? ""}`.trim() : "unknown status";
181
+ logVerbose(`line: ${context} failed (${summary}): ${body}`);
182
+ }
183
+ }
184
+
185
+ function recordLineOutboundActivity(accountId: string): void {
186
+ recordChannelActivity({
187
+ channel: "line",
188
+ accountId,
189
+ direction: "outbound",
190
+ });
191
+ }
192
+
193
+ function resolveLineReceiptKind(messages: readonly Message[]) {
194
+ const types = new Set(messages.map((message) => message.type));
195
+ if (types.has("audio")) {
196
+ return "voice";
197
+ }
198
+ if (types.has("image") || types.has("video")) {
199
+ return "media";
200
+ }
201
+ if (types.has("flex") || types.has("template") || types.has("location")) {
202
+ return "card";
203
+ }
204
+ if (types.has("text")) {
205
+ return "text";
206
+ }
207
+ return "unknown";
208
+ }
209
+
210
+ async function pushLineMessages(
211
+ to: string,
212
+ messages: Message[],
213
+ opts: LinePushOpts,
214
+ behavior: LinePushBehavior = {},
215
+ ): Promise<LineSendResult> {
216
+ if (messages.length === 0) {
217
+ throw new Error("Message must be non-empty for LINE sends");
218
+ }
219
+
220
+ const { account, client, chatId } = createLinePushContext(to, opts);
221
+ const pushRequest = client.pushMessage({
222
+ to: chatId,
223
+ messages,
224
+ });
225
+
226
+ if (behavior.errorContext) {
227
+ await pushRequest.catch((err) => {
228
+ logLineHttpError(err, behavior.errorContext!);
229
+ throw err;
230
+ });
231
+ } else {
232
+ await pushRequest;
233
+ }
234
+
235
+ recordLineOutboundActivity(account.accountId);
236
+
237
+ if (opts.verbose) {
238
+ const logMessage =
239
+ behavior.verboseMessage?.(chatId, messages.length) ??
240
+ `line: pushed ${messages.length} messages to ${chatId}`;
241
+ logVerbose(logMessage);
242
+ }
243
+
244
+ return {
245
+ messageId: "push",
246
+ chatId,
247
+ receipt: createLineSendReceipt({
248
+ messageId: "push",
249
+ chatId,
250
+ kind: resolveLineReceiptKind(messages),
251
+ messageCount: messages.length,
252
+ }),
253
+ };
254
+ }
255
+
256
+ async function replyLineMessages(
257
+ replyToken: string,
258
+ messages: Message[],
259
+ opts: LinePushOpts,
260
+ behavior: LineReplyBehavior = {},
261
+ ): Promise<void> {
262
+ const { account, client } = createLineMessagingClient(opts);
263
+
264
+ await client.replyMessage({
265
+ replyToken,
266
+ messages,
267
+ });
268
+
269
+ recordLineOutboundActivity(account.accountId);
270
+
271
+ if (opts.verbose) {
272
+ logVerbose(
273
+ behavior.verboseMessage?.(messages.length) ??
274
+ `line: replied with ${messages.length} messages`,
275
+ );
276
+ }
277
+ }
278
+
279
+ export async function sendMessageLine(
280
+ to: string,
281
+ text: string,
282
+ opts: LineSendOpts,
283
+ ): Promise<LineSendResult> {
284
+ const chatId = normalizeTarget(to);
285
+ const messages: Message[] = [];
286
+
287
+ const mediaUrl = opts.mediaUrl?.trim();
288
+ if (mediaUrl) {
289
+ await validateLineMediaUrl(mediaUrl);
290
+ switch (opts.mediaKind) {
291
+ case "video": {
292
+ const previewImageUrl = opts.previewImageUrl?.trim();
293
+ if (!previewImageUrl) {
294
+ throw new Error("LINE video messages require previewImageUrl to reference an image URL");
295
+ }
296
+ await validateLineMediaUrl(previewImageUrl);
297
+ const trackingId = isLineUserChatId(chatId) ? opts.trackingId : undefined;
298
+ messages.push(createVideoMessage(mediaUrl, previewImageUrl, trackingId));
299
+ break;
300
+ }
301
+ case "audio":
302
+ messages.push(createAudioMessage(mediaUrl, opts.durationMs ?? 60000));
303
+ break;
304
+ case "image":
305
+ default:
306
+ // Backward compatibility: keep image as default when media kind is unspecified.
307
+ {
308
+ const previewImageUrl = opts.previewImageUrl?.trim() || mediaUrl;
309
+ await validateLineMediaUrl(previewImageUrl);
310
+ messages.push(createImageMessage(mediaUrl, previewImageUrl));
311
+ }
312
+ break;
313
+ }
314
+ }
315
+
316
+ if (text?.trim()) {
317
+ messages.push(createTextMessage(text.trim()));
318
+ }
319
+
320
+ if (messages.length === 0) {
321
+ throw new Error("Message must be non-empty for LINE sends");
322
+ }
323
+
324
+ if (opts.replyToken) {
325
+ await replyLineMessages(opts.replyToken, messages, opts, {
326
+ verboseMessage: () => `line: replied to ${chatId}`,
327
+ });
328
+
329
+ return {
330
+ messageId: "reply",
331
+ chatId,
332
+ receipt: createLineSendReceipt({
333
+ messageId: "reply",
334
+ chatId,
335
+ kind: resolveLineReceiptKind(messages),
336
+ messageCount: messages.length,
337
+ }),
338
+ };
339
+ }
340
+
341
+ return pushLineMessages(chatId, messages, opts, {
342
+ verboseMessage: (resolvedChatId) => `line: pushed message to ${resolvedChatId}`,
343
+ });
344
+ }
345
+
346
+ export async function pushMessageLine(
347
+ to: string,
348
+ text: string,
349
+ opts: LineSendOpts,
350
+ ): Promise<LineSendResult> {
351
+ return sendMessageLine(to, text, { ...opts, replyToken: undefined });
352
+ }
353
+
354
+ export async function replyMessageLine(
355
+ replyToken: string,
356
+ messages: Message[],
357
+ opts: LinePushOpts,
358
+ ): Promise<void> {
359
+ await replyLineMessages(replyToken, messages, opts);
360
+ }
361
+
362
+ export async function pushMessagesLine(
363
+ to: string,
364
+ messages: Message[],
365
+ opts: LinePushOpts,
366
+ ): Promise<LineSendResult> {
367
+ return pushLineMessages(to, messages, opts, {
368
+ errorContext: "push message",
369
+ });
370
+ }
371
+
372
+ export function createFlexMessage(
373
+ altText: string,
374
+ contents: messagingApi.FlexContainer,
375
+ ): messagingApi.FlexMessage {
376
+ return {
377
+ type: "flex",
378
+ altText,
379
+ contents,
380
+ };
381
+ }
382
+
383
+ export async function pushImageMessage(
384
+ to: string,
385
+ originalContentUrl: string,
386
+ previewImageUrl: string | undefined,
387
+ opts: LinePushOpts,
388
+ ): Promise<LineSendResult> {
389
+ await validateLineMediaUrl(originalContentUrl);
390
+ if (previewImageUrl) {
391
+ await validateLineMediaUrl(previewImageUrl);
392
+ }
393
+ return pushLineMessages(to, [createImageMessage(originalContentUrl, previewImageUrl)], opts, {
394
+ verboseMessage: (chatId) => `line: pushed image to ${chatId}`,
395
+ });
396
+ }
397
+
398
+ export async function pushLocationMessage(
399
+ to: string,
400
+ location: {
401
+ title: string;
402
+ address: string;
403
+ latitude: number;
404
+ longitude: number;
405
+ },
406
+ opts: LinePushOpts,
407
+ ): Promise<LineSendResult> {
408
+ return pushLineMessages(to, [createLocationMessage(location)], opts, {
409
+ verboseMessage: (chatId) => `line: pushed location to ${chatId}`,
410
+ });
411
+ }
412
+
413
+ export async function pushFlexMessage(
414
+ to: string,
415
+ altText: string,
416
+ contents: FlexContainer,
417
+ opts: LinePushOpts,
418
+ ): Promise<LineSendResult> {
419
+ const flexMessage: FlexMessage = {
420
+ type: "flex",
421
+ altText: altText.slice(0, 400),
422
+ contents,
423
+ };
424
+
425
+ return pushLineMessages(to, [flexMessage], opts, {
426
+ errorContext: "push flex message",
427
+ verboseMessage: (chatId) => `line: pushed flex message to ${chatId}`,
428
+ });
429
+ }
430
+
431
+ export async function pushTemplateMessage(
432
+ to: string,
433
+ template: TemplateMessage,
434
+ opts: LinePushOpts,
435
+ ): Promise<LineSendResult> {
436
+ return pushLineMessages(to, [template], opts, {
437
+ verboseMessage: (chatId) => `line: pushed template message to ${chatId}`,
438
+ });
439
+ }
440
+
441
+ export async function pushTextMessageWithQuickReplies(
442
+ to: string,
443
+ text: string,
444
+ quickReplyLabels: string[],
445
+ opts: LinePushOpts,
446
+ ): Promise<LineSendResult> {
447
+ const message = createTextMessageWithQuickReplies(text, quickReplyLabels);
448
+
449
+ return pushLineMessages(to, [message], opts, {
450
+ verboseMessage: (chatId) => `line: pushed message with quick replies to ${chatId}`,
451
+ });
452
+ }
453
+
454
+ export function createQuickReplyItems(labels: string[]): QuickReply {
455
+ const items: QuickReplyItem[] = labels.slice(0, 13).map((label) => ({
456
+ type: "action",
457
+ action: {
458
+ type: "message",
459
+ label: label.slice(0, 20),
460
+ text: label,
461
+ },
462
+ }));
463
+ return { items };
464
+ }
465
+
466
+ export function createTextMessageWithQuickReplies(
467
+ text: string,
468
+ quickReplyLabels: string[],
469
+ ): TextMessage & { quickReply: QuickReply } {
470
+ return {
471
+ type: "text",
472
+ text,
473
+ quickReply: createQuickReplyItems(quickReplyLabels),
474
+ };
475
+ }
476
+
477
+ export async function showLoadingAnimation(
478
+ chatId: string,
479
+ opts: LineClientOpts & { loadingSeconds?: number },
480
+ ): Promise<void> {
481
+ const { client } = createLineMessagingClient(opts);
482
+
483
+ try {
484
+ await client.showLoadingAnimation({
485
+ chatId: normalizeTarget(chatId),
486
+ loadingSeconds: opts.loadingSeconds ?? 20,
487
+ });
488
+ logVerbose(`line: showing loading animation to ${chatId}`);
489
+ } catch (err) {
490
+ logVerbose(`line: loading animation failed (non-fatal): ${String(err)}`);
491
+ }
492
+ }
493
+
494
+ export async function getUserProfile(
495
+ userId: string,
496
+ opts: LineClientOpts & { useCache?: boolean },
497
+ ): Promise<{ displayName: string; pictureUrl?: string } | null> {
498
+ const useCache = opts.useCache ?? true;
499
+
500
+ if (useCache) {
501
+ const cached = userProfileCache.get(userId);
502
+ if (cached && Date.now() - cached.fetchedAt < PROFILE_CACHE_TTL_MS) {
503
+ return { displayName: cached.displayName, pictureUrl: cached.pictureUrl };
504
+ }
505
+ }
506
+
507
+ const { client } = createLineMessagingClient(opts);
508
+
509
+ try {
510
+ const profile = await client.getProfile(userId);
511
+ const result = {
512
+ displayName: profile.displayName,
513
+ pictureUrl: profile.pictureUrl,
514
+ };
515
+
516
+ userProfileCache.set(userId, {
517
+ ...result,
518
+ fetchedAt: Date.now(),
519
+ });
520
+
521
+ return result;
522
+ } catch (err) {
523
+ logVerbose(`line: failed to fetch profile for ${userId}: ${String(err)}`);
524
+ return null;
525
+ }
526
+ }
527
+
528
+ export async function getUserDisplayName(userId: string, opts: LineClientOpts): Promise<string> {
529
+ const profile = await getUserProfile(userId, opts);
530
+ return profile?.displayName ?? userId;
531
+ }
@@ -0,0 +1,149 @@
1
+ import type { ChannelSetupAdapter, AutoBotConfig } from "autobot/plugin-sdk/setup";
2
+ import { createSetupInputPresenceValidator } from "autobot/plugin-sdk/setup";
3
+ import { hasLineCredentials, parseLineAllowFromId } from "./account-helpers.js";
4
+ import {
5
+ DEFAULT_ACCOUNT_ID,
6
+ listLineAccountIds,
7
+ normalizeAccountId,
8
+ resolveLineAccount,
9
+ type LineConfig,
10
+ } from "./setup-runtime-api.js";
11
+
12
+ export function patchLineAccountConfig(params: {
13
+ cfg: AutoBotConfig;
14
+ accountId: string;
15
+ patch: Record<string, unknown>;
16
+ clearFields?: string[];
17
+ enabled?: boolean;
18
+ }): AutoBotConfig {
19
+ const accountId = normalizeAccountId(params.accountId);
20
+ const lineConfig = (params.cfg.channels?.line ?? {}) as LineConfig;
21
+ const clearFields = params.clearFields ?? [];
22
+
23
+ if (accountId === DEFAULT_ACCOUNT_ID) {
24
+ const nextLine = { ...lineConfig } as Record<string, unknown>;
25
+ for (const field of clearFields) {
26
+ delete nextLine[field];
27
+ }
28
+ return {
29
+ ...params.cfg,
30
+ channels: {
31
+ ...params.cfg.channels,
32
+ line: {
33
+ ...nextLine,
34
+ ...(params.enabled ? { enabled: true } : {}),
35
+ ...params.patch,
36
+ },
37
+ },
38
+ };
39
+ }
40
+
41
+ const nextAccount = {
42
+ ...lineConfig.accounts?.[accountId],
43
+ } as Record<string, unknown>;
44
+ for (const field of clearFields) {
45
+ delete nextAccount[field];
46
+ }
47
+
48
+ return {
49
+ ...params.cfg,
50
+ channels: {
51
+ ...params.cfg.channels,
52
+ line: {
53
+ ...lineConfig,
54
+ ...(params.enabled ? { enabled: true } : {}),
55
+ accounts: {
56
+ ...lineConfig.accounts,
57
+ [accountId]: {
58
+ ...nextAccount,
59
+ ...(params.enabled ? { enabled: true } : {}),
60
+ ...params.patch,
61
+ },
62
+ },
63
+ },
64
+ },
65
+ };
66
+ }
67
+
68
+ export function isLineConfigured(cfg: AutoBotConfig, accountId: string): boolean {
69
+ return hasLineCredentials(resolveLineAccount({ cfg, accountId }));
70
+ }
71
+
72
+ export { parseLineAllowFromId };
73
+
74
+ export const lineSetupAdapter: ChannelSetupAdapter = {
75
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
76
+ applyAccountName: ({ cfg, accountId, name }) =>
77
+ patchLineAccountConfig({
78
+ cfg,
79
+ accountId,
80
+ patch: name?.trim() ? { name: name.trim() } : {},
81
+ }),
82
+ validateInput: createSetupInputPresenceValidator({
83
+ defaultAccountOnlyEnvError:
84
+ "LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account.",
85
+ whenNotUseEnv: [
86
+ {
87
+ someOf: ["channelAccessToken", "tokenFile"],
88
+ message: "LINE requires channelAccessToken or --token-file (or --use-env).",
89
+ },
90
+ {
91
+ someOf: ["channelSecret", "secretFile"],
92
+ message: "LINE requires channelSecret or --secret-file (or --use-env).",
93
+ },
94
+ ],
95
+ }),
96
+ applyAccountConfig: ({ cfg, accountId, input }) => {
97
+ const typedInput = input as {
98
+ useEnv?: boolean;
99
+ channelAccessToken?: string;
100
+ channelSecret?: string;
101
+ tokenFile?: string;
102
+ secretFile?: string;
103
+ };
104
+ const normalizedAccountId = normalizeAccountId(accountId);
105
+ if (normalizedAccountId === DEFAULT_ACCOUNT_ID) {
106
+ return patchLineAccountConfig({
107
+ cfg,
108
+ accountId: normalizedAccountId,
109
+ enabled: true,
110
+ clearFields: typedInput.useEnv
111
+ ? ["channelAccessToken", "channelSecret", "tokenFile", "secretFile"]
112
+ : undefined,
113
+ patch: typedInput.useEnv
114
+ ? {}
115
+ : {
116
+ ...(typedInput.tokenFile
117
+ ? { tokenFile: typedInput.tokenFile }
118
+ : typedInput.channelAccessToken
119
+ ? { channelAccessToken: typedInput.channelAccessToken }
120
+ : {}),
121
+ ...(typedInput.secretFile
122
+ ? { secretFile: typedInput.secretFile }
123
+ : typedInput.channelSecret
124
+ ? { channelSecret: typedInput.channelSecret }
125
+ : {}),
126
+ },
127
+ });
128
+ }
129
+ return patchLineAccountConfig({
130
+ cfg,
131
+ accountId: normalizedAccountId,
132
+ enabled: true,
133
+ patch: {
134
+ ...(typedInput.tokenFile
135
+ ? { tokenFile: typedInput.tokenFile }
136
+ : typedInput.channelAccessToken
137
+ ? { channelAccessToken: typedInput.channelAccessToken }
138
+ : {}),
139
+ ...(typedInput.secretFile
140
+ ? { secretFile: typedInput.secretFile }
141
+ : typedInput.channelSecret
142
+ ? { channelSecret: typedInput.channelSecret }
143
+ : {}),
144
+ },
145
+ });
146
+ },
147
+ };
148
+
149
+ export { listLineAccountIds };
@@ -0,0 +1,9 @@
1
+ export {
2
+ DEFAULT_ACCOUNT_ID,
3
+ formatDocsLink,
4
+ setSetupChannelEnabled,
5
+ splitSetupEntries,
6
+ } from "autobot/plugin-sdk/setup";
7
+ export type { ChannelSetupDmPolicy, ChannelSetupWizard } from "autobot/plugin-sdk/setup";
8
+ export { listLineAccountIds, normalizeAccountId, resolveLineAccount } from "./accounts.js";
9
+ export type { LineConfig } from "./types.js";