detz: baseline 1.46.1 pre-transpilation

This commit is contained in:
2026-10-28 01:58:28 +01:00
parent 69542ab933
commit 12ee647d78
32 changed files with 1824 additions and 68 deletions
+201
View File
@@ -15,6 +15,7 @@
#include "json.h"
#include "wa_binary.h"
#include "wa_client.h"
#include "wa_crypto.h"
#include "wa_device.h"
#include "wa_noise.h"
@@ -24,6 +25,7 @@
#include "wa_media.h"
#include "wa_receiver.h"
#include "wa_store.h"
#include "wa_vcf.h"
using namespace detz::wa;
using detz::json::Value;
@@ -666,6 +668,200 @@ static void test_contact_search() {
std::filesystem::remove_all(home);
}
// Key-request bookkeeping (whatsmeow PR 1268): a request that never left the device must not block the
// retry for 24 h, so the mark can be cleared again.
static void test_key_request_rollback() {
std::printf("[appstate: key request rollback]\n");
std::string home = make_temp_home();
WaStore store(home, "keyreq");
Bytes id{0x01, 0x02, 0x03};
check(!store.app_key_request_time(id).has_value(), "no request mark for a fresh key id");
store.put_app_key_request_time(id, 1700000000);
auto got = store.app_key_request_time(id);
check(got && *got == 1700000000, "request mark is stored");
store.clear_app_key_request_time(id);
check(!store.app_key_request_time(id).has_value(), "clearing the mark frees the key id for an immediate retry");
store.clear_app_key_request_time(id); // clearing twice is a no-op, not an error
check(!store.app_key_request_time(id).has_value(), "clearing an unmarked key id is harmless");
std::filesystem::remove_all(home);
}
// Peer message helpers: own-sender classification (PN or LID, whatsmeow IsFromMe), the peer node shape
// (device-identity only on a prekey message), server_sync parsing and retry-receipt detection.
static void test_peer_helpers() {
std::printf("[appstate: peer message helpers]\n");
check(WaClient::is_own_sender("31630045827@s.whatsapp.net", "31630045827", "173735822786802"),
"own PN sender is accepted");
check(WaClient::is_own_sender("173735822786802@lid", "31630045827", "173735822786802"),
"own LID sender is accepted (the phone answers from its LID)");
check(WaClient::is_own_sender("31630045827:0@s.whatsapp.net", "31630045827", ""),
"own sender with an explicit device 0 is accepted");
check(!WaClient::is_own_sender("31611111111@s.whatsapp.net", "31630045827", "173735822786802"),
"another user is not an own sender");
check(!WaClient::is_own_sender("173735822786802@lid", "31630045827", ""),
"an unknown LID is not accepted without a stored LID");
WaNode enc("enc");
enc.attrs["v"] = "2";
enc.attrs["type"] = "pkmsg";
enc.content = Bytes{1, 2, 3};
Bytes account{9, 9, 9};
WaNode pre = WaClient::build_peer_message_node("3EB0AA", "31630045827@s.whatsapp.net", enc, true, account);
check(pre.tag == "message" && pre.attr("category") == "peer" && pre.attr("push_priority") == "high" &&
pre.attr("to") == "31630045827@s.whatsapp.net" && pre.child("meta") != nullptr &&
pre.child("enc") != nullptr && pre.child("device-identity") != nullptr,
"peer node: meta + enc + device-identity on a prekey message");
WaNode msg = WaClient::build_peer_message_node("3EB0AB", "31630045827@s.whatsapp.net", enc, false, account);
check(msg.child("device-identity") == nullptr && msg.child("enc") != nullptr,
"peer node: no device-identity on an existing-session message");
WaNode notif("notification");
notif.attrs["type"] = "server_sync";
WaNode c1("collection");
c1.attrs["name"] = "critical_unblock_low";
WaNode c2("collection");
c2.attrs["name"] = "regular_high";
notif.content = WaNode::Children{c1, c2};
auto cols = WaClient::server_sync_collections(notif);
check(cols.size() == 2 && cols[0] == "critical_unblock_low" && cols[1] == "regular_high",
"server_sync notification yields its collection names");
WaNode retry("receipt");
retry.attrs["type"] = "retry";
WaNode read("receipt");
read.attrs["type"] = "read";
WaNode ack("ack");
check(WaClient::is_retry_receipt(retry) && !WaClient::is_retry_receipt(read) && !WaClient::is_retry_receipt(ack),
"only a retry receipt reads as one");
}
// Empty key shares (openclaw/wacli PR 443): the phone answers with keys that carry no key data. The share
// still parses, so the client can tell "answered empty" from "never answered".
static void test_empty_key_share() {
std::printf("[appstate: empty key share]\n");
ProtoWriter key;
{
ProtoWriter keyid;
keyid.bytes(1, Bytes{0x7c, 0x57});
key.message(1, keyid.data());
ProtoWriter data; // AppStateSyncKeyData without keyData: the phone has none
data.varint(3, 1767225600);
key.message(2, data.data());
}
ProtoWriter share;
share.message(1, key.data());
ProtoWriter pm;
pm.varint(2, 6); // APP_STATE_SYNC_KEY_SHARE
pm.message(7, share.data());
ProtoWriter m;
m.message(12, pm.data());
WaProtocolMessage out = parse_protocol_message(m.take());
check(out.type == 6 && out.key_share.size() == 1 && out.key_share[0].key_data.empty(),
"an empty key share parses with empty key data (answered, but nothing to store)");
}
// Push-name learning: every incoming message carries the sender's self-reported name in `notify`
// (whatsmeow message.go). A sender we have no name for is learned; saved, imported and aliased names win.
static void test_push_name_learning() {
std::printf("[contacts: push-name learning]\n");
const std::string me = "31630045827", my_lid = "173735822786802";
auto msg = [](const std::string& from, const std::string& notify) {
WaNode m("message");
m.attrs["from"] = from;
if (!notify.empty()) m.attrs["notify"] = notify;
return m;
};
auto cand = [&](const WaNode& m) { return WaClient::push_name_candidate(m, me, my_lid); };
{
auto c = cand(msg("31611111111@s.whatsapp.net", "Nigel Dokter"));
check(c && c->first == "31611111111@s.whatsapp.net" && c->second == "Nigel Dokter",
"1:1 message learns the sender's push name");
}
{
WaNode m = msg("179000000000001@lid", "Anne");
m.attrs["sender_pn"] = "31622222222@s.whatsapp.net";
auto c = cand(m);
check(c && c->first == "31622222222@s.whatsapp.net" && c->second == "Anne",
"a LID sender with a PN alternate is learned under the sendable PN");
}
{
auto c = cand(msg("179000000000001@lid", "Anne"));
check(c && c->first == "179000000000001@lid", "a LID sender without alternate is learned under its LID");
}
{
WaNode m = msg("120363000000000@g.us", "Joop");
m.attrs["participant"] = "31633333333@s.whatsapp.net";
auto c = cand(m);
check(c && c->first == "31633333333@s.whatsapp.net" && c->second == "Joop",
"a group message learns the participant, not the group");
}
{
WaNode m = msg("120363000000000@g.us", "Joop");
m.attrs["participant"] = "179000000000002@lid";
m.attrs["participant_pn"] = "31633333333@s.whatsapp.net";
auto c = cand(m);
check(c && c->first == "31633333333@s.whatsapp.net", "a group LID participant prefers its PN alternate");
}
check(!cand(msg("31630045827@s.whatsapp.net", "Me")), "our own messages teach nothing");
check(!cand(msg("31611111111@s.whatsapp.net", "-")), "a '-' push name is not learned");
check(!cand(msg("31611111111@s.whatsapp.net", "")), "a missing push name is not learned");
check(!cand(msg("1234567890@newsletter", "Channel")), "a newsletter sender is not learned");
{
auto c = cand(msg("31611111111@s.whatsapp.net", std::string(300, 'x')));
check(c && c->second.size() == 128, "an over-long push name is truncated, not refused");
}
std::string home = make_temp_home();
WaStore store(home, "push");
check(store.put_contact_if_absent("31611111111@s.whatsapp.net", "Nigel Dokter"),
"first sighting of a sender is stored");
check(!store.put_contact_if_absent("31611111111@s.whatsapp.net", "Nigel Changed"),
"a second sighting does not overwrite");
check(store.contacts().size() == 1 && store.contacts()[0].second == "Nigel Dokter",
"the first learned name sticks");
auto hits = store.search_contacts("Niggel", 3, 0.3);
check(!hits.empty() && hits[0].name == "Nigel Dokter", "a learned name is searchable with typos");
store.put_contact("31611111111@s.whatsapp.net", "Nigel Dokter (work)"); // app-state-grade name wins
check(store.contacts()[0].second == "Nigel Dokter (work)", "a saved name overwrites a learned push name");
check(!store.put_contact_if_absent("31611111111@s.whatsapp.net", "Nigel Again"),
"a push name never overwrites a saved name");
std::filesystem::remove_all(home);
}
// VCF fallback (problem.md): a .vcf exported on the phone feeds the same contact search the app-state sync feeds.
static void test_vcf_import() {
std::printf("[contacts: vcf import]\n");
const char* vcf =
"BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Nigel Dokter\r\nTEL;TYPE=CELL:+31 6 1234 5001\r\nEND:VCARD\r\n"
"BEGIN:VCARD\r\nVERSION:3.0\r\nN:Smit;Nina;;;\r\nTEL:+31612345002\r\nTEL:0031612345003\r\nEND:VCARD\r\n"
"BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Long \r\n Name\r\nTEL;TYPE=HOME:+31-6-1234-5004\r\nEND:VCARD\r\n"
"BEGIN:VCARD\r\nVERSION:3.0\r\nFN:No Number\r\nEND:VCARD\r\n"
"BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Local Only\r\nTEL:0612345\r\nEND:VCARD\r\n";
int skipped = 0;
auto contacts = WaVcf::parse(vcf, skipped);
check(contacts.size() == 4, "four usable numbers parse from five cards (one numberless, one local)");
check(skipped == 2, "two cards skipped: no number, and a national number without country code");
bool nigel = false, nina = false, unfolded = false;
for (const auto& c : contacts) {
if (c.name == "Nigel Dokter" && c.number == "31612345001") nigel = true;
if (c.name == "Nina Smit" && (c.number == "31612345002" || c.number == "31612345003")) nina = true;
if (c.name == "Long Name") unfolded = true;
}
check(nigel, "FN plus an international TEL with spaces");
check(nina, "N fallback name plus two TELs (00 form normalized)");
check(unfolded, "a folded FN line is unfolded");
std::string home = make_temp_home();
WaStore store(home, "vcf");
WaVcf::Report rep = WaVcf::import_text(store, vcf);
check(rep.imported == 4 && rep.skipped == 2 && rep.capped == 0, "import stores 4 numbers, skips 2, caps none");
auto hits = store.search_contacts("Niggel", 3, 0.3);
check(!hits.empty() && hits[0].name == "Nigel Dokter", "an imported contact is found by the typo-tolerant search");
WaVcf::Report empty = WaVcf::import_text(store, "not a vcard at all");
check(empty.imported == 0, "garbage imports nothing and throws nothing");
std::filesystem::remove_all(home);
}
int main(int argc, char** argv) {
DIR = argc > 1 ? argv[1] : "tests/whatsapp_data";
test_contact_search();
@@ -682,6 +878,11 @@ int main(int argc, char** argv) {
test_lthash_and_keys();
test_appstate_sync();
test_receiver();
test_key_request_rollback();
test_peer_helpers();
test_empty_key_share();
test_vcf_import();
test_push_name_learning();
std::printf("%s\n", g_fail == 0 ? "whatsapp: ALL OK" : "whatsapp: FAILURES");
return g_fail == 0 ? 0 : 1;
}