@openclaw/line 2026.5.2-beta.2 → 2026.5.3-beta.1

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 (109) hide show
  1. package/dist/accounts-UXAvdBo_.js +105 -0
  2. package/dist/api.js +11 -0
  3. package/dist/basic-cards-7DTG6E-A.js +307 -0
  4. package/dist/card-command-UlPc6Vuc.js +240 -0
  5. package/dist/channel-Rls9s60K.js +601 -0
  6. package/dist/channel-plugin-api.js +2 -0
  7. package/dist/channel.runtime-bXoj8_Sl.js +4 -0
  8. package/dist/contract-api.js +2 -0
  9. package/dist/index.js +45 -0
  10. package/dist/markdown-to-line-CCWK6PJh.js +789 -0
  11. package/dist/monitor-B4_RxHc0.js +1523 -0
  12. package/dist/monitor.runtime-DyxYlrM9.js +2 -0
  13. package/dist/outbound-media-BfhBmWq5.js +55 -0
  14. package/dist/outbound.runtime-B-luYwEB.js +2 -0
  15. package/dist/probe-D9BPzb2n.js +30 -0
  16. package/dist/probe.runtime-N91OWW1J.js +2 -0
  17. package/dist/quick-reply-fallback-HskUUWsn.js +55 -0
  18. package/dist/reply-payload-transform-CFGTs9ln.js +713 -0
  19. package/dist/runtime-api.js +277 -0
  20. package/dist/schedule-cards-BwQChTdM.js +359 -0
  21. package/dist/secret-contract-api.js +5 -0
  22. package/dist/setup-api.js +2 -0
  23. package/dist/setup-entry.js +11 -0
  24. package/dist/setup-surface-DCH7CE-y.js +281 -0
  25. package/package.json +14 -6
  26. package/api.ts +0 -11
  27. package/channel-plugin-api.ts +0 -1
  28. package/contract-api.ts +0 -5
  29. package/index.ts +0 -54
  30. package/runtime-api.ts +0 -187
  31. package/secret-contract-api.ts +0 -4
  32. package/setup-api.ts +0 -2
  33. package/setup-entry.ts +0 -9
  34. package/src/account-helpers.ts +0 -16
  35. package/src/accounts.test.ts +0 -290
  36. package/src/accounts.ts +0 -187
  37. package/src/actions.ts +0 -61
  38. package/src/auto-reply-delivery.test.ts +0 -248
  39. package/src/auto-reply-delivery.ts +0 -200
  40. package/src/bindings.ts +0 -65
  41. package/src/bot-access.ts +0 -48
  42. package/src/bot-handlers.test.ts +0 -1089
  43. package/src/bot-handlers.ts +0 -642
  44. package/src/bot-message-context.test.ts +0 -405
  45. package/src/bot-message-context.ts +0 -581
  46. package/src/bot.ts +0 -70
  47. package/src/card-command.ts +0 -347
  48. package/src/channel-access-token.ts +0 -14
  49. package/src/channel-api.ts +0 -17
  50. package/src/channel-setup-status.contract.test.ts +0 -70
  51. package/src/channel-shared.ts +0 -48
  52. package/src/channel.logout.test.ts +0 -145
  53. package/src/channel.runtime.ts +0 -3
  54. package/src/channel.sendPayload.test.ts +0 -514
  55. package/src/channel.setup.ts +0 -11
  56. package/src/channel.status.test.ts +0 -63
  57. package/src/channel.ts +0 -154
  58. package/src/config-adapter.ts +0 -29
  59. package/src/config-schema.ts +0 -57
  60. package/src/download.test.ts +0 -133
  61. package/src/download.ts +0 -87
  62. package/src/flex-templates/basic-cards.ts +0 -395
  63. package/src/flex-templates/common.ts +0 -20
  64. package/src/flex-templates/media-control-cards.ts +0 -555
  65. package/src/flex-templates/message.ts +0 -13
  66. package/src/flex-templates/schedule-cards.ts +0 -467
  67. package/src/flex-templates/types.ts +0 -22
  68. package/src/flex-templates.ts +0 -32
  69. package/src/gateway.ts +0 -129
  70. package/src/group-keys.test.ts +0 -123
  71. package/src/group-keys.ts +0 -65
  72. package/src/group-policy.ts +0 -22
  73. package/src/markdown-to-line.test.ts +0 -348
  74. package/src/markdown-to-line.ts +0 -416
  75. package/src/message-cards.test.ts +0 -204
  76. package/src/monitor.lifecycle.test.ts +0 -421
  77. package/src/monitor.runtime.ts +0 -1
  78. package/src/monitor.ts +0 -506
  79. package/src/outbound-media.test.ts +0 -189
  80. package/src/outbound-media.ts +0 -120
  81. package/src/outbound.runtime.ts +0 -12
  82. package/src/outbound.ts +0 -356
  83. package/src/probe.contract.test.ts +0 -9
  84. package/src/probe.runtime.ts +0 -1
  85. package/src/probe.ts +0 -34
  86. package/src/quick-reply-fallback.ts +0 -10
  87. package/src/reply-chunks.test.ts +0 -179
  88. package/src/reply-chunks.ts +0 -110
  89. package/src/reply-payload-transform.test.ts +0 -387
  90. package/src/reply-payload-transform.ts +0 -317
  91. package/src/rich-menu.test.ts +0 -310
  92. package/src/rich-menu.ts +0 -326
  93. package/src/runtime.ts +0 -32
  94. package/src/send.test.ts +0 -346
  95. package/src/send.ts +0 -489
  96. package/src/setup-core.ts +0 -149
  97. package/src/setup-runtime-api.ts +0 -9
  98. package/src/setup-surface.test.ts +0 -474
  99. package/src/setup-surface.ts +0 -227
  100. package/src/signature.test.ts +0 -34
  101. package/src/signature.ts +0 -24
  102. package/src/status.ts +0 -37
  103. package/src/template-messages.ts +0 -333
  104. package/src/types.ts +0 -128
  105. package/src/webhook-node.test.ts +0 -513
  106. package/src/webhook-node.ts +0 -131
  107. package/src/webhook-utils.ts +0 -10
  108. package/src/webhook.ts +0 -111
  109. package/tsconfig.json +0 -16
@@ -1,642 +0,0 @@
1
- import type { webhook } from "@line/bot-sdk";
2
- import {
3
- buildMentionRegexes,
4
- matchesMentionPatterns,
5
- resolveInboundMentionDecision,
6
- } from "openclaw/plugin-sdk/channel-inbound";
7
- import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
8
- import { hasControlCommand, resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
9
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
10
- import {
11
- readChannelAllowFromStore,
12
- resolvePairingIdLabel,
13
- upsertChannelPairingRequest,
14
- } from "openclaw/plugin-sdk/conversation-runtime";
15
- import { evaluateMatchedGroupAccessForPolicy } from "openclaw/plugin-sdk/group-access";
16
- import { createClaimableDedupe, type ClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
17
- import {
18
- DEFAULT_GROUP_HISTORY_LIMIT,
19
- clearHistoryEntriesIfEnabled,
20
- recordPendingHistoryEntryIfEnabled,
21
- type HistoryEntry,
22
- } from "openclaw/plugin-sdk/reply-history";
23
- import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
24
- import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
25
- import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
26
- import {
27
- resolveAllowlistProviderRuntimeGroupPolicy,
28
- resolveDefaultGroupPolicy,
29
- warnMissingProviderGroupPolicyFallbackOnce,
30
- } from "openclaw/plugin-sdk/runtime-group-policy";
31
- import {
32
- firstDefined,
33
- isSenderAllowed,
34
- normalizeAllowFrom,
35
- normalizeDmAllowFromWithStore,
36
- type NormalizedAllowFrom,
37
- } from "./bot-access.js";
38
- import {
39
- buildLineMessageContext,
40
- buildLinePostbackContext,
41
- getLineSourceInfo,
42
- type LineInboundContext,
43
- } from "./bot-message-context.js";
44
- import { downloadLineMedia } from "./download.js";
45
- import { resolveLineGroupConfigEntry } from "./group-keys.js";
46
- import { pushMessageLine, replyMessageLine } from "./send.js";
47
- import type { LineGroupConfig, ResolvedLineAccount } from "./types.js";
48
-
49
- type FollowEvent = webhook.FollowEvent;
50
- type JoinEvent = webhook.JoinEvent;
51
- type LeaveEvent = webhook.LeaveEvent;
52
- type MessageEvent = webhook.MessageEvent;
53
- type PostbackEvent = webhook.PostbackEvent;
54
- type UnfollowEvent = webhook.UnfollowEvent;
55
- type WebhookEvent = webhook.Event;
56
-
57
- interface MediaRef {
58
- path: string;
59
- contentType?: string;
60
- }
61
-
62
- const LINE_DOWNLOADABLE_MESSAGE_TYPES: ReadonlySet<string> = new Set([
63
- "image",
64
- "video",
65
- "audio",
66
- "file",
67
- ]);
68
-
69
- function isDownloadableLineMessageType(
70
- messageType: MessageEvent["message"]["type"],
71
- ): messageType is "image" | "video" | "audio" | "file" {
72
- return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
73
- }
74
-
75
- export interface LineHandlerContext {
76
- cfg: OpenClawConfig;
77
- account: ResolvedLineAccount;
78
- runtime: RuntimeEnv;
79
- mediaMaxBytes: number;
80
- processMessage: (ctx: LineInboundContext) => Promise<void>;
81
- replayCache?: LineWebhookReplayCache;
82
- groupHistories?: Map<string, HistoryEntry[]>;
83
- historyLimit?: number;
84
- }
85
-
86
- const LINE_WEBHOOK_REPLAY_WINDOW_MS = 10 * 60 * 1000;
87
- const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
88
- export type LineWebhookReplayCache = ClaimableDedupe;
89
-
90
- export class LineRetryableWebhookError extends Error {
91
- constructor(message: string, options?: ErrorOptions) {
92
- super(message, options);
93
- this.name = "LineRetryableWebhookError";
94
- }
95
- }
96
-
97
- export function createLineWebhookReplayCache(): LineWebhookReplayCache {
98
- return createClaimableDedupe({
99
- ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
100
- memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES,
101
- });
102
- }
103
-
104
- function buildLineWebhookReplayKey(
105
- event: WebhookEvent,
106
- accountId: string,
107
- ): { key: string; eventId: string } | null {
108
- if (event.type === "message") {
109
- const messageId = event.message?.id?.trim();
110
- if (messageId) {
111
- return {
112
- key: `${accountId}|message:${messageId}`,
113
- eventId: `message:${messageId}`,
114
- };
115
- }
116
- }
117
- const eventId = (event as { webhookEventId?: string }).webhookEventId?.trim();
118
- if (!eventId) {
119
- return null;
120
- }
121
-
122
- const source = (
123
- event as {
124
- source?: { type?: string; userId?: string; groupId?: string; roomId?: string };
125
- }
126
- ).source;
127
- const sourceId =
128
- source?.type === "group"
129
- ? `group:${source.groupId ?? ""}`
130
- : source?.type === "room"
131
- ? `room:${source.roomId ?? ""}`
132
- : `user:${source?.userId ?? ""}`;
133
- return { key: `${accountId}|${event.type}|${sourceId}|${eventId}`, eventId: `event:${eventId}` };
134
- }
135
-
136
- type LineReplayCandidate = {
137
- key: string;
138
- eventId: string;
139
- cache: LineWebhookReplayCache;
140
- };
141
-
142
- function getLineReplayCandidate(
143
- event: WebhookEvent,
144
- context: LineHandlerContext,
145
- ): LineReplayCandidate | null {
146
- const replay = buildLineWebhookReplayKey(event, context.account.accountId);
147
- const cache = context.replayCache;
148
- if (!replay || !cache) {
149
- return null;
150
- }
151
- return { key: replay.key, eventId: replay.eventId, cache };
152
- }
153
-
154
- async function claimLineReplayEvent(
155
- candidate: LineReplayCandidate,
156
- ): Promise<{ skip: true; inFlightResult?: Promise<void> } | { skip: false }> {
157
- const claim = await candidate.cache.claim(candidate.key);
158
- if (claim.kind === "claimed") {
159
- return { skip: false };
160
- }
161
- if (claim.kind === "inflight") {
162
- logVerbose(`line: skipped in-flight replayed webhook event ${candidate.eventId}`);
163
- return { skip: true, inFlightResult: claim.pending.then(() => undefined) };
164
- }
165
- logVerbose(`line: skipped replayed webhook event ${candidate.eventId}`);
166
- return { skip: true };
167
- }
168
-
169
- function resolveLineGroupConfig(params: {
170
- config: ResolvedLineAccount["config"];
171
- groupId?: string;
172
- roomId?: string;
173
- }): LineGroupConfig | undefined {
174
- return resolveLineGroupConfigEntry(params.config.groups, {
175
- groupId: params.groupId,
176
- roomId: params.roomId,
177
- });
178
- }
179
-
180
- async function sendLinePairingReply(params: {
181
- senderId: string;
182
- replyToken?: string;
183
- context: LineHandlerContext;
184
- }): Promise<void> {
185
- const { senderId, replyToken, context } = params;
186
- const idLabel = (() => {
187
- try {
188
- return resolvePairingIdLabel("line");
189
- } catch {
190
- return "lineUserId";
191
- }
192
- })();
193
- await createChannelPairingChallengeIssuer({
194
- channel: "line",
195
- upsertPairingRequest: async ({ id, meta }) =>
196
- await upsertChannelPairingRequest({
197
- channel: "line",
198
- id,
199
- accountId: context.account.accountId,
200
- meta,
201
- }),
202
- })({
203
- senderId,
204
- senderIdLine: `Your ${idLabel}: ${senderId}`,
205
- onCreated: () => {
206
- logVerbose(`line pairing request sender=${senderId}`);
207
- },
208
- sendPairingReply: async (text) => {
209
- if (replyToken) {
210
- try {
211
- await replyMessageLine(replyToken, [{ type: "text", text }], {
212
- cfg: context.cfg,
213
- accountId: context.account.accountId,
214
- channelAccessToken: context.account.channelAccessToken,
215
- });
216
- return;
217
- } catch (err) {
218
- logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
219
- }
220
- }
221
- try {
222
- await pushMessageLine(`line:${senderId}`, text, {
223
- cfg: context.cfg,
224
- accountId: context.account.accountId,
225
- channelAccessToken: context.account.channelAccessToken,
226
- });
227
- } catch (err) {
228
- logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
229
- }
230
- },
231
- });
232
- }
233
-
234
- async function shouldProcessLineEvent(
235
- event: MessageEvent | PostbackEvent,
236
- context: LineHandlerContext,
237
- ): Promise<{ allowed: boolean; commandAuthorized: boolean }> {
238
- const denied = { allowed: false, commandAuthorized: false };
239
- const { cfg, account } = context;
240
- const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
241
- const senderId = userId ?? "";
242
- const dmPolicy = account.config.dmPolicy ?? "pairing";
243
-
244
- const storeAllowFrom = await readChannelAllowFromStore(
245
- "line",
246
- undefined,
247
- account.accountId,
248
- ).catch(() => []);
249
- const effectiveDmAllow = normalizeDmAllowFromWithStore({
250
- allowFrom: account.config.allowFrom,
251
- storeAllowFrom,
252
- dmPolicy,
253
- });
254
- const groupConfig = resolveLineGroupConfig({ config: account.config, groupId, roomId });
255
- const groupAllowOverride = groupConfig?.allowFrom;
256
- const fallbackGroupAllowFrom = account.config.allowFrom?.length
257
- ? account.config.allowFrom
258
- : undefined;
259
- const groupAllowFrom = firstDefined(
260
- groupAllowOverride,
261
- account.config.groupAllowFrom,
262
- fallbackGroupAllowFrom,
263
- );
264
- const effectiveGroupAllow = normalizeAllowFrom(groupAllowFrom);
265
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
266
- const { groupPolicy, providerMissingFallbackApplied } =
267
- resolveAllowlistProviderRuntimeGroupPolicy({
268
- providerConfigPresent: cfg.channels?.line !== undefined,
269
- groupPolicy: account.config.groupPolicy,
270
- defaultGroupPolicy,
271
- });
272
- warnMissingProviderGroupPolicyFallbackOnce({
273
- providerMissingFallbackApplied,
274
- providerKey: "line",
275
- accountId: account.accountId,
276
- log: (message) => logVerbose(message),
277
- });
278
-
279
- if (isGroup) {
280
- if (groupConfig?.enabled === false) {
281
- logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
282
- return denied;
283
- }
284
- if (groupAllowOverride !== undefined) {
285
- if (!senderId) {
286
- logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
287
- return denied;
288
- }
289
- if (!isSenderAllowed({ allow: effectiveGroupAllow, senderId })) {
290
- logVerbose(`Blocked line group sender ${senderId} (group allowFrom override)`);
291
- return denied;
292
- }
293
- }
294
- const senderGroupAccess = evaluateMatchedGroupAccessForPolicy({
295
- groupPolicy,
296
- requireMatchInput: true,
297
- hasMatchInput: Boolean(senderId),
298
- allowlistConfigured: effectiveGroupAllow.entries.length > 0,
299
- allowlistMatched:
300
- Boolean(senderId) &&
301
- isSenderAllowed({
302
- allow: effectiveGroupAllow,
303
- senderId,
304
- }),
305
- });
306
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
307
- logVerbose("Blocked line group message (groupPolicy: disabled)");
308
- return denied;
309
- }
310
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "missing_match_input") {
311
- logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
312
- return denied;
313
- }
314
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
315
- logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
316
- return denied;
317
- }
318
- if (!senderGroupAccess.allowed && senderGroupAccess.reason === "not_allowlisted") {
319
- logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
320
- return denied;
321
- }
322
- return {
323
- allowed: true,
324
- commandAuthorized: resolveLineCommandAuthorized({
325
- cfg,
326
- event,
327
- senderId,
328
- allow: effectiveGroupAllow,
329
- }),
330
- };
331
- }
332
-
333
- if (dmPolicy === "disabled") {
334
- logVerbose("Blocked line sender (dmPolicy: disabled)");
335
- return denied;
336
- }
337
-
338
- const dmAllowed = isSenderAllowed({ allow: effectiveDmAllow, senderId });
339
- if (!dmAllowed) {
340
- if (dmPolicy === "pairing") {
341
- if (!senderId) {
342
- logVerbose("Blocked line sender (dmPolicy: pairing, no sender ID)");
343
- return denied;
344
- }
345
- await sendLinePairingReply({
346
- senderId,
347
- replyToken: "replyToken" in event ? event.replyToken : undefined,
348
- context,
349
- });
350
- } else {
351
- logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${dmPolicy})`);
352
- }
353
- return denied;
354
- }
355
-
356
- return {
357
- allowed: true,
358
- commandAuthorized: resolveLineCommandAuthorized({
359
- cfg,
360
- event,
361
- senderId,
362
- allow: effectiveDmAllow,
363
- }),
364
- };
365
- }
366
-
367
- function getLineMentionees(
368
- message: MessageEvent["message"],
369
- ): Array<{ type?: string; isSelf?: boolean }> {
370
- if (message.type !== "text") {
371
- return [];
372
- }
373
- const mentionees = (
374
- message as Record<string, unknown> & {
375
- mention?: { mentionees?: Array<{ type?: string; isSelf?: boolean }> };
376
- }
377
- ).mention?.mentionees;
378
- return Array.isArray(mentionees) ? mentionees : [];
379
- }
380
-
381
- function isLineBotMentioned(message: MessageEvent["message"]): boolean {
382
- return getLineMentionees(message).some((m) => m.isSelf === true || m.type === "all");
383
- }
384
-
385
- function hasAnyLineMention(message: MessageEvent["message"]): boolean {
386
- return getLineMentionees(message).length > 0;
387
- }
388
-
389
- function resolveEventRawText(event: MessageEvent | PostbackEvent): string {
390
- if (event.type === "message") {
391
- const msg = event.message;
392
- if (msg.type === "text") {
393
- return msg.text;
394
- }
395
- return "";
396
- }
397
- if (event.type === "postback") {
398
- return event.postback?.data?.trim() ?? "";
399
- }
400
- return "";
401
- }
402
-
403
- function resolveLineCommandAuthorized(params: {
404
- cfg: OpenClawConfig;
405
- event: MessageEvent | PostbackEvent;
406
- senderId?: string;
407
- allow: NormalizedAllowFrom;
408
- }): boolean {
409
- const senderAllowedForCommands = isSenderAllowed({
410
- allow: params.allow,
411
- senderId: params.senderId,
412
- });
413
- const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
414
- const rawText = resolveEventRawText(params.event);
415
- const commandGate = resolveControlCommandGate({
416
- useAccessGroups,
417
- authorizers: [{ configured: params.allow.hasEntries, allowed: senderAllowedForCommands }],
418
- allowTextCommands: true,
419
- hasControlCommand: hasControlCommand(rawText, params.cfg),
420
- });
421
- return commandGate.commandAuthorized;
422
- }
423
-
424
- async function handleMessageEvent(event: MessageEvent, context: LineHandlerContext): Promise<void> {
425
- const { cfg, account, runtime, mediaMaxBytes, processMessage } = context;
426
- const message = event.message;
427
-
428
- const decision = await shouldProcessLineEvent(event, context);
429
- if (!decision.allowed) {
430
- return;
431
- }
432
-
433
- const { isGroup, groupId, roomId } = getLineSourceInfo(event.source);
434
- if (isGroup) {
435
- const groupConfig = resolveLineGroupConfig({ config: account.config, groupId, roomId });
436
- const requireMention = groupConfig?.requireMention !== false;
437
- const rawText = message.type === "text" ? message.text : "";
438
- const sourceInfo = getLineSourceInfo(event.source);
439
- const peerId = groupId ?? roomId ?? sourceInfo.userId ?? "unknown";
440
- const { agentId } = resolveAgentRoute({
441
- cfg,
442
- channel: "line",
443
- accountId: account.accountId,
444
- peer: { kind: "group", id: peerId },
445
- });
446
- const mentionRegexes = buildMentionRegexes(cfg, agentId);
447
- const wasMentionedByNative = isLineBotMentioned(message);
448
- const wasMentionedByPattern =
449
- message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
450
- const wasMentioned = wasMentionedByNative || wasMentionedByPattern;
451
- const mentionDecision = resolveInboundMentionDecision({
452
- facts: {
453
- canDetectMention: message.type === "text",
454
- wasMentioned,
455
- hasAnyMention: hasAnyLineMention(message),
456
- implicitMentionKinds: [],
457
- },
458
- policy: {
459
- isGroup: true,
460
- requireMention,
461
- allowTextCommands: true,
462
- hasControlCommand: hasControlCommand(rawText, cfg),
463
- commandAuthorized: decision.commandAuthorized,
464
- },
465
- });
466
- if (mentionDecision.shouldSkip) {
467
- logVerbose(`line: skipping group message (requireMention, not mentioned)`);
468
- const historyKey = groupId ?? roomId;
469
- const senderId = sourceInfo.userId ?? "unknown";
470
- if (historyKey && context.groupHistories) {
471
- recordPendingHistoryEntryIfEnabled({
472
- historyMap: context.groupHistories,
473
- historyKey,
474
- limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
475
- entry: {
476
- sender: `user:${senderId}`,
477
- body: rawText || `<${message.type}>`,
478
- timestamp: event.timestamp,
479
- },
480
- });
481
- }
482
- return;
483
- }
484
- }
485
-
486
- const allMedia: MediaRef[] = [];
487
-
488
- if (isDownloadableLineMessageType(message.type)) {
489
- try {
490
- const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes);
491
- allMedia.push({
492
- path: media.path,
493
- contentType: media.contentType,
494
- });
495
- } catch (err) {
496
- const errMsg = String(err);
497
- if (errMsg.includes("exceeds") && errMsg.includes("limit")) {
498
- logVerbose(`line: media exceeds size limit for message ${message.id}`);
499
- } else {
500
- runtime.error?.(danger(`line: failed to download media: ${errMsg}`));
501
- }
502
- }
503
- }
504
-
505
- const messageContext = await buildLineMessageContext({
506
- event,
507
- allMedia,
508
- cfg,
509
- account,
510
- commandAuthorized: decision.commandAuthorized,
511
- groupHistories: context.groupHistories,
512
- historyLimit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
513
- });
514
-
515
- if (!messageContext) {
516
- logVerbose("line: skipping empty message");
517
- return;
518
- }
519
-
520
- await processMessage(messageContext);
521
-
522
- if (isGroup && context.groupHistories) {
523
- const historyKey = groupId ?? roomId;
524
- if (historyKey && context.groupHistories.has(historyKey)) {
525
- clearHistoryEntriesIfEnabled({
526
- historyMap: context.groupHistories,
527
- historyKey,
528
- limit: context.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
529
- });
530
- }
531
- }
532
- }
533
-
534
- async function handleFollowEvent(event: FollowEvent, _context: LineHandlerContext): Promise<void> {
535
- const { userId } = getLineSourceInfo(event.source);
536
- logVerbose(`line: user ${userId ?? "unknown"} followed`);
537
- }
538
-
539
- async function handleUnfollowEvent(
540
- event: UnfollowEvent,
541
- _context: LineHandlerContext,
542
- ): Promise<void> {
543
- const { userId } = getLineSourceInfo(event.source);
544
- logVerbose(`line: user ${userId ?? "unknown"} unfollowed`);
545
- }
546
-
547
- async function handleJoinEvent(event: JoinEvent, _context: LineHandlerContext): Promise<void> {
548
- const { groupId, roomId } = getLineSourceInfo(event.source);
549
- logVerbose(`line: bot joined ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
550
- }
551
-
552
- async function handleLeaveEvent(event: LeaveEvent, _context: LineHandlerContext): Promise<void> {
553
- const { groupId, roomId } = getLineSourceInfo(event.source);
554
- logVerbose(`line: bot left ${groupId ? `group ${groupId}` : `room ${roomId}`}`);
555
- }
556
-
557
- async function handlePostbackEvent(
558
- event: PostbackEvent,
559
- context: LineHandlerContext,
560
- ): Promise<void> {
561
- const data = event.postback.data;
562
- logVerbose(`line: received postback: ${data}`);
563
-
564
- const decision = await shouldProcessLineEvent(event, context);
565
- if (!decision.allowed) {
566
- return;
567
- }
568
-
569
- const postbackContext = await buildLinePostbackContext({
570
- event,
571
- cfg: context.cfg,
572
- account: context.account,
573
- commandAuthorized: decision.commandAuthorized,
574
- });
575
- if (!postbackContext) {
576
- return;
577
- }
578
-
579
- await context.processMessage(postbackContext);
580
- }
581
-
582
- export async function handleLineWebhookEvents(
583
- events: WebhookEvent[],
584
- context: LineHandlerContext,
585
- ): Promise<void> {
586
- let firstError: unknown;
587
- for (const event of events) {
588
- const replayCandidate = getLineReplayCandidate(event, context);
589
- const replaySkip = replayCandidate ? await claimLineReplayEvent(replayCandidate) : null;
590
- if (replaySkip?.skip) {
591
- if (replaySkip.inFlightResult) {
592
- try {
593
- await replaySkip.inFlightResult;
594
- } catch (err) {
595
- context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
596
- firstError ??= err;
597
- }
598
- }
599
- continue;
600
- }
601
- try {
602
- switch (event.type) {
603
- case "message":
604
- await handleMessageEvent(event, context);
605
- break;
606
- case "follow":
607
- await handleFollowEvent(event, context);
608
- break;
609
- case "unfollow":
610
- await handleUnfollowEvent(event, context);
611
- break;
612
- case "join":
613
- await handleJoinEvent(event, context);
614
- break;
615
- case "leave":
616
- await handleLeaveEvent(event, context);
617
- break;
618
- case "postback":
619
- await handlePostbackEvent(event, context);
620
- break;
621
- default:
622
- logVerbose(`line: unhandled event type: ${(event as WebhookEvent).type}`);
623
- }
624
- if (replayCandidate) {
625
- await replayCandidate.cache.commit(replayCandidate.key);
626
- }
627
- } catch (err) {
628
- if (replayCandidate) {
629
- if (err instanceof LineRetryableWebhookError) {
630
- replayCandidate.cache.release(replayCandidate.key, { error: err });
631
- } else {
632
- await replayCandidate.cache.commit(replayCandidate.key);
633
- }
634
- }
635
- context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
636
- firstError ??= err;
637
- }
638
- }
639
- if (firstError) {
640
- throw firstError;
641
- }
642
- }