@elizaos/plugin-line 2.0.3-beta.6 → 2.0.3-beta.7

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.
@@ -0,0 +1,861 @@
1
+ /**
2
+ * LINE service implementation for ElizaOS.
3
+ */
4
+ import { logger, Service } from "@elizaos/core";
5
+ import { messagingApi, middleware } from "@line/bot-sdk";
6
+ import { getChatTypeFromId, LINE_SERVICE_NAME, LineApiError, LineConfigurationError, LineEventTypes, MAX_LINE_BATCH_SIZE, normalizeLineTarget, splitMessageForLine, } from "./types.js";
7
+ function objectToMetadataValue(obj) {
8
+ return JSON.parse(JSON.stringify(obj));
9
+ }
10
+ function normalizeLineQuery(query) {
11
+ return query.trim().toLowerCase();
12
+ }
13
+ function scoreLineCandidate(values, query) {
14
+ const normalized = normalizeLineQuery(query);
15
+ if (!normalized) {
16
+ return 0.45;
17
+ }
18
+ const candidates = values
19
+ .filter((value) => typeof value === "string" && value.trim().length > 0)
20
+ .map((value) => value.trim().toLowerCase());
21
+ if (candidates.some((candidate) => candidate === normalized)) {
22
+ return 1;
23
+ }
24
+ return candidates.some((candidate) => candidate.includes(normalized)) ? 0.8 : 0;
25
+ }
26
+ function lineRoomToConnectorTarget(room, score = 0.55) {
27
+ const channelId = String(room.channelId ?? "");
28
+ const chatType = channelId ? getChatTypeFromId(channelId) : undefined;
29
+ return {
30
+ target: {
31
+ source: LINE_SERVICE_NAME,
32
+ roomId: room.id,
33
+ channelId,
34
+ },
35
+ label: room.name || channelId || String(room.id),
36
+ kind: chatType === "user" ? "contact" : chatType || "room",
37
+ description: "LINE chat from stored room context",
38
+ score,
39
+ contexts: ["social", "connectors"],
40
+ metadata: {
41
+ chatType,
42
+ channelId,
43
+ roomType: room.type,
44
+ },
45
+ };
46
+ }
47
+ function normalizeConnectorLimit(limit, fallback = 50) {
48
+ if (!Number.isFinite(limit) || !limit || limit <= 0) {
49
+ return fallback;
50
+ }
51
+ return Math.min(Math.floor(limit), 200);
52
+ }
53
+ async function readStoredMessageMemories(runtime, roomId, limit) {
54
+ return runtime.getMemories({
55
+ tableName: "messages",
56
+ roomId,
57
+ limit,
58
+ orderBy: "createdAt",
59
+ orderDirection: "desc",
60
+ });
61
+ }
62
+ async function readStoredMessagesForTargets(runtime, targets, limit) {
63
+ const roomIds = Array.from(new Set(targets.map((target) => target.target.roomId).filter((id) => Boolean(id))));
64
+ const chunks = await Promise.all(roomIds.map((roomId) => readStoredMessageMemories(runtime, roomId, limit)));
65
+ return chunks
66
+ .flat()
67
+ .sort((left, right) => (right.createdAt ?? 0) - (left.createdAt ?? 0))
68
+ .slice(0, limit);
69
+ }
70
+ function filterMemoriesByQuery(memories, query, limit) {
71
+ const normalized = query.trim().toLowerCase();
72
+ if (!normalized) {
73
+ return memories.slice(0, limit);
74
+ }
75
+ return memories
76
+ .filter((memory) => {
77
+ const text = typeof memory.content.text === "string" ? memory.content.text : "";
78
+ return text.toLowerCase().includes(normalized);
79
+ })
80
+ .slice(0, limit);
81
+ }
82
+ function quickReplyItemsFromStrings(values) {
83
+ if (!Array.isArray(values)) {
84
+ return undefined;
85
+ }
86
+ const items = values
87
+ .filter((value) => typeof value === "string" && value.trim().length > 0)
88
+ .slice(0, 13)
89
+ .map((text) => ({
90
+ type: "action",
91
+ action: {
92
+ type: "message",
93
+ label: text.slice(0, 20),
94
+ text,
95
+ },
96
+ }));
97
+ return items.length > 0 ? items : undefined;
98
+ }
99
+ function lineDataFromContent(content) {
100
+ const data = content.data;
101
+ if (data?.line && typeof data.line === "object") {
102
+ return data.line;
103
+ }
104
+ return data ?? {};
105
+ }
106
+ function isRecord(value) {
107
+ return typeof value === "object" && value !== null;
108
+ }
109
+ function isValidHttpsUrl(value) {
110
+ try {
111
+ const url = new URL(value);
112
+ return url.protocol === "https:";
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ function validateTemplateUrls(template) {
119
+ const content = template.template;
120
+ if ("thumbnailImageUrl" in content && content.thumbnailImageUrl) {
121
+ if (!isValidHttpsUrl(content.thumbnailImageUrl)) {
122
+ return "LINE template thumbnailImageUrl must be an HTTPS URL";
123
+ }
124
+ }
125
+ for (const action of content.actions) {
126
+ if (action.type === "uri" && (!action.uri || !isValidHttpsUrl(action.uri))) {
127
+ return "LINE template URI actions must use HTTPS URLs";
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+ function validateLocation(location) {
133
+ if (!Number.isFinite(location.latitude) || location.latitude < -90 || location.latitude > 90) {
134
+ return "LINE location latitude must be between -90 and 90";
135
+ }
136
+ if (!Number.isFinite(location.longitude) ||
137
+ location.longitude < -180 ||
138
+ location.longitude > 180) {
139
+ return "LINE location longitude must be between -180 and 180";
140
+ }
141
+ return null;
142
+ }
143
+ /**
144
+ * LINE messaging service for ElizaOS agents.
145
+ */
146
+ export class LineService extends Service {
147
+ static serviceType = LINE_SERVICE_NAME;
148
+ capabilityDescription = "The agent is able to send and receive messages via LINE";
149
+ settings = null;
150
+ client = null;
151
+ connected = false;
152
+ constructor(runtime) {
153
+ super(runtime);
154
+ if (!runtime)
155
+ return;
156
+ this.settings = this.loadSettings();
157
+ }
158
+ /**
159
+ * Start the LINE service.
160
+ */
161
+ static async start(runtime) {
162
+ const service = new LineService(runtime);
163
+ await service.initialize();
164
+ return service;
165
+ }
166
+ static registerSendHandlers(runtime, service) {
167
+ const sendHandler = async (handlerRuntime, target, content) => {
168
+ await service.handleSendMessage(handlerRuntime, target, content);
169
+ return undefined;
170
+ };
171
+ if (typeof runtime.registerMessageConnector === "function") {
172
+ const registration = {
173
+ source: LINE_SERVICE_NAME,
174
+ label: "LINE",
175
+ capabilities: [
176
+ "send_message",
177
+ "send_flex_message",
178
+ "send_location",
179
+ "send_template_message",
180
+ "quick_reply",
181
+ ],
182
+ supportedTargetKinds: ["contact", "group", "room", "channel"],
183
+ contexts: ["social", "connectors"],
184
+ description: "Send LINE text, flex/card, template, quick reply, and location messages to known LINE chats.",
185
+ sendHandler,
186
+ resolveTargets: async (query, context) => {
187
+ const normalizedTarget = normalizeLineTarget(query);
188
+ const exactTarget = normalizedTarget
189
+ ? [
190
+ {
191
+ target: {
192
+ source: LINE_SERVICE_NAME,
193
+ channelId: normalizedTarget,
194
+ },
195
+ label: normalizedTarget,
196
+ kind: getChatTypeFromId(normalizedTarget) === "user"
197
+ ? "contact"
198
+ : getChatTypeFromId(normalizedTarget),
199
+ score: 1,
200
+ contexts: ["social", "connectors"],
201
+ metadata: {
202
+ chatType: getChatTypeFromId(normalizedTarget),
203
+ },
204
+ },
205
+ ]
206
+ : [];
207
+ const roomTargets = (await service.listConnectorRooms(context.runtime))
208
+ .map((room) => ({
209
+ room,
210
+ score: scoreLineCandidate([room.name, room.channelId, String(room.id)], query),
211
+ }))
212
+ .filter(({ score }) => score > 0)
213
+ .map(({ room, score }) => lineRoomToConnectorTarget(room, score));
214
+ return [...exactTarget, ...roomTargets]
215
+ .sort((left, right) => (right.score ?? 0) - (left.score ?? 0))
216
+ .slice(0, 10);
217
+ },
218
+ listRecentTargets: async (context) => (await service.listConnectorRooms(context.runtime))
219
+ .slice(0, 10)
220
+ .map((room) => lineRoomToConnectorTarget(room)),
221
+ listRooms: async (context) => (await service.listConnectorRooms(context.runtime)).map((room) => lineRoomToConnectorTarget(room)),
222
+ fetchMessages: async (context, params) => {
223
+ const limit = normalizeConnectorLimit(params?.limit);
224
+ const target = params?.target ?? context.target;
225
+ if (target?.roomId) {
226
+ return readStoredMessageMemories(context.runtime, target.roomId, limit);
227
+ }
228
+ const targets = (await service.listConnectorRooms(context.runtime))
229
+ .slice(0, 10)
230
+ .map((room) => lineRoomToConnectorTarget(room));
231
+ return readStoredMessagesForTargets(context.runtime, targets, limit);
232
+ },
233
+ searchMessages: async (context, params) => {
234
+ const limit = normalizeConnectorLimit(params?.limit);
235
+ const target = params?.target ?? context.target;
236
+ const messages = target?.roomId
237
+ ? await readStoredMessageMemories(context.runtime, target.roomId, Math.max(limit, 100))
238
+ : await readStoredMessagesForTargets(context.runtime, (await service.listConnectorRooms(context.runtime))
239
+ .slice(0, 10)
240
+ .map((room) => lineRoomToConnectorTarget(room)), Math.max(limit, 100));
241
+ return filterMemoriesByQuery(messages, params.query, limit);
242
+ },
243
+ leaveHandler: async (handlerRuntime, params) => {
244
+ const target = params.target ?? { source: LINE_SERVICE_NAME };
245
+ const room = target.roomId ? await handlerRuntime.getRoom(target.roomId) : null;
246
+ const channelId = String(params.channelId ?? target.channelId ?? room?.channelId ?? "");
247
+ const chatType = getChatTypeFromId(channelId);
248
+ if (chatType !== "group" && chatType !== "room") {
249
+ throw new Error("LINE leaveHandler requires a group or room target");
250
+ }
251
+ await service.leaveChat(channelId, chatType);
252
+ },
253
+ getUser: async (_handlerRuntime, params) => {
254
+ const userId = String(params.userId ??
255
+ params.username ??
256
+ params.handle ??
257
+ params.target?.entityId ??
258
+ params.target?.channelId ??
259
+ "").trim();
260
+ if (!userId || getChatTypeFromId(userId) !== "user") {
261
+ return null;
262
+ }
263
+ const user = await service.getUserProfile(userId).catch(() => null);
264
+ if (!user) {
265
+ return null;
266
+ }
267
+ return {
268
+ id: user.userId,
269
+ names: [user.displayName, user.userId].filter((value) => value.length > 0),
270
+ agentId: _handlerRuntime.agentId,
271
+ metadata: { line: objectToMetadataValue(user) },
272
+ };
273
+ },
274
+ getChatContext: async (target, context) => {
275
+ const room = target.roomId ? await context.runtime.getRoom(target.roomId) : null;
276
+ const channelId = String(target.channelId ?? room?.channelId ?? "").trim();
277
+ if (!channelId) {
278
+ return null;
279
+ }
280
+ const chatType = getChatTypeFromId(channelId);
281
+ let label = room?.name || channelId;
282
+ let metadata = { chatType, channelId };
283
+ if (chatType === "group" || chatType === "room") {
284
+ const group = await service.getGroupInfo(channelId).catch(() => null);
285
+ if (group?.groupName) {
286
+ label = group.groupName;
287
+ }
288
+ metadata = {
289
+ ...metadata,
290
+ ...(group ? { group: objectToMetadataValue(group) } : {}),
291
+ };
292
+ }
293
+ else {
294
+ const user = await service.getUserProfile(channelId).catch(() => null);
295
+ if (user?.displayName) {
296
+ label = user.displayName;
297
+ }
298
+ metadata = {
299
+ ...metadata,
300
+ ...(user ? { user: objectToMetadataValue(user) } : {}),
301
+ };
302
+ }
303
+ return {
304
+ target: {
305
+ source: LINE_SERVICE_NAME,
306
+ roomId: target.roomId,
307
+ channelId,
308
+ },
309
+ label,
310
+ summary: `LINE ${chatType} chat`,
311
+ metadata,
312
+ };
313
+ },
314
+ getUserContext: async (entityId, context) => {
315
+ const entity = typeof context.runtime.getEntityById === "function"
316
+ ? await context.runtime.getEntityById(String(entityId))
317
+ : null;
318
+ if (!entity) {
319
+ return null;
320
+ }
321
+ return {
322
+ entityId,
323
+ label: entity.names[0],
324
+ aliases: entity.names,
325
+ handles: {},
326
+ metadata: entity.metadata,
327
+ };
328
+ },
329
+ };
330
+ runtime.registerMessageConnector(registration);
331
+ return;
332
+ }
333
+ runtime.registerSendHandler(LINE_SERVICE_NAME, sendHandler);
334
+ }
335
+ /**
336
+ * Initialize the service.
337
+ */
338
+ async initialize() {
339
+ if (!this.runtime)
340
+ return;
341
+ logger.info("Starting LINE service...");
342
+ // Load settings
343
+ if (!this.settings) {
344
+ this.settings = this.loadSettings();
345
+ }
346
+ if (!this.settings.enabled) {
347
+ this.connected = false;
348
+ logger.info("LINE service disabled by LINE_ENABLED=false");
349
+ return;
350
+ }
351
+ this.validateSettings();
352
+ // Initialize LINE client
353
+ this.client = new messagingApi.MessagingApiClient({
354
+ channelAccessToken: this.settings.channelAccessToken,
355
+ });
356
+ this.connected = true;
357
+ logger.info("LINE service started");
358
+ // Emit connection ready event
359
+ if (this.runtime) {
360
+ this.runtime.emitEvent([LineEventTypes.CONNECTION_READY], {
361
+ runtime: this.runtime,
362
+ source: "line",
363
+ service: this,
364
+ });
365
+ }
366
+ }
367
+ /**
368
+ * Stop the LINE service.
369
+ */
370
+ async stop() {
371
+ logger.info("Stopping LINE service...");
372
+ this.connected = false;
373
+ this.client = null;
374
+ this.settings = null;
375
+ logger.info("LINE service stopped");
376
+ }
377
+ /**
378
+ * Check if the service is connected.
379
+ */
380
+ isConnected() {
381
+ return this.connected && this.client !== null;
382
+ }
383
+ /**
384
+ * Get bot info.
385
+ */
386
+ async getBotInfo() {
387
+ if (!this.client) {
388
+ return null;
389
+ }
390
+ const info = await this.client.getBotInfo();
391
+ return {
392
+ userId: info.userId,
393
+ displayName: info.displayName,
394
+ pictureUrl: info.pictureUrl,
395
+ };
396
+ }
397
+ /**
398
+ * Send a text message.
399
+ */
400
+ async sendMessage(to, text, options) {
401
+ if (!this.client) {
402
+ return { success: false, error: "Service not connected" };
403
+ }
404
+ const chunks = splitMessageForLine(text);
405
+ const messages = chunks.map((chunk) => ({
406
+ type: "text",
407
+ text: chunk,
408
+ }));
409
+ // Add quick replies to last message if provided
410
+ if (options?.quickReplyItems && messages.length > 0) {
411
+ const lastIdx = messages.length - 1;
412
+ messages[lastIdx].quickReply = {
413
+ items: options.quickReplyItems,
414
+ };
415
+ }
416
+ return this.pushMessages(to, messages);
417
+ }
418
+ /**
419
+ * Send multiple messages.
420
+ */
421
+ async sendMessages(to, messages) {
422
+ return this.pushMessages(to, messages);
423
+ }
424
+ /**
425
+ * Send a flex message.
426
+ */
427
+ async sendFlexMessage(to, flex) {
428
+ if (!this.client) {
429
+ return { success: false, error: "Service not connected" };
430
+ }
431
+ const message = {
432
+ type: "flex",
433
+ altText: flex.altText.slice(0, 400),
434
+ contents: flex.contents,
435
+ };
436
+ return this.pushMessages(to, [message]);
437
+ }
438
+ /**
439
+ * Send a template message.
440
+ */
441
+ async sendTemplateMessage(to, template) {
442
+ if (!this.client) {
443
+ return { success: false, error: "Service not connected" };
444
+ }
445
+ const validationError = validateTemplateUrls(template);
446
+ if (validationError) {
447
+ return { success: false, error: validationError };
448
+ }
449
+ const message = {
450
+ type: "template",
451
+ altText: template.altText.slice(0, 400),
452
+ template: template.template,
453
+ };
454
+ return this.pushMessages(to, [message]);
455
+ }
456
+ /**
457
+ * Send a location message.
458
+ */
459
+ async sendLocationMessage(to, location) {
460
+ if (!this.client) {
461
+ return { success: false, error: "Service not connected" };
462
+ }
463
+ const validationError = validateLocation(location);
464
+ if (validationError) {
465
+ return { success: false, error: validationError };
466
+ }
467
+ const message = {
468
+ type: "location",
469
+ title: location.title.slice(0, 100),
470
+ address: location.address.slice(0, 100),
471
+ latitude: location.latitude,
472
+ longitude: location.longitude,
473
+ };
474
+ return this.pushMessages(to, [message]);
475
+ }
476
+ /**
477
+ * Reply to a message using reply token.
478
+ */
479
+ async replyMessage(replyToken, messages) {
480
+ if (!this.client) {
481
+ return { success: false, error: "Service not connected" };
482
+ }
483
+ await this.client.replyMessage({
484
+ replyToken,
485
+ messages: messages.slice(0, MAX_LINE_BATCH_SIZE),
486
+ });
487
+ return {
488
+ success: true,
489
+ messageId: "reply",
490
+ chatId: "reply",
491
+ };
492
+ }
493
+ /**
494
+ * Get user profile.
495
+ */
496
+ async getUserProfile(userId) {
497
+ if (!this.client) {
498
+ return null;
499
+ }
500
+ const profile = await this.client.getProfile(userId);
501
+ return {
502
+ userId: profile.userId,
503
+ displayName: profile.displayName,
504
+ pictureUrl: profile.pictureUrl,
505
+ statusMessage: profile.statusMessage,
506
+ language: profile.language,
507
+ };
508
+ }
509
+ /**
510
+ * Get group info.
511
+ */
512
+ async getGroupInfo(groupId) {
513
+ if (!this.client) {
514
+ return null;
515
+ }
516
+ const chatType = getChatTypeFromId(groupId);
517
+ if (chatType === "group") {
518
+ const summary = await this.client.getGroupSummary(groupId);
519
+ return {
520
+ groupId: summary.groupId,
521
+ groupName: summary.groupName,
522
+ pictureUrl: summary.pictureUrl,
523
+ type: "group",
524
+ };
525
+ }
526
+ else if (chatType === "room") {
527
+ // Rooms don't have summary, just return ID
528
+ return {
529
+ groupId,
530
+ type: "room",
531
+ };
532
+ }
533
+ return null;
534
+ }
535
+ /**
536
+ * Leave a group or room.
537
+ */
538
+ async leaveChat(chatId, chatType) {
539
+ if (!this.client) {
540
+ throw new LineApiError("Service not connected");
541
+ }
542
+ if (chatType === "group") {
543
+ await this.client.leaveGroup(chatId);
544
+ }
545
+ else {
546
+ await this.client.leaveRoom(chatId);
547
+ }
548
+ }
549
+ /**
550
+ * Get the middleware config for webhook verification.
551
+ */
552
+ getMiddlewareConfig() {
553
+ if (!this.settings) {
554
+ throw new LineConfigurationError("Service not configured");
555
+ }
556
+ return {
557
+ channelSecret: this.settings.channelSecret,
558
+ };
559
+ }
560
+ /**
561
+ * Create Express middleware for webhook handling.
562
+ */
563
+ createMiddleware() {
564
+ return middleware(this.getMiddlewareConfig());
565
+ }
566
+ /**
567
+ * Handle webhook events.
568
+ */
569
+ async handleWebhookEvents(events) {
570
+ if (!this.runtime) {
571
+ return;
572
+ }
573
+ if (!Array.isArray(events)) {
574
+ return;
575
+ }
576
+ for (const event of events) {
577
+ if (!isRecord(event) || typeof event.type !== "string") {
578
+ continue;
579
+ }
580
+ await this.handleWebhookEvent(event);
581
+ }
582
+ }
583
+ /**
584
+ * Get current settings.
585
+ */
586
+ getSettings() {
587
+ return this.settings;
588
+ }
589
+ async sendDirectMessage(target, content) {
590
+ await this.sendConnectorContent(target, content);
591
+ }
592
+ async sendRoomMessage(target, content) {
593
+ await this.sendConnectorContent(target, content);
594
+ }
595
+ // Private methods
596
+ async handleSendMessage(runtime, target, content) {
597
+ const room = target.roomId ? await runtime.getRoom(target.roomId) : null;
598
+ const chatId = String(target.channelId ?? room?.channelId ?? "").trim();
599
+ if (!chatId) {
600
+ throw new Error("LINE target is missing a user, group, or room ID");
601
+ }
602
+ await this.sendConnectorContent(chatId, content);
603
+ }
604
+ async sendConnectorContent(to, content) {
605
+ const data = lineDataFromContent(content);
606
+ const flexMessage = data.flexMessage;
607
+ if (flexMessage) {
608
+ const result = await this.sendFlexMessage(to, flexMessage);
609
+ if (!result.success) {
610
+ throw new Error(result.error || "LINE flex message send failed");
611
+ }
612
+ return;
613
+ }
614
+ const location = data.location;
615
+ if (location) {
616
+ const result = await this.sendLocationMessage(to, location);
617
+ if (!result.success) {
618
+ throw new Error(result.error || "LINE location send failed");
619
+ }
620
+ return;
621
+ }
622
+ const templateMessage = data.templateMessage;
623
+ if (templateMessage) {
624
+ const result = await this.sendTemplateMessage(to, templateMessage);
625
+ if (!result.success) {
626
+ throw new Error(result.error || "LINE template message send failed");
627
+ }
628
+ return;
629
+ }
630
+ // Agent-generated attachments (#8876). LINE has a dedicated image message
631
+ // (url-based); it has no generic file message, so non-image media (video
632
+ // without a thumbnail, audio, documents) is appended to the text as a link
633
+ // rather than dropped. LINE requires https media URLs.
634
+ const attachments = Array.isArray(content.attachments) ? content.attachments : [];
635
+ const imageMessages = [];
636
+ const linkedUrls = [];
637
+ for (const media of attachments) {
638
+ const url = typeof media?.url === "string" ? media.url.trim() : "";
639
+ if (!/^https:\/\//i.test(url))
640
+ continue;
641
+ if (media.contentType === "image") {
642
+ imageMessages.push({
643
+ type: "image",
644
+ originalContentUrl: url,
645
+ previewImageUrl: url,
646
+ });
647
+ }
648
+ else {
649
+ linkedUrls.push(url);
650
+ }
651
+ }
652
+ const baseText = typeof content.text === "string" ? content.text.trim() : "";
653
+ const text = linkedUrls.length > 0 ? [baseText, ...linkedUrls].filter(Boolean).join("\n") : baseText;
654
+ if (!text && imageMessages.length === 0) {
655
+ return;
656
+ }
657
+ if (text) {
658
+ const result = await this.sendMessage(to, text, {
659
+ quickReplyItems: quickReplyItemsFromStrings(data.quickReplies),
660
+ });
661
+ if (!result.success) {
662
+ throw new Error(result.error || "LINE message send failed");
663
+ }
664
+ }
665
+ if (imageMessages.length > 0) {
666
+ const result = await this.pushMessages(to, imageMessages);
667
+ if (!result.success) {
668
+ throw new Error(result.error || "LINE image send failed");
669
+ }
670
+ }
671
+ }
672
+ async listConnectorRooms(runtime) {
673
+ if (typeof runtime.getRoomsForParticipant !== "function") {
674
+ return [];
675
+ }
676
+ const roomIds = await runtime.getRoomsForParticipant(runtime.agentId).catch(() => []);
677
+ const rooms = [];
678
+ for (const roomId of roomIds) {
679
+ const room = await runtime.getRoom(roomId).catch(() => null);
680
+ if (room?.source === LINE_SERVICE_NAME && room.channelId) {
681
+ rooms.push(room);
682
+ }
683
+ }
684
+ return rooms;
685
+ }
686
+ loadSettings() {
687
+ if (!this.runtime) {
688
+ throw new LineConfigurationError("Runtime not initialized");
689
+ }
690
+ const getStringSetting = (key) => {
691
+ const value = this.runtime.getSetting(key);
692
+ return typeof value === "string" ? value : "";
693
+ };
694
+ const channelAccessToken = getStringSetting("LINE_CHANNEL_ACCESS_TOKEN") || process.env.LINE_CHANNEL_ACCESS_TOKEN || "";
695
+ const channelSecret = getStringSetting("LINE_CHANNEL_SECRET") || process.env.LINE_CHANNEL_SECRET || "";
696
+ const webhookPath = getStringSetting("LINE_WEBHOOK_PATH") || process.env.LINE_WEBHOOK_PATH || "/webhooks/line";
697
+ const dmPolicyRaw = getStringSetting("LINE_DM_POLICY") || process.env.LINE_DM_POLICY || "pairing";
698
+ const dmPolicy = dmPolicyRaw;
699
+ const groupPolicyRaw = getStringSetting("LINE_GROUP_POLICY") || process.env.LINE_GROUP_POLICY || "allowlist";
700
+ const groupPolicy = groupPolicyRaw;
701
+ const allowFromRaw = getStringSetting("LINE_ALLOW_FROM") || process.env.LINE_ALLOW_FROM || "";
702
+ const allowFrom = allowFromRaw
703
+ .split(",")
704
+ .map((s) => s.trim())
705
+ .filter(Boolean);
706
+ const enabledRaw = getStringSetting("LINE_ENABLED") || process.env.LINE_ENABLED || "true";
707
+ const enabled = enabledRaw !== "false";
708
+ return {
709
+ channelAccessToken,
710
+ channelSecret,
711
+ webhookPath,
712
+ dmPolicy,
713
+ groupPolicy,
714
+ allowFrom,
715
+ enabled,
716
+ };
717
+ }
718
+ validateSettings() {
719
+ if (!this.settings) {
720
+ throw new LineConfigurationError("Settings not loaded");
721
+ }
722
+ if (!this.settings.channelAccessToken) {
723
+ throw new LineConfigurationError("LINE_CHANNEL_ACCESS_TOKEN is required", "LINE_CHANNEL_ACCESS_TOKEN");
724
+ }
725
+ if (!this.settings.channelSecret) {
726
+ throw new LineConfigurationError("LINE_CHANNEL_SECRET is required", "LINE_CHANNEL_SECRET");
727
+ }
728
+ }
729
+ async pushMessages(to, messages) {
730
+ if (!this.client) {
731
+ return { success: false, error: "Service not connected" };
732
+ }
733
+ // Send in batches of 5
734
+ for (let i = 0; i < messages.length; i += MAX_LINE_BATCH_SIZE) {
735
+ const batch = messages.slice(i, i + MAX_LINE_BATCH_SIZE);
736
+ await this.client.pushMessage({
737
+ to,
738
+ messages: batch,
739
+ });
740
+ }
741
+ // Emit sent event
742
+ if (this.runtime) {
743
+ this.runtime.emitEvent(LineEventTypes.MESSAGE_SENT, {
744
+ runtime: this.runtime,
745
+ source: "line",
746
+ to,
747
+ messageCount: messages.length,
748
+ });
749
+ }
750
+ return {
751
+ success: true,
752
+ messageId: Date.now().toString(),
753
+ chatId: to,
754
+ };
755
+ }
756
+ async handleWebhookEvent(event) {
757
+ if (!this.runtime) {
758
+ return;
759
+ }
760
+ switch (event.type) {
761
+ case "message":
762
+ await this.handleMessageEvent(event);
763
+ break;
764
+ case "follow":
765
+ this.runtime.emitEvent([LineEventTypes.FOLLOW], {
766
+ runtime: this.runtime,
767
+ source: "line",
768
+ userId: event.source?.userId,
769
+ timestamp: event.timestamp,
770
+ });
771
+ break;
772
+ case "unfollow":
773
+ this.runtime.emitEvent([LineEventTypes.UNFOLLOW], {
774
+ runtime: this.runtime,
775
+ source: "line",
776
+ userId: event.source?.userId,
777
+ timestamp: event.timestamp,
778
+ });
779
+ break;
780
+ case "join":
781
+ this.runtime.emitEvent([LineEventTypes.JOIN_GROUP], {
782
+ runtime: this.runtime,
783
+ source: "line",
784
+ groupId: event.source?.type === "group"
785
+ ? event.source.groupId
786
+ : event.source?.type === "room"
787
+ ? event.source.roomId
788
+ : undefined,
789
+ type: event.source?.type,
790
+ timestamp: event.timestamp,
791
+ });
792
+ break;
793
+ case "leave":
794
+ this.runtime.emitEvent([LineEventTypes.LEAVE_GROUP], {
795
+ runtime: this.runtime,
796
+ source: "line",
797
+ groupId: event.source?.type === "group"
798
+ ? event.source.groupId
799
+ : event.source?.type === "room"
800
+ ? event.source.roomId
801
+ : undefined,
802
+ type: event.source?.type,
803
+ timestamp: event.timestamp,
804
+ });
805
+ break;
806
+ case "postback":
807
+ if (!isRecord(event.postback) || typeof event.postback.data !== "string") {
808
+ return;
809
+ }
810
+ this.runtime.emitEvent([LineEventTypes.POSTBACK], {
811
+ runtime: this.runtime,
812
+ source: "line",
813
+ userId: event.source?.userId,
814
+ data: event.postback.data,
815
+ params: event.postback.params,
816
+ timestamp: event.timestamp,
817
+ });
818
+ break;
819
+ }
820
+ }
821
+ async handleMessageEvent(event) {
822
+ if (!this.runtime) {
823
+ return;
824
+ }
825
+ if (!isRecord(event.message) || typeof event.message.id !== "string") {
826
+ return;
827
+ }
828
+ if (typeof event.message.type !== "string") {
829
+ return;
830
+ }
831
+ const timestamp = Number.isFinite(event.timestamp) ? event.timestamp : Date.now();
832
+ const message = {
833
+ id: event.message.id,
834
+ type: event.message.type,
835
+ userId: event.source?.userId || "",
836
+ timestamp,
837
+ replyToken: event.replyToken,
838
+ };
839
+ // Add text for text messages
840
+ if (event.message.type === "text") {
841
+ message.text = event.message.text;
842
+ message.mention = event.message.mention;
843
+ }
844
+ // Add group/room ID if applicable
845
+ if (event.source?.type === "group") {
846
+ message.groupId = event.source.groupId;
847
+ }
848
+ else if (event.source?.type === "room") {
849
+ message.roomId = event.source.roomId;
850
+ }
851
+ // Emit message received event
852
+ this.runtime.emitEvent([LineEventTypes.MESSAGE_RECEIVED], {
853
+ runtime: this.runtime,
854
+ source: "line",
855
+ message,
856
+ lineSource: event.source,
857
+ replyToken: event.replyToken,
858
+ });
859
+ }
860
+ }
861
+ //# sourceMappingURL=service.js.map