openclaw - ✅(Solved) Fix LINE: Add inbound media persistence to ~/.openclaw/media/inbound/ [2 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#73370Fetched 2026-04-29 06:20:29
View on GitHub
Comments
2
Participants
3
Timeline
5
Reactions
0
Author
Timeline (top)
commented ×2cross-referenced ×2closed ×1

Fix Action

Fixed

PR fix notes

PR #73392: fix(line): persist inbound media to ~/.openclaw/media/inbound/ via saveMediaBuffer

Description (problem / solution / changelog)

What

LINE inbound media (images/audio/video from downloadLineMedia) is currently written to a random line-media-* file under the OpenClaw temp directory. The WhatsApp inbound monitor instead persists inbound media under ~/.openclaw/media/inbound/ via saveMediaBuffer(..., "inbound", ...). That difference means LINE files don't survive temp-dir cleanup and break the cross-channel inbound media contract.

Fix

Switch downloadLineMedia to call saveMediaBuffer(buffer, contentType, "inbound", maxBytes) (matching WhatsApp). Removed the now-unused getExtensionForContentType helper and fs / buildRandomTempFilePath imports — saveMediaBuffer already derives the extension from the detected MIME and writes under media/inbound/ with the same secure mode policy.

The streaming size guard is preserved (returns the same Media exceeds <N>MB limit error before any write), and saveMediaBuffer re-enforces it on the final buffer.

Tests

Updated extensions/line/src/download.test.ts to mock the new saveMediaBuffer dependency. All 4 existing cases pass:

✓ persists inbound media via saveMediaBuffer (inbound subdir)
✓ rejects oversized media before persisting
✓ classifies M4A ftyp major brand as audio/mp4
✓ detects MP4 video from ftyp major brand (isom)

The path-traversal-from-messageId concern from the prior test is now structurally moot: the path is computed by saveMediaBuffer from a random UUID, not from messageId.

Diff

  • extensions/line/src/download.ts — swap temp write for saveMediaBuffer, drop unused helper/imports.
  • extensions/line/src/download.test.ts — mock openclaw/plugin-sdk/media-store instead of asserting on temp path semantics.

Fixes #73370

Changed files

  • extensions/line/src/download.test.ts (modified, +30/-31)
  • extensions/line/src/download.ts (modified, +5/-28)

PR #73398: fix(line): persist inbound LINE media to ~/.openclaw/media/inbound/ via saveMediaBuffer

Description (problem / solution / changelog)

Summary

  • Problem: downloadLineMedia in extensions/line/src/download.ts wrote inbound LINE media (images, audio, video) only to /tmp/openclaw/line-media-*.<ext>. After /tmp cleanup the file disappears, so any agent path that re-reads the inbound media later (memory, follow-up turns, retries, downstream tools) gets a missing file. Other channels (WhatsApp, BlueBubbles, Zalo) already persist via saveMediaBuffer(..., "inbound", ...) to ~/.openclaw/media/inbound/.
  • Why it matters: LINE inbound media is silently transient; behaviour diverges from every other inbound channel that ships in the bundled distribution.
  • What changed: downloadLineMedia now calls saveMediaBuffer(buffer, contentType, "inbound", maxBytes) from openclaw/plugin-sdk/media-store and returns the persisted path. The temp-file write and the now-unused getExtensionForContentType helper / buildRandomTempFilePath import are removed. Tests are rewritten to mock the media-store and assert the new persistence behaviour while keeping the existing security property (externally supplied messageId is never reflected in the on-disk path).
  • What did NOT change (scope boundary): no LINE webhook / bot-handler logic, no content-type detection (still local in this file), no size cap semantics (maxBytes is still enforced before reading the full buffer, and saveMediaBuffer re-applies it defensively). No other channel touched.

Change Type

  • Bug fix

Scope

  • line plugin (inbound media download)

Linked Issue/PR

  • Closes #73370
  • This PR fixes a bug or regression

Root Cause

  • Root cause: downloadLineMedia was implemented to write to a per-call random buildRandomTempFilePath({ prefix: "line-media", ... }) and never copied the buffer to the canonical inbound media store. When the OS cleans /tmp, the file referenced by path disappears.
  • Missing detection / guardrail: no test asserted persistence to ~/.openclaw/media/inbound/. Existing tests only covered content-type detection and the security property that messageId is not used in the path.
  • Contributing context: WhatsApp, BlueBubbles, Zalo already integrate with saveMediaBuffer; LINE was the inconsistent outlier.

Regression Test Plan

  • Coverage level that should have caught this:
    • Unit test
  • Target test or file: extensions/line/src/download.test.ts
  • Scenarios the test locks in:
    • saveMediaBuffer is called exactly once with ("inbound", maxBytes) and the detected content type.
    • The returned result.path is the inbound path, not a /tmp/openclaw/line-media-* path.
    • The user-supplied messageId (including a path-traversal probe) does not appear in any string argument to saveMediaBuffer and does not end up in result.path.
    • Oversize media is rejected before saveMediaBuffer runs.
    • Content-type detection (M4A audio, isom MP4 video) still flows through the saved path.
    • Defensive errors from saveMediaBuffer propagate out of downloadLineMedia.

User-visible / Behavior Changes

  • LINE inbound images, audio, and video now appear under ~/.openclaw/media/inbound/<uuid>.<ext> instead of /tmp/openclaw/line-media-*.<ext>.
  • Anything consuming the returned path (bot-handlers passing it to the agent) keeps working — the path is still an absolute file on disk; only the directory changed.
  • No config or env changes.

Security Impact

  • New permissions/capabilities? No — the inbound dir is already created with 0o700 by the media store and lives under the user's ~/.openclaw/.
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? Inbound LINE media now persists alongside WhatsApp/BlueBubbles/Zalo media until rotated by the existing media-store policy, matching documented behaviour for inbound channels.
  • Path injection: still safe — saveMediaBuffer builds the filename from crypto.randomUUID() and never uses the LINE messageId. The existing security test in download.test.ts is preserved (and strengthened: it now also asserts the messageId is not passed through any of the saveMediaBuffer arguments).

Repro + Verification

Steps

  1. Configure a LINE channel.
  2. Send the bot an image.
  3. After a /tmp cleanup interval (or immediately on systems with aggressive /tmp policy), check ~/.openclaw/media/inbound/.

Expected

  • The image is in ~/.openclaw/media/inbound/<uuid>.<ext> and survives /tmp rotation.

Actual (before fix)

  • The image is in /tmp/openclaw/line-media-*.<ext> and is lost after /tmp cleanup.

Evidence

  • The new unit tests in extensions/line/src/download.test.ts lock in the persistence call and the path returned to bot-handlers. Live LINE validation requires a configured LINE bot and is left to CI / downstream verification.

Human Verification

  • Verified scenarios: TypeScript clean on touched files; getErrors returned no errors. Test mock structure follows the same vi.hoisted(() => vi.fn()) pattern already used elsewhere in the LINE test suite.
  • Edge cases checked: oversize media (still rejected before saving), M4A/MP4 content-type detection, malicious messageId (a/../../../../etc/passwd).
  • What I did NOT verify: a live LINE webhook end-to-end run; full pnpm check / pnpm test was not exercised locally, leaving CI as the verification surface.

Compatibility / Migration

  • Backward compatible? Yes — the return type of downloadLineMedia is unchanged; only the directory in the path differs.
  • Config/env changes? No
  • Migration needed? No — old /tmp files are simply not regenerated; no on-disk migration required.

Risks and Mitigations

  • Risk: the media-store has a different default size cap than the LINE plugin's maxBytes parameter.
    • Mitigation: saveMediaBuffer accepts maxBytes as the 4th argument and we forward the LINE-side cap explicitly, so the effective cap is unchanged.
  • Risk: LINE plugin previously assumed temp files would be cleaned implicitly by /tmp rotation.
    • Mitigation: inbound media-store rotation is handled by the same shared cleanup path other channels rely on. No new disk-leak vector relative to WhatsApp/BlueBubbles/Zalo.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/line/src/download.test.ts (modified, +50/-30)
  • extensions/line/src/download.ts (modified, +11/-30)

Code Example

await fs.promises.writeFile(filePath, buffer);
logVerbose(`line: downloaded media ${messageId} to ${filePath} (${buffer.length} bytes)`);
return { path: filePath, contentType, size: buffer.length };

---

mediaPath = (await saveMediaBuffer(..., "inbound", ...)).path;

---

await fs.promises.writeFile(filePath, buffer);

// Persist to inbound directory
const saved = await saveMediaBuffer(buffer, contentType, "inbound", maxBytes);
await fs.promises.unlink(filePath).catch(() => {}); // Clean up temp file

logVerbose(`line: persisted media ${messageId} to ${saved.path}`);
return { path: saved.path, contentType, size: buffer.length };
RAW_BUFFERClick to expand / collapse

Issue: LINE inbound media missing persistence compared to WhatsApp

Problem Description

The LINE channel handles inbound images differently from the WhatsApp channel:

  • WhatsApp: Uses saveMediaBuffer(..., "inbound", ...) to persist images to ~/.openclaw/media/inbound/
  • LINE: Only saves to /tmp/openclaw/line-media-*.ext (temporary storage)

This causes images to be inaccessible after cleanup and creates inconsistent behavior across channels.

Reproduction Steps

  1. Send an image to the bot via LINE
  2. Check ~/.openclaw/media/inbound/ → No file exists
  3. Check /tmp/openclaw/ → File exists as line-media-*.jpg

Expected Behavior

LINE images should persist to ~/.openclaw/media/inbound/ just like WhatsApp images do.

Code Location

File: extensions/line/runtime/monitor.ts

Function downloadLineMedia() currently returns a temporary file path:

await fs.promises.writeFile(filePath, buffer);
logVerbose(`line: downloaded media ${messageId} to ${filePath} (${buffer.length} bytes)`);
return { path: filePath, contentType, size: buffer.length };

WhatsApp reference implementation (extensions/whatsapp/runtime/monitor.ts):

mediaPath = (await saveMediaBuffer(..., "inbound", ...)).path;

Proposed Fix

Add saveMediaBuffer call in downloadLineMedia() to persist media:

await fs.promises.writeFile(filePath, buffer);

// Persist to inbound directory
const saved = await saveMediaBuffer(buffer, contentType, "inbound", maxBytes);
await fs.promises.unlink(filePath).catch(() => {}); // Clean up temp file

logVerbose(`line: persisted media ${messageId} to ${saved.path}`);
return { path: saved.path, contentType, size: buffer.length };

Related Code

  • extensions/line/runtime/monitor.ts - downloadLineMedia()
  • extensions/whatsapp/runtime/monitor.ts - saveMediaBuffer usage example
  • plugin-sdk/media-store - saveMediaBuffer API

Version

OpenClaw 2026.4.25 (aa36ee6)


Note: I have verified this fix works locally and am willing to submit a PR.

extent analysis

TL;DR

The proposed fix involves adding a saveMediaBuffer call in downloadLineMedia() to persist LINE inbound media to the ~/.openclaw/media/inbound/ directory.

Guidance

  • Review the downloadLineMedia() function in extensions/line/runtime/monitor.ts to ensure the saveMediaBuffer call is correctly implemented.
  • Verify that the saveMediaBuffer API in plugin-sdk/media-store is compatible with the proposed fix.
  • Test the fix by sending an image to the bot via LINE and checking if the image is persisted to ~/.openclaw/media/inbound/.
  • Consider submitting a PR with the proposed fix, as the author has already verified it works locally.

Example

The proposed fix code snippet is already provided in the issue:

await fs.promises.writeFile(filePath, buffer);

// Persist to inbound directory
const saved = await saveMediaBuffer(buffer, contentType, "inbound", maxBytes);
await fs.promises.unlink(filePath).catch(() => {}); // Clean up temp file

logVerbose(`line: persisted media ${messageId} to ${saved.path}`);
return { path: saved.path, contentType, size: buffer.length };

Notes

The fix assumes that the saveMediaBuffer API is correctly implemented and compatible with the LINE channel. Additional testing may be necessary to ensure the fix works in all scenarios.

Recommendation

Apply the proposed workaround by adding the saveMediaBuffer call in downloadLineMedia(), as it has been verified to work locally and addresses the inconsistent behavior across channels.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix LINE: Add inbound media persistence to ~/.openclaw/media/inbound/ [2 pull requests, 2 comments, 3 participants]