mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 01:04:32 +00:00
fix(line): synthesize media/auth/routing webhook regressions (openclaw#32546) thanks @Takhoffman
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import type {
|
||||
LeaveEvent,
|
||||
PostbackEvent,
|
||||
} from "@line/bot-sdk";
|
||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||
import { resolveControlCommandGate } from "../channels/command-gating.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
@@ -42,6 +44,19 @@ interface MediaRef {
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
const LINE_DOWNLOADABLE_MESSAGE_TYPES: ReadonlySet<string> = new Set([
|
||||
"image",
|
||||
"video",
|
||||
"audio",
|
||||
"file",
|
||||
]);
|
||||
|
||||
function isDownloadableLineMessageType(
|
||||
messageType: MessageEvent["message"]["type"],
|
||||
): messageType is "image" | "video" | "audio" | "file" {
|
||||
return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
|
||||
}
|
||||
|
||||
export interface LineHandlerContext {
|
||||
cfg: OpenClawConfig;
|
||||
account: ResolvedLineAccount;
|
||||
@@ -113,10 +128,15 @@ async function sendLinePairingReply(params: {
|
||||
}
|
||||
}
|
||||
|
||||
type LineAccessDecision = {
|
||||
allowed: boolean;
|
||||
commandAuthorized: boolean;
|
||||
};
|
||||
|
||||
async function shouldProcessLineEvent(
|
||||
event: MessageEvent | PostbackEvent,
|
||||
context: LineHandlerContext,
|
||||
): Promise<boolean> {
|
||||
): Promise<LineAccessDecision> {
|
||||
const { cfg, account } = context;
|
||||
const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source);
|
||||
const senderId = userId ?? "";
|
||||
@@ -159,45 +179,59 @@ async function shouldProcessLineEvent(
|
||||
log: (message) => logVerbose(message),
|
||||
});
|
||||
|
||||
const denied = { allowed: false, commandAuthorized: false };
|
||||
|
||||
if (isGroup) {
|
||||
if (groupConfig?.enabled === false) {
|
||||
logVerbose(`Blocked line group ${groupId ?? roomId ?? "unknown"} (group disabled)`);
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
if (typeof groupAllowOverride !== "undefined") {
|
||||
if (!senderId) {
|
||||
logVerbose("Blocked line group message (group allowFrom override, no sender ID)");
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
if (!isSenderAllowed({ allow: effectiveGroupAllow, senderId })) {
|
||||
logVerbose(`Blocked line group sender ${senderId} (group allowFrom override)`);
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
}
|
||||
if (groupPolicy === "disabled") {
|
||||
logVerbose("Blocked line group message (groupPolicy: disabled)");
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
if (groupPolicy === "allowlist") {
|
||||
if (!senderId) {
|
||||
logVerbose("Blocked line group message (no sender ID, groupPolicy: allowlist)");
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
if (!effectiveGroupAllow.hasEntries) {
|
||||
logVerbose("Blocked line group message (groupPolicy: allowlist, no groupAllowFrom)");
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
if (!isSenderAllowed({ allow: effectiveGroupAllow, senderId })) {
|
||||
logVerbose(`Blocked line group message from ${senderId} (groupPolicy: allowlist)`);
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
// Resolve command authorization using the same pattern as Telegram/Discord/Slack.
|
||||
const allowForCommands = effectiveGroupAllow;
|
||||
const senderAllowedForCommands = isSenderAllowed({ allow: allowForCommands, senderId });
|
||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||
const rawText = resolveEventRawText(event);
|
||||
const commandGate = resolveControlCommandGate({
|
||||
useAccessGroups,
|
||||
authorizers: [{ configured: allowForCommands.hasEntries, allowed: senderAllowedForCommands }],
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommand(rawText, cfg),
|
||||
});
|
||||
return { allowed: true, commandAuthorized: commandGate.commandAuthorized };
|
||||
}
|
||||
|
||||
if (dmPolicy === "disabled") {
|
||||
logVerbose("Blocked line sender (dmPolicy: disabled)");
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
|
||||
const dmAllowed = dmPolicy === "open" || isSenderAllowed({ allow: effectiveDmAllow, senderId });
|
||||
@@ -205,7 +239,7 @@ async function shouldProcessLineEvent(
|
||||
if (dmPolicy === "pairing") {
|
||||
if (!senderId) {
|
||||
logVerbose("Blocked line sender (dmPolicy: pairing, no sender ID)");
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
await sendLinePairingReply({
|
||||
senderId,
|
||||
@@ -215,24 +249,51 @@ async function shouldProcessLineEvent(
|
||||
} else {
|
||||
logVerbose(`Blocked line sender ${senderId || "unknown"} (dmPolicy: ${dmPolicy})`);
|
||||
}
|
||||
return false;
|
||||
return denied;
|
||||
}
|
||||
|
||||
return true;
|
||||
// Resolve command authorization for DMs.
|
||||
const allowForCommands = effectiveDmAllow;
|
||||
const senderAllowedForCommands = isSenderAllowed({ allow: allowForCommands, senderId });
|
||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||
const rawText = resolveEventRawText(event);
|
||||
const commandGate = resolveControlCommandGate({
|
||||
useAccessGroups,
|
||||
authorizers: [{ configured: allowForCommands.hasEntries, allowed: senderAllowedForCommands }],
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommand(rawText, cfg),
|
||||
});
|
||||
return { allowed: true, commandAuthorized: commandGate.commandAuthorized };
|
||||
}
|
||||
|
||||
/** Extract raw text from a LINE message or postback event for command detection. */
|
||||
function resolveEventRawText(event: MessageEvent | PostbackEvent): string {
|
||||
if (event.type === "message") {
|
||||
const msg = event.message;
|
||||
if (msg.type === "text") {
|
||||
return msg.text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (event.type === "postback") {
|
||||
return event.postback?.data?.trim() ?? "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function handleMessageEvent(event: MessageEvent, context: LineHandlerContext): Promise<void> {
|
||||
const { cfg, account, runtime, mediaMaxBytes, processMessage } = context;
|
||||
const message = event.message;
|
||||
|
||||
if (!(await shouldProcessLineEvent(event, context))) {
|
||||
const decision = await shouldProcessLineEvent(event, context);
|
||||
if (!decision.allowed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Download media if applicable
|
||||
const allMedia: MediaRef[] = [];
|
||||
|
||||
if (message.type === "image" || message.type === "video" || message.type === "audio") {
|
||||
if (isDownloadableLineMessageType(message.type)) {
|
||||
try {
|
||||
const media = await downloadLineMedia(message.id, account.channelAccessToken, mediaMaxBytes);
|
||||
allMedia.push({
|
||||
@@ -255,6 +316,7 @@ async function handleMessageEvent(event: MessageEvent, context: LineHandlerConte
|
||||
allMedia,
|
||||
cfg,
|
||||
account,
|
||||
commandAuthorized: decision.commandAuthorized,
|
||||
});
|
||||
|
||||
if (!messageContext) {
|
||||
@@ -298,7 +360,8 @@ async function handlePostbackEvent(
|
||||
const data = event.postback.data;
|
||||
logVerbose(`line: received postback: ${data}`);
|
||||
|
||||
if (!(await shouldProcessLineEvent(event, context))) {
|
||||
const decision = await shouldProcessLineEvent(event, context);
|
||||
if (!decision.allowed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,6 +369,7 @@ async function handlePostbackEvent(
|
||||
event,
|
||||
cfg: context.cfg,
|
||||
account: context.account,
|
||||
commandAuthorized: decision.commandAuthorized,
|
||||
});
|
||||
if (!postbackContext) {
|
||||
return;
|
||||
@@ -344,6 +408,9 @@ export async function handleLineWebhookEvents(
|
||||
}
|
||||
} catch (err) {
|
||||
context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
|
||||
// Continue processing remaining events in this batch. Webhook ACK is sent
|
||||
// before processing, so dropping later events here would make them unrecoverable.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user