Brings the X Chat adapter to parity with mature gateway platforms:
- Encrypted media, both directions. Inbound attachments are downloaded
(GET /2/chat/media/{conv}/{hash}), decrypted with the conversation key
for the EVENT's key version (post-rotation media stays readable),
size-capped, cached locally, and surfaced on MessageEvent
(media_urls/media_types + correct MessageType) so vision/file tools
see them. Outbound send_image/send_image_file/send_voice/send_video/
send_document encrypt with the latest conversation key
(encrypt_stream), upload via the 3-step chat-media flow
(initialize/append/finalize, base64 JSON segments), and attach by
media_hash_key. Standalone sends (cron/send_message_tool) carry
media_files the same way.
- Native threaded replies. A bounded per-conversation cache of decrypted
events lets send(reply_to=...) use encrypt_reply against the real
target event; unknown targets fall back to a plain send. Inbound
reply context (reply_to_message_id/text/author,
reply_to_is_own_message) now propagates on MessageEvent.
- New-conversation initiation. Standalone send to a bare numeric user id
performs the conversation-key handshake: fetch both parties' public
keys, verify each identity↔signing binding (verify_key_binding — a
substituted key must never receive the conversation key), wrap a
fresh key per participant (prepare_conversation_key_change), POST to
add-conversation-keys, then encrypt under the returned raw key.
- meta.conversation_key_events. The events endpoint returns KeyChange
events SEPARATELY in meta — previously they were never decrypted, so
conversations whose key changes fell outside the data array could
never seed a key. Both the poll loop and the standalone sender now
feed them through the batch decrypt path (after signing-key
registration) before processing messages.
- Read receipts (opt-in, XCHAT_SEND_READ_RECEIPTS, default off) via
POST /2/chat/conversations/{id}/read.
- Latest-key-version tracking per conversation for media encrypt and
correct key selection after rotations.
api.py: media_upload (chunked 3-step), media_download, mark_read.
crypto.py: encrypt_reply, encrypt_media/decrypt_media, verify_key_binding,
prepare_conversation_key_change (SDK->API body mapping incl.
action_signatures), attachments/explicit-key support on encrypt_text,
latest_key_version surfaced from decrypt_events, message_attachments,
detect_mime_type/detect_image_dimensions helpers.
Docs: media/replies/handshake/read-receipts documented; stale "text
only" / "reply flows only" limitations removed; capability row added to
the messaging comparison table; media.write scope noted in setup + docs.
Tests: 51 total — inbound attachment decrypt-and-cache, outbound
encrypt-upload-attach (+ no-key failure), threaded-reply cache hit and
fallback, meta key-event absorption order, read-receipt opt-in/default,
reply-context propagation, full handshake happy path (bindings verified,
key change POSTed, explicit key used), chunked upload reassembly,
media download, mark-read body.