@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.
- package/api.ts +11 -0
- package/autobot.plugin.json +15 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +5 -0
- package/index.ts +54 -0
- package/package.json +60 -0
- package/runtime-api.ts +182 -0
- package/secret-contract-api.ts +4 -0
- package/setup-api.ts +2 -0
- package/setup-entry.ts +9 -0
- package/src/account-helpers.ts +16 -0
- package/src/accounts.ts +187 -0
- package/src/actions.ts +61 -0
- package/src/auto-reply-delivery.ts +200 -0
- package/src/bindings.ts +65 -0
- package/src/bot-access.ts +30 -0
- package/src/bot-handlers.ts +620 -0
- package/src/bot-message-context.ts +586 -0
- package/src/bot.ts +70 -0
- package/src/card-command.ts +347 -0
- package/src/channel-access-token.ts +14 -0
- package/src/channel-api.ts +17 -0
- package/src/channel-shared.ts +48 -0
- package/src/channel.runtime.ts +3 -0
- package/src/channel.setup.ts +11 -0
- package/src/channel.ts +155 -0
- package/src/config-adapter.ts +29 -0
- package/src/config-schema.ts +81 -0
- package/src/download.ts +34 -0
- package/src/flex-templates/basic-cards.ts +395 -0
- package/src/flex-templates/common.ts +20 -0
- package/src/flex-templates/media-control-cards.ts +555 -0
- package/src/flex-templates/message.ts +13 -0
- package/src/flex-templates/schedule-cards.ts +467 -0
- package/src/flex-templates/types.ts +22 -0
- package/src/flex-templates.ts +32 -0
- package/src/gateway.ts +129 -0
- package/src/group-keys.ts +65 -0
- package/src/group-policy.ts +22 -0
- package/src/markdown-to-line.ts +416 -0
- package/src/monitor-durable.ts +37 -0
- package/src/monitor.runtime.ts +1 -0
- package/src/monitor.ts +507 -0
- package/src/outbound-media.ts +120 -0
- package/src/outbound.runtime.ts +12 -0
- package/src/outbound.ts +427 -0
- package/src/probe.runtime.ts +1 -0
- package/src/probe.ts +34 -0
- package/src/quick-reply-fallback.ts +10 -0
- package/src/reply-chunks.ts +110 -0
- package/src/reply-payload-transform.ts +317 -0
- package/src/rich-menu.ts +326 -0
- package/src/runtime.ts +32 -0
- package/src/send-receipt.ts +32 -0
- package/src/send.ts +531 -0
- package/src/setup-core.ts +149 -0
- package/src/setup-runtime-api.ts +9 -0
- package/src/setup-surface.ts +229 -0
- package/src/signature.ts +24 -0
- package/src/status.ts +37 -0
- package/src/template-messages.ts +333 -0
- package/src/types.ts +130 -0
- package/src/webhook-node.ts +155 -0
- package/src/webhook-utils.ts +10 -0
- package/src/webhook.ts +135 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { attachFooterText } from "./common.js";
|
|
2
|
+
import type { Action, FlexBox, FlexBubble, FlexComponent, FlexText } from "./types.js";
|
|
3
|
+
|
|
4
|
+
function buildTitleSubtitleHeader(params: { title: string; subtitle?: string }): FlexComponent[] {
|
|
5
|
+
const { title, subtitle } = params;
|
|
6
|
+
const headerContents: FlexComponent[] = [
|
|
7
|
+
{
|
|
8
|
+
type: "text",
|
|
9
|
+
text: title,
|
|
10
|
+
weight: "bold",
|
|
11
|
+
size: "xl",
|
|
12
|
+
color: "#111111",
|
|
13
|
+
wrap: true,
|
|
14
|
+
} as FlexText,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
if (subtitle) {
|
|
18
|
+
headerContents.push({
|
|
19
|
+
type: "text",
|
|
20
|
+
text: subtitle,
|
|
21
|
+
size: "sm",
|
|
22
|
+
color: "#888888",
|
|
23
|
+
margin: "sm",
|
|
24
|
+
wrap: true,
|
|
25
|
+
} as FlexText);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return headerContents;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildCardHeaderSections(headerContents: FlexComponent[]): FlexComponent[] {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
type: "box",
|
|
35
|
+
layout: "vertical",
|
|
36
|
+
contents: headerContents,
|
|
37
|
+
paddingBottom: "lg",
|
|
38
|
+
} as FlexBox,
|
|
39
|
+
{
|
|
40
|
+
type: "separator",
|
|
41
|
+
color: "#EEEEEE",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createMegaBubbleWithFooter(params: {
|
|
47
|
+
bodyContents: FlexComponent[];
|
|
48
|
+
footer?: string;
|
|
49
|
+
}): FlexBubble {
|
|
50
|
+
const bubble: FlexBubble = {
|
|
51
|
+
type: "bubble",
|
|
52
|
+
size: "mega",
|
|
53
|
+
body: {
|
|
54
|
+
type: "box",
|
|
55
|
+
layout: "vertical",
|
|
56
|
+
contents: params.bodyContents,
|
|
57
|
+
paddingAll: "xl",
|
|
58
|
+
backgroundColor: "#FFFFFF",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (params.footer) {
|
|
63
|
+
attachFooterText(bubble, params.footer);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return bubble;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a receipt/summary card (for orders, transactions, data tables)
|
|
71
|
+
*
|
|
72
|
+
* Editorial design: Clean table layout with alternating row backgrounds,
|
|
73
|
+
* prominent total section, and clear visual hierarchy.
|
|
74
|
+
*/
|
|
75
|
+
export function createReceiptCard(params: {
|
|
76
|
+
title: string;
|
|
77
|
+
subtitle?: string;
|
|
78
|
+
items: Array<{ name: string; value: string; highlight?: boolean }>;
|
|
79
|
+
total?: { label: string; value: string };
|
|
80
|
+
footer?: string;
|
|
81
|
+
}): FlexBubble {
|
|
82
|
+
const { title, subtitle, items, total, footer } = params;
|
|
83
|
+
|
|
84
|
+
const itemRows: FlexComponent[] = items.slice(0, 12).map(
|
|
85
|
+
(item, index) =>
|
|
86
|
+
({
|
|
87
|
+
type: "box",
|
|
88
|
+
layout: "horizontal",
|
|
89
|
+
contents: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: item.name,
|
|
93
|
+
size: "sm",
|
|
94
|
+
color: item.highlight ? "#111111" : "#666666",
|
|
95
|
+
weight: item.highlight ? "bold" : "regular",
|
|
96
|
+
flex: 3,
|
|
97
|
+
wrap: true,
|
|
98
|
+
} as FlexText,
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: item.value,
|
|
102
|
+
size: "sm",
|
|
103
|
+
color: item.highlight ? "#06C755" : "#333333",
|
|
104
|
+
weight: item.highlight ? "bold" : "regular",
|
|
105
|
+
flex: 2,
|
|
106
|
+
align: "end",
|
|
107
|
+
wrap: true,
|
|
108
|
+
} as FlexText,
|
|
109
|
+
],
|
|
110
|
+
paddingAll: "md",
|
|
111
|
+
backgroundColor: index % 2 === 0 ? "#FFFFFF" : "#FAFAFA",
|
|
112
|
+
}) as FlexBox,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Header section
|
|
116
|
+
const headerContents = buildTitleSubtitleHeader({ title, subtitle });
|
|
117
|
+
|
|
118
|
+
const bodyContents: FlexComponent[] = [
|
|
119
|
+
...buildCardHeaderSections(headerContents),
|
|
120
|
+
{
|
|
121
|
+
type: "box",
|
|
122
|
+
layout: "vertical",
|
|
123
|
+
contents: itemRows,
|
|
124
|
+
margin: "md",
|
|
125
|
+
cornerRadius: "md",
|
|
126
|
+
borderWidth: "light",
|
|
127
|
+
borderColor: "#EEEEEE",
|
|
128
|
+
} as FlexBox,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// Total section with emphasis
|
|
132
|
+
if (total) {
|
|
133
|
+
bodyContents.push({
|
|
134
|
+
type: "box",
|
|
135
|
+
layout: "horizontal",
|
|
136
|
+
contents: [
|
|
137
|
+
{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: total.label,
|
|
140
|
+
size: "lg",
|
|
141
|
+
weight: "bold",
|
|
142
|
+
color: "#111111",
|
|
143
|
+
flex: 2,
|
|
144
|
+
} as FlexText,
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: total.value,
|
|
148
|
+
size: "xl",
|
|
149
|
+
weight: "bold",
|
|
150
|
+
color: "#06C755",
|
|
151
|
+
flex: 2,
|
|
152
|
+
align: "end",
|
|
153
|
+
} as FlexText,
|
|
154
|
+
],
|
|
155
|
+
margin: "xl",
|
|
156
|
+
paddingAll: "lg",
|
|
157
|
+
backgroundColor: "#F0FDF4",
|
|
158
|
+
cornerRadius: "lg",
|
|
159
|
+
} as FlexBox);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return createMegaBubbleWithFooter({ bodyContents, footer });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a calendar event card (for meetings, appointments, reminders)
|
|
167
|
+
*
|
|
168
|
+
* Editorial design: Date as hero, strong typographic hierarchy,
|
|
169
|
+
* color-blocked zones, full text wrapping for readability.
|
|
170
|
+
*/
|
|
171
|
+
export function createEventCard(params: {
|
|
172
|
+
title: string;
|
|
173
|
+
date: string;
|
|
174
|
+
time?: string;
|
|
175
|
+
location?: string;
|
|
176
|
+
description?: string;
|
|
177
|
+
calendar?: string;
|
|
178
|
+
isAllDay?: boolean;
|
|
179
|
+
action?: Action;
|
|
180
|
+
}): FlexBubble {
|
|
181
|
+
const { title, date, time, location, description, calendar, isAllDay, action } = params;
|
|
182
|
+
|
|
183
|
+
// Hero date block - the most important information
|
|
184
|
+
const dateBlock: FlexBox = {
|
|
185
|
+
type: "box",
|
|
186
|
+
layout: "vertical",
|
|
187
|
+
contents: [
|
|
188
|
+
{
|
|
189
|
+
type: "text",
|
|
190
|
+
text: date.toUpperCase(),
|
|
191
|
+
size: "sm",
|
|
192
|
+
weight: "bold",
|
|
193
|
+
color: "#06C755",
|
|
194
|
+
wrap: true,
|
|
195
|
+
} as FlexText,
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: isAllDay ? "ALL DAY" : (time ?? ""),
|
|
199
|
+
size: "xxl",
|
|
200
|
+
weight: "bold",
|
|
201
|
+
color: "#111111",
|
|
202
|
+
wrap: true,
|
|
203
|
+
margin: "xs",
|
|
204
|
+
} as FlexText,
|
|
205
|
+
],
|
|
206
|
+
paddingBottom: "lg",
|
|
207
|
+
borderWidth: "none",
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// If no time and not all day, hide the time display
|
|
211
|
+
if (!time && !isAllDay) {
|
|
212
|
+
dateBlock.contents = [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: date,
|
|
216
|
+
size: "xl",
|
|
217
|
+
weight: "bold",
|
|
218
|
+
color: "#111111",
|
|
219
|
+
wrap: true,
|
|
220
|
+
} as FlexText,
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Event title with accent bar
|
|
225
|
+
const titleBlock: FlexBox = {
|
|
226
|
+
type: "box",
|
|
227
|
+
layout: "horizontal",
|
|
228
|
+
contents: [
|
|
229
|
+
{
|
|
230
|
+
type: "box",
|
|
231
|
+
layout: "vertical",
|
|
232
|
+
contents: [],
|
|
233
|
+
width: "4px",
|
|
234
|
+
backgroundColor: "#06C755",
|
|
235
|
+
cornerRadius: "2px",
|
|
236
|
+
} as FlexBox,
|
|
237
|
+
{
|
|
238
|
+
type: "box",
|
|
239
|
+
layout: "vertical",
|
|
240
|
+
contents: [
|
|
241
|
+
{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: title,
|
|
244
|
+
size: "lg",
|
|
245
|
+
weight: "bold",
|
|
246
|
+
color: "#1a1a1a",
|
|
247
|
+
wrap: true,
|
|
248
|
+
} as FlexText,
|
|
249
|
+
...(calendar
|
|
250
|
+
? [
|
|
251
|
+
{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: calendar,
|
|
254
|
+
size: "xs",
|
|
255
|
+
color: "#888888",
|
|
256
|
+
margin: "sm",
|
|
257
|
+
wrap: true,
|
|
258
|
+
} as FlexText,
|
|
259
|
+
]
|
|
260
|
+
: []),
|
|
261
|
+
],
|
|
262
|
+
flex: 1,
|
|
263
|
+
paddingStart: "lg",
|
|
264
|
+
} as FlexBox,
|
|
265
|
+
],
|
|
266
|
+
paddingTop: "lg",
|
|
267
|
+
paddingBottom: "lg",
|
|
268
|
+
borderWidth: "light",
|
|
269
|
+
borderColor: "#EEEEEE",
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const bodyContents: FlexComponent[] = [dateBlock, titleBlock];
|
|
273
|
+
|
|
274
|
+
// Details section (location + description) in subtle background
|
|
275
|
+
const hasDetails = location || description;
|
|
276
|
+
if (hasDetails) {
|
|
277
|
+
const detailItems: FlexComponent[] = [];
|
|
278
|
+
|
|
279
|
+
if (location) {
|
|
280
|
+
detailItems.push({
|
|
281
|
+
type: "box",
|
|
282
|
+
layout: "horizontal",
|
|
283
|
+
contents: [
|
|
284
|
+
{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: "📍",
|
|
287
|
+
size: "sm",
|
|
288
|
+
flex: 0,
|
|
289
|
+
} as FlexText,
|
|
290
|
+
{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: location,
|
|
293
|
+
size: "sm",
|
|
294
|
+
color: "#444444",
|
|
295
|
+
margin: "md",
|
|
296
|
+
flex: 1,
|
|
297
|
+
wrap: true,
|
|
298
|
+
} as FlexText,
|
|
299
|
+
],
|
|
300
|
+
alignItems: "flex-start",
|
|
301
|
+
} as FlexBox);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (description) {
|
|
305
|
+
detailItems.push({
|
|
306
|
+
type: "text",
|
|
307
|
+
text: description,
|
|
308
|
+
size: "sm",
|
|
309
|
+
color: "#666666",
|
|
310
|
+
wrap: true,
|
|
311
|
+
margin: location ? "lg" : "none",
|
|
312
|
+
} as FlexText);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
bodyContents.push({
|
|
316
|
+
type: "box",
|
|
317
|
+
layout: "vertical",
|
|
318
|
+
contents: detailItems,
|
|
319
|
+
margin: "lg",
|
|
320
|
+
paddingAll: "lg",
|
|
321
|
+
backgroundColor: "#F8F9FA",
|
|
322
|
+
cornerRadius: "lg",
|
|
323
|
+
} as FlexBox);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
type: "bubble",
|
|
328
|
+
size: "mega",
|
|
329
|
+
body: {
|
|
330
|
+
type: "box",
|
|
331
|
+
layout: "vertical",
|
|
332
|
+
contents: bodyContents,
|
|
333
|
+
paddingAll: "xl",
|
|
334
|
+
backgroundColor: "#FFFFFF",
|
|
335
|
+
action,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create a calendar agenda card showing multiple events
|
|
342
|
+
*
|
|
343
|
+
* Editorial timeline design: Time-focused left column with event details
|
|
344
|
+
* on the right. Visual accent bars indicate event priority/recency.
|
|
345
|
+
*/
|
|
346
|
+
export function createAgendaCard(params: {
|
|
347
|
+
title: string;
|
|
348
|
+
subtitle?: string;
|
|
349
|
+
events: Array<{
|
|
350
|
+
title: string;
|
|
351
|
+
time?: string;
|
|
352
|
+
location?: string;
|
|
353
|
+
calendar?: string;
|
|
354
|
+
isNow?: boolean;
|
|
355
|
+
}>;
|
|
356
|
+
footer?: string;
|
|
357
|
+
}): FlexBubble {
|
|
358
|
+
const { title, subtitle, events, footer } = params;
|
|
359
|
+
|
|
360
|
+
// Header with title and optional subtitle
|
|
361
|
+
const headerContents = buildTitleSubtitleHeader({ title, subtitle });
|
|
362
|
+
|
|
363
|
+
// Event timeline items
|
|
364
|
+
const eventItems: FlexComponent[] = events.slice(0, 6).map((event, index) => {
|
|
365
|
+
const isActive = event.isNow || index === 0;
|
|
366
|
+
const accentColor = isActive ? "#06C755" : "#E5E5E5";
|
|
367
|
+
|
|
368
|
+
// Time column (fixed width)
|
|
369
|
+
const timeColumn: FlexBox = {
|
|
370
|
+
type: "box",
|
|
371
|
+
layout: "vertical",
|
|
372
|
+
contents: [
|
|
373
|
+
{
|
|
374
|
+
type: "text",
|
|
375
|
+
text: event.time ?? "—",
|
|
376
|
+
size: "sm",
|
|
377
|
+
weight: isActive ? "bold" : "regular",
|
|
378
|
+
color: isActive ? "#06C755" : "#666666",
|
|
379
|
+
align: "end",
|
|
380
|
+
wrap: true,
|
|
381
|
+
} as FlexText,
|
|
382
|
+
],
|
|
383
|
+
width: "65px",
|
|
384
|
+
justifyContent: "flex-start",
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Accent dot
|
|
388
|
+
const dotColumn: FlexBox = {
|
|
389
|
+
type: "box",
|
|
390
|
+
layout: "vertical",
|
|
391
|
+
contents: [
|
|
392
|
+
{
|
|
393
|
+
type: "box",
|
|
394
|
+
layout: "vertical",
|
|
395
|
+
contents: [],
|
|
396
|
+
width: "10px",
|
|
397
|
+
height: "10px",
|
|
398
|
+
backgroundColor: accentColor,
|
|
399
|
+
cornerRadius: "5px",
|
|
400
|
+
} as FlexBox,
|
|
401
|
+
],
|
|
402
|
+
width: "24px",
|
|
403
|
+
alignItems: "center",
|
|
404
|
+
justifyContent: "flex-start",
|
|
405
|
+
paddingTop: "xs",
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Event details column
|
|
409
|
+
const detailContents: FlexComponent[] = [
|
|
410
|
+
{
|
|
411
|
+
type: "text",
|
|
412
|
+
text: event.title,
|
|
413
|
+
size: "md",
|
|
414
|
+
weight: "bold",
|
|
415
|
+
color: "#1a1a1a",
|
|
416
|
+
wrap: true,
|
|
417
|
+
} as FlexText,
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
// Secondary info line
|
|
421
|
+
const secondaryParts: string[] = [];
|
|
422
|
+
if (event.location) {
|
|
423
|
+
secondaryParts.push(event.location);
|
|
424
|
+
}
|
|
425
|
+
if (event.calendar) {
|
|
426
|
+
secondaryParts.push(event.calendar);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (secondaryParts.length > 0) {
|
|
430
|
+
detailContents.push({
|
|
431
|
+
type: "text",
|
|
432
|
+
text: secondaryParts.join(" · "),
|
|
433
|
+
size: "xs",
|
|
434
|
+
color: "#888888",
|
|
435
|
+
wrap: true,
|
|
436
|
+
margin: "xs",
|
|
437
|
+
} as FlexText);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const detailColumn: FlexBox = {
|
|
441
|
+
type: "box",
|
|
442
|
+
layout: "vertical",
|
|
443
|
+
contents: detailContents,
|
|
444
|
+
flex: 1,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
type: "box",
|
|
449
|
+
layout: "horizontal",
|
|
450
|
+
contents: [timeColumn, dotColumn, detailColumn],
|
|
451
|
+
margin: index > 0 ? "xl" : undefined,
|
|
452
|
+
alignItems: "flex-start",
|
|
453
|
+
} as FlexBox;
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const bodyContents: FlexComponent[] = [
|
|
457
|
+
...buildCardHeaderSections(headerContents),
|
|
458
|
+
{
|
|
459
|
+
type: "box",
|
|
460
|
+
layout: "vertical",
|
|
461
|
+
contents: eventItems,
|
|
462
|
+
paddingTop: "xl",
|
|
463
|
+
} as FlexBox,
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
return createMegaBubbleWithFooter({ bodyContents, footer });
|
|
467
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { messagingApi } from "@line/bot-sdk";
|
|
2
|
+
|
|
3
|
+
export type FlexContainer = messagingApi.FlexContainer;
|
|
4
|
+
export type FlexBubble = messagingApi.FlexBubble;
|
|
5
|
+
export type FlexCarousel = messagingApi.FlexCarousel;
|
|
6
|
+
export type FlexBox = messagingApi.FlexBox;
|
|
7
|
+
export type FlexText = messagingApi.FlexText;
|
|
8
|
+
export type FlexImage = messagingApi.FlexImage;
|
|
9
|
+
export type FlexButton = messagingApi.FlexButton;
|
|
10
|
+
export type FlexComponent = messagingApi.FlexComponent;
|
|
11
|
+
export type Action = messagingApi.Action;
|
|
12
|
+
|
|
13
|
+
export interface ListItem {
|
|
14
|
+
title: string;
|
|
15
|
+
subtitle?: string;
|
|
16
|
+
action?: Action;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CardAction {
|
|
20
|
+
label: string;
|
|
21
|
+
action: Action;
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createActionCard,
|
|
3
|
+
createCarousel,
|
|
4
|
+
createImageCard,
|
|
5
|
+
createInfoCard,
|
|
6
|
+
createListCard,
|
|
7
|
+
createNotificationBubble,
|
|
8
|
+
} from "./flex-templates/basic-cards.js";
|
|
9
|
+
export {
|
|
10
|
+
createAgendaCard,
|
|
11
|
+
createEventCard,
|
|
12
|
+
createReceiptCard,
|
|
13
|
+
} from "./flex-templates/schedule-cards.js";
|
|
14
|
+
export {
|
|
15
|
+
createAppleTvRemoteCard,
|
|
16
|
+
createDeviceControlCard,
|
|
17
|
+
createMediaPlayerCard,
|
|
18
|
+
} from "./flex-templates/media-control-cards.js";
|
|
19
|
+
export { toFlexMessage } from "./flex-templates/message.js";
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
CardAction,
|
|
23
|
+
FlexBox,
|
|
24
|
+
FlexBubble,
|
|
25
|
+
FlexButton,
|
|
26
|
+
FlexCarousel,
|
|
27
|
+
FlexComponent,
|
|
28
|
+
FlexContainer,
|
|
29
|
+
FlexImage,
|
|
30
|
+
FlexText,
|
|
31
|
+
ListItem,
|
|
32
|
+
} from "./flex-templates/types.js";
|
package/src/gateway.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createLazyRuntimeModule } from "autobot/plugin-sdk/lazy-runtime";
|
|
2
|
+
import { resolveLineAccount } from "./accounts.js";
|
|
3
|
+
import {
|
|
4
|
+
clearAccountEntryFields,
|
|
5
|
+
DEFAULT_ACCOUNT_ID,
|
|
6
|
+
type ChannelPlugin,
|
|
7
|
+
type LineConfig,
|
|
8
|
+
type AutoBotConfig,
|
|
9
|
+
type ResolvedLineAccount,
|
|
10
|
+
} from "./channel-api.js";
|
|
11
|
+
import { getLineRuntime } from "./runtime.js";
|
|
12
|
+
|
|
13
|
+
const loadLineProbeRuntime = createLazyRuntimeModule(() => import("./probe.runtime.js"));
|
|
14
|
+
const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime.js"));
|
|
15
|
+
|
|
16
|
+
export const lineGatewayAdapter: NonNullable<ChannelPlugin<ResolvedLineAccount>["gateway"]> = {
|
|
17
|
+
startAccount: async (ctx) => {
|
|
18
|
+
const account = ctx.account;
|
|
19
|
+
const token = account.channelAccessToken.trim();
|
|
20
|
+
const secret = account.channelSecret.trim();
|
|
21
|
+
if (!token) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`LINE webhook mode requires a non-empty channel access token for account "${account.accountId}".`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (!secret) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`LINE webhook mode requires a non-empty channel secret for account "${account.accountId}".`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let lineBotLabel = "";
|
|
33
|
+
try {
|
|
34
|
+
const probe = await (await loadLineProbeRuntime()).probeLineBot(token, 2500);
|
|
35
|
+
const displayName = probe.ok ? probe.bot?.displayName?.trim() : null;
|
|
36
|
+
if (displayName) {
|
|
37
|
+
lineBotLabel = ` (${displayName})`;
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (getLineRuntime().logging.shouldLogVerbose()) {
|
|
41
|
+
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ctx.log?.info(`[${account.accountId}] starting LINE provider${lineBotLabel}`);
|
|
46
|
+
|
|
47
|
+
const monitorLineProvider =
|
|
48
|
+
getLineRuntime().channel.line?.monitorLineProvider ??
|
|
49
|
+
(await loadLineMonitorRuntime()).monitorLineProvider;
|
|
50
|
+
|
|
51
|
+
return await monitorLineProvider({
|
|
52
|
+
channelAccessToken: token,
|
|
53
|
+
channelSecret: secret,
|
|
54
|
+
accountId: account.accountId,
|
|
55
|
+
config: ctx.cfg,
|
|
56
|
+
runtime: ctx.runtime,
|
|
57
|
+
abortSignal: ctx.abortSignal,
|
|
58
|
+
webhookPath: account.config.webhookPath,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
logoutAccount: async ({ accountId, cfg }) => {
|
|
62
|
+
const envToken = process.env.LINE_CHANNEL_ACCESS_TOKEN?.trim() ?? "";
|
|
63
|
+
const nextCfg = { ...cfg } as AutoBotConfig;
|
|
64
|
+
const lineConfig = (cfg.channels?.line ?? {}) as LineConfig;
|
|
65
|
+
const nextLine = { ...lineConfig };
|
|
66
|
+
let cleared = false;
|
|
67
|
+
let changed = false;
|
|
68
|
+
|
|
69
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
70
|
+
if (
|
|
71
|
+
nextLine.channelAccessToken ||
|
|
72
|
+
nextLine.channelSecret ||
|
|
73
|
+
nextLine.tokenFile ||
|
|
74
|
+
nextLine.secretFile
|
|
75
|
+
) {
|
|
76
|
+
delete nextLine.channelAccessToken;
|
|
77
|
+
delete nextLine.channelSecret;
|
|
78
|
+
delete nextLine.tokenFile;
|
|
79
|
+
delete nextLine.secretFile;
|
|
80
|
+
cleared = true;
|
|
81
|
+
changed = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const accountCleanup = clearAccountEntryFields({
|
|
86
|
+
accounts: nextLine.accounts,
|
|
87
|
+
accountId,
|
|
88
|
+
fields: ["channelAccessToken", "channelSecret", "tokenFile", "secretFile"],
|
|
89
|
+
markClearedOnFieldPresence: true,
|
|
90
|
+
});
|
|
91
|
+
if (accountCleanup.changed) {
|
|
92
|
+
changed = true;
|
|
93
|
+
if (accountCleanup.cleared) {
|
|
94
|
+
cleared = true;
|
|
95
|
+
}
|
|
96
|
+
if (accountCleanup.nextAccounts) {
|
|
97
|
+
nextLine.accounts = accountCleanup.nextAccounts;
|
|
98
|
+
} else {
|
|
99
|
+
delete nextLine.accounts;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (changed) {
|
|
104
|
+
if (Object.keys(nextLine).length > 0) {
|
|
105
|
+
nextCfg.channels = { ...nextCfg.channels, line: nextLine };
|
|
106
|
+
} else {
|
|
107
|
+
const nextChannels = { ...nextCfg.channels };
|
|
108
|
+
delete (nextChannels as Record<string, unknown>).line;
|
|
109
|
+
if (Object.keys(nextChannels).length > 0) {
|
|
110
|
+
nextCfg.channels = nextChannels;
|
|
111
|
+
} else {
|
|
112
|
+
delete nextCfg.channels;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
await getLineRuntime().config.replaceConfigFile({
|
|
116
|
+
nextConfig: nextCfg,
|
|
117
|
+
afterWrite: { mode: "auto" },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const resolved = resolveLineAccount({
|
|
122
|
+
cfg: changed ? nextCfg : cfg,
|
|
123
|
+
accountId,
|
|
124
|
+
});
|
|
125
|
+
const loggedOut = resolved.tokenSource === "none";
|
|
126
|
+
|
|
127
|
+
return { cleared, envToken: Boolean(envToken), loggedOut };
|
|
128
|
+
},
|
|
129
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { normalizeAccountId } from "autobot/plugin-sdk/account-id";
|
|
2
|
+
import type { AutoBotConfig } from "autobot/plugin-sdk/account-resolution";
|
|
3
|
+
import { resolveAccountEntry } from "autobot/plugin-sdk/account-resolution";
|
|
4
|
+
import type { LineConfig, LineGroupConfig } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export function resolveLineGroupLookupIds(groupId?: string | null): string[] {
|
|
7
|
+
const normalized = groupId?.trim();
|
|
8
|
+
if (!normalized) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
if (normalized.startsWith("group:") || normalized.startsWith("room:")) {
|
|
12
|
+
const rawId = normalized.split(":").slice(1).join(":");
|
|
13
|
+
return rawId ? [rawId, normalized] : [normalized];
|
|
14
|
+
}
|
|
15
|
+
return [normalized, `group:${normalized}`, `room:${normalized}`];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveLineGroupConfigEntry<T>(
|
|
19
|
+
groups: Record<string, T | undefined> | undefined,
|
|
20
|
+
params: { groupId?: string | null; roomId?: string | null },
|
|
21
|
+
): T | undefined {
|
|
22
|
+
if (!groups) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
for (const candidate of resolveLineGroupLookupIds(params.groupId)) {
|
|
26
|
+
const hit = groups[candidate];
|
|
27
|
+
if (hit) {
|
|
28
|
+
return hit;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const candidate of resolveLineGroupLookupIds(params.roomId)) {
|
|
32
|
+
const hit = groups[candidate];
|
|
33
|
+
if (hit) {
|
|
34
|
+
return hit;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return groups["*"];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveLineGroupsConfig(
|
|
41
|
+
cfg: AutoBotConfig,
|
|
42
|
+
accountId?: string | null,
|
|
43
|
+
): Record<string, LineGroupConfig | undefined> | undefined {
|
|
44
|
+
const lineConfig = cfg.channels?.line as LineConfig | undefined;
|
|
45
|
+
if (!lineConfig) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
49
|
+
const accountGroups = resolveAccountEntry(lineConfig.accounts, normalizedAccountId)?.groups;
|
|
50
|
+
return accountGroups ?? lineConfig.groups;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveExactLineGroupConfigKey(params: {
|
|
54
|
+
cfg: AutoBotConfig;
|
|
55
|
+
accountId?: string | null;
|
|
56
|
+
groupId?: string | null;
|
|
57
|
+
}): string | undefined {
|
|
58
|
+
const groups = resolveLineGroupsConfig(params.cfg, params.accountId);
|
|
59
|
+
if (!groups) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return resolveLineGroupLookupIds(params.groupId).find((candidate) =>
|
|
63
|
+
Object.hasOwn(groups, candidate),
|
|
64
|
+
);
|
|
65
|
+
}
|