whatsapp: one encrypted copy per device, treat an ack with error as a rejection

A message to the user's own number listed the phone twice (as recipient device and as
own device), so the server accepted it but nothing arrived. The device list is now
deduplicated; devices of the sender's user get the DeviceSentMessage wrapper, all
others the plain message (whatsmeow encryptMessageForDevices). The participant hash is
computed and compared with the server's (they match: 2:TKwG4B1n), and an ack carrying
error= is reported as a failed send instead of success.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 21:37:48 +01:00
co-authored by Claude Sonnet 5.5
parent da67d8dad3
commit bad5ea72ab
+47 -7
View File
@@ -620,24 +620,52 @@ std::optional<std::string> WaClient::send_message(const std::string& to_jid, con
log("send: recipient not on WhatsApp or no devices");
return std::nullopt;
}
// our other devices (the phone) get the DeviceSentMessage copy so they show the message as sent
auto own = own_other_devices();
// One deduplicated device list (whatsmeow encryptMessageForDevices): the recipient's devices plus our own other
// devices. Devices of OUR user get the DeviceSentMessage wrapper, every other device the plain message. A message to
// ourselves lists the phone once, as an own device; sending it twice made the server drop the message silently.
std::string own_user = device_.jid ? device_.jid->substr(0, device_.jid->find_first_of(":@")) : std::string();
auto user_of = [](const std::string& jid) { return jid.substr(0, jid.find_first_of(":@")); };
auto targets = devices;
targets.insert(targets.end(), own.begin(), own.end());
for (const auto& d : own_other_devices()) {
if (std::find_if(targets.begin(), targets.end(), [&](const std::string& t) { return addr_of(t) == addr_of(d); }) == targets.end()) targets.push_back(d);
}
if (!ensure_sessions(targets)) {
log("send: could not establish sessions");
return std::nullopt;
}
auto part_nodes = encrypt_for_devices(devices, WaCrypto::pkcs7_pad(message_plaintext), media_type);
if (!own.empty()) {
std::vector<std::string> plain_targets, own_targets;
for (const auto& d : targets) (user_of(d) == own_user ? own_targets : plain_targets).push_back(d);
auto part_nodes = encrypt_for_devices(plain_targets, WaCrypto::pkcs7_pad(message_plaintext), media_type);
if (!own_targets.empty()) {
WaMessage::DeviceSent ds;
ds.destination_jid = to_jid;
ds.inner = message_plaintext;
WaMessage wrapped;
wrapped.device_sent = ds;
auto own_nodes = encrypt_for_devices(own, WaCrypto::pkcs7_pad(wrapped.encode()), media_type);
auto own_nodes = encrypt_for_devices(own_targets, WaCrypto::pkcs7_pad(wrapped.encode()), media_type);
part_nodes.insert(part_nodes.end(), own_nodes.begin(), own_nodes.end());
}
// participant hash, whatsmeow participantListHashV2 over every device including this one; the server answers with
// its own in the ack, so a difference means our device list is stale
std::string our_phash;
{
std::vector<std::string> ad;
for (const auto& d : targets) {
size_t at = d.find('@'), colon = d.find(':');
std::string user = d.substr(0, std::min(at, colon));
std::string dev = colon != std::string::npos && colon < at ? d.substr(colon + 1, at - colon - 1) : "0";
ad.push_back(user + ".0:" + dev + "@" + (at == std::string::npos ? "s.whatsapp.net" : d.substr(at + 1)));
}
if (device_.jid) {
size_t at = device_.jid->find('@'), colon = device_.jid->find(':');
ad.push_back(device_.jid->substr(0, std::min(at, colon)) + ".0:" + (colon != std::string::npos ? device_.jid->substr(colon + 1, at - colon - 1) : "0") + "@" + device_.jid->substr(at + 1));
}
std::sort(ad.begin(), ad.end());
std::string joined;
for (const auto& a : ad) joined += a;
Bytes h = WaCrypto::sha256(Bytes(joined.begin(), joined.end()));
our_phash = "2:" + b64(Bytes(h.begin(), h.begin() + 6));
}
if (part_nodes.empty()) {
log("send: no recipients encrypted");
return std::nullopt;
@@ -672,7 +700,19 @@ std::optional<std::string> WaClient::send_message(const std::string& to_jid, con
if (!socket_open()) break;
auto node = recv_node(2);
if (!node) continue;
if (node->tag == "ack" && node->attr("id") == msg_id) return msg_id;
if (node->tag == "ack" && node->attr("id") == msg_id) {
// an ack can carry an error (error="479" ...): that is a rejection, not a delivery
std::string line = "send: server ack";
for (const auto& kv : node->attrs) line += " " + kv.first + "=" + kv.second;
log(line);
if (!our_phash.empty() && node->has_attr("phash") && node->attr("phash") != our_phash)
log("send: device list differs from the server's (ours " + our_phash + ", server " + node->attr("phash") + ")");
if (node->has_attr("error")) {
log("send: the server rejected the message (error " + node->attr("error") + ")");
return std::nullopt;
}
return msg_id;
}
if (node->tag == "message") dispatch(*node); // acks and handles key shares
else if (node->tag == "receipt") send_ack(*node);
else dispatch(*node);