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:
@@ -1,5 +1,6 @@
|
||||
#include "chat/chat_crypto.h"
|
||||
#include "chat/chat_identity.h"
|
||||
#include "chat/chat_service.h"
|
||||
#include "daemon/daemon_controller.h"
|
||||
#include "data/transaction_history_cache.h"
|
||||
#include "daemon/lifecycle_adapters.h"
|
||||
@@ -5662,6 +5663,74 @@ void testHushChatReceivePath()
|
||||
EXPECT_EQ((int)off.metadata.size(), 0);
|
||||
}
|
||||
|
||||
// ChatService: metadata batch -> decrypt -> threaded, deduped in-memory store.
|
||||
void testHushChatService()
|
||||
{
|
||||
using namespace dragonx::chat;
|
||||
|
||||
ChatKeyPair alice, bob;
|
||||
ChatIdentityResult ra = deriveChatIdentityFromSecret("alice-svc", alice, true);
|
||||
ChatIdentityResult rb = deriveChatIdentityFromSecret("bob-svc", bob, true);
|
||||
|
||||
// Two Alice->Bob messages in one conversation, as harvested metadata.
|
||||
std::vector<HushChatTransactionMetadata> batch;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
std::string e, ct;
|
||||
EXPECT_TRUE(encryptOutgoing(alice, rb.public_key_hex, i == 0 ? "first" : "second", e, ct) == ChatCryptoStatus::Ok);
|
||||
HushChatTransactionMetadata m;
|
||||
m.txid = std::string("tx") + std::to_string(i);
|
||||
m.type = HushChatHeaderType::Message;
|
||||
m.conversation_id = "conv-x";
|
||||
m.reply_zaddr = "zs-alice";
|
||||
m.sender_public_key_hex = ra.public_key_hex;
|
||||
m.secretstream_header_hex = e;
|
||||
m.payload_memo = ct;
|
||||
m.payload_position = 1;
|
||||
batch.push_back(m);
|
||||
}
|
||||
|
||||
ChatService svc;
|
||||
EXPECT_TRUE(!svc.hasIdentity());
|
||||
EXPECT_EQ(svc.ingest(batch), 0); // no identity -> nothing ingested
|
||||
|
||||
svc.setIdentity(bob);
|
||||
EXPECT_TRUE(svc.hasIdentity());
|
||||
EXPECT_EQ(svc.ingest(batch, 1000), 2);
|
||||
EXPECT_EQ((int)svc.store().size(), 2);
|
||||
EXPECT_EQ(svc.ingest(batch, 1000), 0); // re-scan dedups
|
||||
EXPECT_EQ((int)svc.store().size(), 2);
|
||||
|
||||
std::vector<ChatMessage> conv = svc.store().conversation("conv-x");
|
||||
EXPECT_EQ((int)conv.size(), 2);
|
||||
EXPECT_EQ(conv[0].body, std::string("first"));
|
||||
EXPECT_EQ(conv[1].body, std::string("second"));
|
||||
EXPECT_TRUE(conv[0].direction == ChatDirection::Incoming);
|
||||
EXPECT_EQ(conv[0].peer_public_key_hex, ra.public_key_hex);
|
||||
EXPECT_EQ(conv[0].timestamp, (std::int64_t)1000);
|
||||
|
||||
// A contact request carries plaintext through without decryption.
|
||||
std::vector<HushChatTransactionMetadata> creq(1);
|
||||
creq[0].txid = "txc";
|
||||
creq[0].type = HushChatHeaderType::ContactRequest;
|
||||
creq[0].conversation_id = "conv-y";
|
||||
creq[0].sender_public_key_hex = ra.public_key_hex;
|
||||
creq[0].payload_memo = "hi, add me";
|
||||
creq[0].payload_position = 1;
|
||||
EXPECT_EQ(svc.ingest(creq), 1);
|
||||
std::vector<ChatMessage> cy = svc.store().conversation("conv-y");
|
||||
EXPECT_EQ((int)cy.size(), 1);
|
||||
EXPECT_TRUE(cy[0].kind == ChatMessageKind::ContactRequest);
|
||||
EXPECT_EQ(cy[0].body, std::string("hi, add me"));
|
||||
|
||||
// Wrong identity can't decrypt -> messages dropped silently.
|
||||
ChatService svc2;
|
||||
ChatKeyPair mallory;
|
||||
deriveChatIdentityFromSecret("mallory-svc", mallory, true);
|
||||
svc2.setIdentity(mallory);
|
||||
EXPECT_EQ(svc2.ingest(batch, 1000), 0);
|
||||
EXPECT_TRUE(svc2.store().empty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -5754,6 +5823,7 @@ int main()
|
||||
testAtomicFileWrite();
|
||||
testHushChatCrypto();
|
||||
testHushChatReceivePath();
|
||||
testHushChatService();
|
||||
testAddressChecksumValidation();
|
||||
testLiteServerProbeLive();
|
||||
testXmrigLiveInstall();
|
||||
|
||||
Reference in New Issue
Block a user