Skip to content

Read inbound media, and fetch its bytes - #18

Merged
TroyHernandez merged 2 commits into
mainfrom
media-attachments
Aug 20, 2026
Merged

TroyHernandez merged 2 commits into
mainfrom
media-attachments

Conversation

@TroyHernandez

Copy link
Copy Markdown
Contributor

The Matrix adapter's only event source was mx_extract_text_events(), which filters to text msgtypes. An image sent into a room was not merely unreadable to a consumer, it was invisible: nothing in the poll's output said a picture had ever been sent.

What changed

chat_poll() runs mx_extract_media_events() over the same sync and maps each media event onto its own chat_message carrying one chat_attachment — the shape chat_send() already produces outbound, where each file goes as a separate event. Media rides the existing position ordering, so a picture sent before a comment does not arrive after it. A throwing extractor costs the poll its media rather than the room's conversation.

chat_download(client, attachment, dest = NULL) is new. An inbound attachment names where its content lives, not what it is: the mxc URL needs the client's credentials, so a consumer cannot fetch it on its own. Capability-gated on chat_capabilities()$attachments, throwing by default rather than quietly leaving the caller pointing at an empty path. The destination keeps the attachment's extension, so a consumer handing the file to something that sniffs by extension does not have to rename it.

The loopback adapter implements chat_download() as a copy of the path it recorded, so a consumer writes one code path and tests it against the reference.

Encrypted media

Refused rather than fetched. The bytes behind the URL are ciphertext and this adapter has no media decryption path, so downloading would write a file that is not the picture with nothing to say so — the same bargain chat_send() makes refusing to upload into an encrypted room. The capability stays TRUE on an e2ee client, because the events are still reported; it is the fetch that cannot happen.

Floor

mx.client (>= 0.2.0.6), where mx_extract_media_events() arrived. Probed rather than assumed, so an older build degrades to no media instead of failing every poll, and CI's existing floor check makes the real-extractor tests actually run.

One note on the tests

The first version of matrix_media_records() passed an undefined self_id, and all thirteen seamed assertions passed: R hands the argument to the seam as a promise, and a seam double that ignores it never forces it. The real-extractor test in test_matrix_mxclient.R is what went red. The argument is forced up front now, and the seamed test reads it.

1072 tests pass locally with mx.api/mx.client/mx.crypto installed.

The Matrix adapter's only event source was mx_extract_text_events(),
which filters to text msgtypes. An image sent into a room was not
merely unreadable to a consumer, it was invisible: nothing in the
poll's output said a picture had ever been sent, so an agent asked
about one answered that there was no image attached.

chat_poll() now runs mx_extract_media_events() over the same sync and
maps each media event onto its own chat_message carrying one
chat_attachment -- the shape chat_send() already produces outbound,
where each file goes as a separate event. Media rides the existing
position ordering, so a picture sent before a comment does not arrive
after it, and a throwing extractor costs the poll its media rather
than the room's conversation.

An inbound attachment names where its content lives, not what it is:
the mxc URL needs this client's credentials, so a consumer cannot
fetch it on its own. chat_download() is the verb that turns one into a
file on disk, capability-gated on chat_capabilities()$attachments and
throwing by default rather than quietly leaving the caller pointing at
an empty path. The loopback adapter implements it as a copy, so a
consumer writes one code path and tests it against the reference.

Encrypted attachments are refused rather than fetched. The bytes
behind the URL are ciphertext and this adapter has no media decryption
path, so downloading would write a file that is not the picture, with
nothing to say so -- the same bargain chat_send() makes refusing to
upload into an encrypted room. The capability stays TRUE on an e2ee
client, because the events are still reported; it is the fetch that
cannot happen.

The mx.client floor moves to 0.2.0.6, where mx_extract_media_events()
arrived. It is probed rather than assumed, so an older build degrades
to no media instead of failing every poll, and CI enforces the floor
so the tests that meet the real extractor actually run.

That last part matters more than it looks. The first version of
matrix_media_records() passed an undefined self_id, and all thirteen
seamed assertions passed: R hands the argument to the seam as a
promise, and a seam double that ignores it never forces it. The
real-extractor test in test_matrix_mxclient.R is what went red. The
argument is forced up front now, and the seamed test reads it.
@TroyHernandez
TroyHernandez merged commit e77dc65 into main Aug 20, 2026
2 checks passed
@TroyHernandez
TroyHernandez deleted the media-attachments branch August 20, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant