Skip to content
22 changes: 20 additions & 2 deletions src/pair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,32 @@ use prost::Message;

use std::sync::Arc;
use std::sync::atomic::Ordering;
use wacore::companion_reg::{CompanionWebClientType, companion_web_client_type_for_props};
use wacore::libsignal::protocol::KeyPair;
use wacore_binary::NodeRef;
use wacore_binary::{Jid, SERVER_JID};
use waproto::whatsapp as wa;

pub use wacore::pair::{DeviceState, PairCryptoError, PairUtils};

/// Derives `CompanionWebClientType` from `device_props.platform_type`. Use
/// [`make_qr_data_with_client_type`] to override.
pub fn make_qr_data(store: &crate::store::Device, ref_str: String) -> String {
let client_type = companion_web_client_type_for_props(&store.device_props);
make_qr_data_with_client_type(store, ref_str, client_type)
}

pub fn make_qr_data_with_client_type(
store: &crate::store::Device,
ref_str: String,
client_type: CompanionWebClientType,
Comment on lines +25 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-export companion enum for QR override API

make_qr_data_with_client_type is public but its parameter type (CompanionWebClientType) is not exposed by whatsapp-rust, so downstream crates that depend only on whatsapp-rust cannot call this new override path without adding a direct wacore dependency. That makes the advertised override seam effectively unusable for current consumers of the high-level crate.

Useful? React with 👍 / 👎.

) -> String {
let device_state = DeviceState {
identity_key: store.identity_key.clone(),
noise_key: store.noise_key.clone(),
adv_secret_key: store.adv_secret_key,
};
PairUtils::make_qr_data(&device_state, ref_str)
PairUtils::make_qr_data(&device_state, ref_str, client_type)
}

pub async fn handle_iq(client: &Arc<Client>, node: &NodeRef<'_>) -> bool {
Expand Down Expand Up @@ -49,12 +61,18 @@ pub async fn handle_iq(client: &Arc<Client>, node: &NodeRef<'_>) -> bool {
noise_key: device_snapshot.noise_key.clone(),
adv_secret_key: device_snapshot.adv_secret_key,
};
let client_type =
companion_web_client_type_for_props(&device_snapshot.device_props);

for grandchild in child.get_children_by_tag("ref") {
if let Some(bytes) = grandchild.content_bytes()
&& let Ok(r) = std::str::from_utf8(bytes)
{
codes.push(PairUtils::make_qr_data(&device_state, r.to_string()));
codes.push(PairUtils::make_qr_data(
&device_state,
r.to_string(),
client_type,
));
}
}

Expand Down
192 changes: 192 additions & 0 deletions wacore/src/companion_reg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
//! Companion registration client type carried by the pairing QR string.
//! Mirrors `WAWebCompanionRegClientUtils.DEVICE_PLATFORM`
//! (`docs/captured-js/WAWeb/Link/DeviceQrcode.react.js`, `Companion/RegClientUtils.js`).

use std::fmt;

use waproto::whatsapp as wa;

/// Web-client type for the pairing QR. Discriminants are the wire integers,
/// so [`Self::code`] is a zero-cost cast.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum CompanionWebClientType {
Comment on lines +10 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive this protocol enum with WireEnum

CompanionWebClientType is a wire-facing protocol enum, but it is implemented with #[repr(i32)] plus manual Display instead of #[derive(WireEnum)], which violates the repo convention in /workspace/whatsapp-rust/AGENTS.md (“Every protocol enum uses #[derive(WireEnum)] and #[wire = ...] is the single source of truth”). Keeping this as a manual special case makes future wire-tag updates easy to desynchronize from the rest of the protocol layer and can silently produce wrong QR wire values when enum definitions evolve.

Useful? React with 👍 / 👎.

#[default]
Unknown = 0,
Chrome = 1,
Edge = 2,
Firefox = 3,
Ie = 4,
Opera = 5,
Safari = 6,
Electron = 7,
Uwp = 8,
OtherWebClient = 9,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

impl CompanionWebClientType {
pub const fn code(self) -> i32 {
self as i32
}
}

impl fmt::Display for CompanionWebClientType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.code().fmt(f)
}
}

/// Maps `DeviceProps.PlatformType` to the QR pairing enum. Non-web platforms
/// fall through to `OtherWebClient`, matching WA Web's fall-through for
/// unrecognised `WAWebMiscBrowserUtils.info().name`.
pub const fn companion_web_client_type_for_platform(
pt: wa::device_props::PlatformType,
) -> CompanionWebClientType {
use CompanionWebClientType as C;
use wa::device_props::PlatformType as P;
match pt {
P::Unknown => C::Unknown,
P::Chrome => C::Chrome,
P::Firefox => C::Firefox,
P::Ie => C::Ie,
P::Opera => C::Opera,
P::Safari => C::Safari,
P::Edge => C::Edge,
P::Desktop => C::Electron,
P::Uwp => C::Uwp,
P::Ipad
| P::AndroidTablet
| P::Ohana
| P::Aloha
| P::Catalina
| P::TclTv
| P::IosPhone
| P::IosCatalyst
| P::AndroidPhone
| P::AndroidAmbiguous
| P::WearOs
| P::ArWrist
| P::ArDevice
| P::Vr
| P::CloudApi
| P::Smartglasses => C::OtherWebClient,
}
}

/// Missing or out-of-range `platform_type` decays to `Unknown` so the QR flow
/// never fails on DeviceProps shape.
pub fn companion_web_client_type_for_props(props: &wa::DeviceProps) -> CompanionWebClientType {
props
.platform_type
.and_then(|v| wa::device_props::PlatformType::try_from(v).ok())
.map(companion_web_client_type_for_platform)
.unwrap_or_default()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn wire_codes_match_wa_web() {
assert_eq!(CompanionWebClientType::Unknown.code(), 0);
assert_eq!(CompanionWebClientType::Chrome.code(), 1);
assert_eq!(CompanionWebClientType::Edge.code(), 2);
assert_eq!(CompanionWebClientType::Firefox.code(), 3);
assert_eq!(CompanionWebClientType::Ie.code(), 4);
assert_eq!(CompanionWebClientType::Opera.code(), 5);
assert_eq!(CompanionWebClientType::Safari.code(), 6);
assert_eq!(CompanionWebClientType::Electron.code(), 7);
assert_eq!(CompanionWebClientType::Uwp.code(), 8);
assert_eq!(CompanionWebClientType::OtherWebClient.code(), 9);
}

#[test]
fn browser_platform_types_round_trip() {
use CompanionWebClientType as C;
use wa::device_props::PlatformType as P;
assert_eq!(companion_web_client_type_for_platform(P::Chrome), C::Chrome);
assert_eq!(
companion_web_client_type_for_platform(P::Firefox),
C::Firefox
);
assert_eq!(companion_web_client_type_for_platform(P::Edge), C::Edge);
assert_eq!(companion_web_client_type_for_platform(P::Safari), C::Safari);
assert_eq!(companion_web_client_type_for_platform(P::Opera), C::Opera);
assert_eq!(companion_web_client_type_for_platform(P::Ie), C::Ie);
}

#[test]
fn desktop_maps_to_electron_and_uwp_preserved() {
use CompanionWebClientType as C;
use wa::device_props::PlatformType as P;
assert_eq!(
companion_web_client_type_for_platform(P::Desktop),
C::Electron
);
assert_eq!(companion_web_client_type_for_platform(P::Uwp), C::Uwp);
}

#[test]
fn mobile_and_xr_collapse_to_other() {
use CompanionWebClientType as C;
use wa::device_props::PlatformType as P;
for pt in [
P::Ipad,
P::AndroidPhone,
P::AndroidTablet,
P::IosPhone,
P::IosCatalyst,
P::AndroidAmbiguous,
P::WearOs,
P::ArWrist,
P::ArDevice,
P::Vr,
P::Ohana,
P::Aloha,
P::Catalina,
P::TclTv,
P::CloudApi,
P::Smartglasses,
] {
assert_eq!(
companion_web_client_type_for_platform(pt),
C::OtherWebClient,
"{pt:?} must fall back to OtherWebClient",
);
}
}

#[test]
fn for_props_reads_platform_type() {
let props = wa::DeviceProps {
platform_type: Some(wa::device_props::PlatformType::Chrome as i32),
..Default::default()
};
assert_eq!(
companion_web_client_type_for_props(&props),
CompanionWebClientType::Chrome,
);
}

#[test]
fn for_props_missing_platform_type_is_unknown() {
let props = wa::DeviceProps::default();
assert_eq!(
companion_web_client_type_for_props(&props),
CompanionWebClientType::Unknown,
);
}

#[test]
fn for_props_invalid_platform_type_is_unknown() {
let props = wa::DeviceProps {
platform_type: Some(9999),
..Default::default()
};
assert_eq!(
companion_web_client_type_for_props(&props),
CompanionWebClientType::Unknown,
);
}
}
1 change: 1 addition & 0 deletions wacore/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub use wacore_derive::{EmptyNode, ProtocolNode, WireEnum};
pub mod adv;
pub mod appstate_sync;
pub mod client;
pub mod companion_reg;
pub mod download;
pub mod iq;
pub mod protocol;
Expand Down
Loading
Loading