diff --git a/taicpp/src/whatsapp/wa_client.cpp b/taicpp/src/whatsapp/wa_client.cpp index 1edea76..5d94f4a 100644 --- a/taicpp/src/whatsapp/wa_client.cpp +++ b/taicpp/src/whatsapp/wa_client.cpp @@ -620,24 +620,52 @@ std::optional 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 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 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 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);