feat(chat): message model, in-memory store, and receive service

Phase 1 Steps 4-6 (the receive pipeline, minus the App/sync wiring which
lands next).

- chat_message: the in-memory ChatMessage model (direction, kind, txid, cid,
  peer zaddr/pubkey, body, timestamp, payload_position). No libsodium.
- chat_store: threads messages by conversation_id and dedups by
  (txid, payload_position) so re-scanning the chain never double-inserts.
- chat_service: owns the long-lived chat identity keypair (move-disabled,
  wiped on destruction/clear) and the store. ingest() decrypts each Message
  (drops undecryptable ones silently — no plaintext/memo logging), passes a
  ContactRequest's plaintext through, and threads the result. No-op without an
  identity.

Tests: a metadata batch decrypts into a threaded conversation; re-ingest
dedups; a contact request carries through; a wrong identity decrypts nothing;
no-identity ingest is a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 20:11:05 -05:00
parent d043538e2f
commit ba03de938e
7 changed files with 282 additions and 0 deletions

26
src/chat/chat_message.h Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
// DragonX Wallet - HushChat in-memory message model (no libsodium, no persistence).
// Phase 1 keeps decrypted messages in memory only; sqlite persistence is Phase 2.
#include <cstdint>
#include <string>
namespace dragonx::chat {
enum class ChatDirection { Incoming, Outgoing };
enum class ChatMessageKind { Message, ContactRequest };
struct ChatMessage {
ChatDirection direction = ChatDirection::Incoming;
ChatMessageKind kind = ChatMessageKind::Message;
std::string txid;
std::string conversation_id; // cid — the conversation thread key
std::string peer_zaddr; // header "z": peer's reply z-address
std::string peer_public_key_hex; // header "p": peer's crypto_kx public key
std::string body; // decrypted plaintext (Message) or request text (ContactRequest)
std::int64_t timestamp = 0; // tx time in seconds; set by the ingesting caller
std::size_t payload_position = 0; // together with txid, the dedup key
};
} // namespace dragonx::chat