detz: baseline 1.46.1 pre-transpilation
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user