Skip to content
20 changes: 17 additions & 3 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};

pub fn make_qr_data(store: &crate::store::Device, ref_str: String) -> String {
/// Auto-derives client type from `device_props`; see
/// [`make_qr_data_with_client_type`] to override.
pub fn make_qr_data(store: &crate::store::Device, ref_str: &str) -> 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: &str,
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,14 @@ 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, client_type));
}
}

Expand Down
11 changes: 3 additions & 8 deletions src/pair_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,22 +160,17 @@ impl Client {
})
.await;

// Resolve companion_platform_{id,display} from options + device_props.
// This is the single point where the pairing code flow picks what
// identity to announce; bare `PairCodeOptions::default()` derives from
// `Device.device_props` (os + platform_type) rather than the legacy
// "Chrome (Linux)" hardcode.
let (platform_id_str, platform_display_str) =
let (platform_id, platform_display) =
resolve_companion_platform(&options, &device_snapshot.device_props);
let platform_id_str = platform_id.to_string();

// Build the stage 1 IQ node
let req_id = self.generate_request_id();
let iq_content = PairCodeUtils::build_companion_hello_iq(
&phone_number,
&noise_static_pub,
&wrapped_ephemeral,
&platform_id_str,
&platform_display_str,
&platform_display,
options.show_push_notification,
req_id.clone(),
);
Expand Down
299 changes: 299 additions & 0 deletions wacore/src/companion_reg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
//! Mirrors `WAWebCompanionRegClientUtils.DEVICE_PLATFORM`
//! (`docs/captured-js/WAWeb/Companion/RegClientUtils.js`).

use waproto::whatsapp as wa;

/// Encode-only: discriminants pinned to wire ints, no decode fallback.
#[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 std::fmt::Display for CompanionWebClientType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.code().fmt(f)
}
}

/// One of the 6 browsers the server accepts in `companion_platform_display`
/// (per whatsmeow `PairPhone` doc). Non-browser variants fall back to "Chrome"
/// since WA Web emits the same when the underlying renderer is Chromium.
pub const fn companion_browser_name(ct: CompanionWebClientType) -> &'static str {
match ct {
CompanionWebClientType::Chrome => "Chrome",
CompanionWebClientType::Edge => "Edge",
CompanionWebClientType::Firefox => "Firefox",
CompanionWebClientType::Ie => "IE",
CompanionWebClientType::Opera => "Opera",
CompanionWebClientType::Safari => "Safari",
CompanionWebClientType::Unknown
| CompanionWebClientType::Electron
| CompanionWebClientType::Uwp
| CompanionWebClientType::OtherWebClient => "Chrome",
}
}

/// Non-web platforms collapse to `OtherWebClient`, matching WA Web's
/// `info().name` fall-through. WA Web's `gkx("4112") ⇒ UWP` host-shell
/// short-circuit isn't replicated — caller sets `PlatformType::Uwp`
/// explicitly. Same for `Electron` via `Desktop`.
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,
}
}

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()
}

/// `<browser> (<os>)` as WA Web emits. Empty OS falls back to "Linux"
/// since WA Web never sends a bare browser.
pub fn companion_platform_display(ct: CompanionWebClientType, os: &str) -> String {
let os = os.trim();
let os = if os.is_empty() { "Linux" } else { os };
format!("{} ({})", companion_browser_name(ct), os)
}

#[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 display_renders_decimal_wire_integer() {
assert_eq!(format!("{}", CompanionWebClientType::Unknown), "0");
assert_eq!(format!("{}", CompanionWebClientType::Chrome), "1");
assert_eq!(format!("{}", CompanionWebClientType::OtherWebClient), "9");
}

#[test]
fn default_is_unknown_zero() {
assert_eq!(
CompanionWebClientType::default(),
CompanionWebClientType::Unknown,
);
assert_eq!(CompanionWebClientType::default().code(), 0);
}

#[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,
);
}

#[test]
fn browser_name_for_six_valid_browsers() {
assert_eq!(
companion_browser_name(CompanionWebClientType::Chrome),
"Chrome"
);
assert_eq!(companion_browser_name(CompanionWebClientType::Edge), "Edge");
assert_eq!(
companion_browser_name(CompanionWebClientType::Firefox),
"Firefox"
);
assert_eq!(companion_browser_name(CompanionWebClientType::Ie), "IE");
assert_eq!(
companion_browser_name(CompanionWebClientType::Opera),
"Opera"
);
assert_eq!(
companion_browser_name(CompanionWebClientType::Safari),
"Safari"
);
}

#[test]
fn browser_name_for_non_browser_falls_back_to_chrome() {
for ct in [
CompanionWebClientType::Unknown,
CompanionWebClientType::Electron,
CompanionWebClientType::Uwp,
CompanionWebClientType::OtherWebClient,
] {
assert_eq!(companion_browser_name(ct), "Chrome", "{ct:?}");
}
}

#[test]
fn platform_display_always_browser_paren_os() {
assert_eq!(
companion_platform_display(CompanionWebClientType::Chrome, "Linux"),
"Chrome (Linux)"
);
assert_eq!(
companion_platform_display(CompanionWebClientType::Firefox, "Mac"),
"Firefox (Mac)"
);
}

#[test]
fn platform_display_empty_os_defaults_to_linux() {
assert_eq!(
companion_platform_display(CompanionWebClientType::Chrome, ""),
"Chrome (Linux)"
);
assert_eq!(
companion_platform_display(CompanionWebClientType::Chrome, " "),
"Chrome (Linux)"
);
}

#[test]
fn platform_display_non_browser_uses_chrome() {
assert_eq!(
companion_platform_display(CompanionWebClientType::OtherWebClient, "Android"),
"Chrome (Android)"
);
assert_eq!(
companion_platform_display(CompanionWebClientType::Electron, "Mac"),
"Chrome (Mac)"
);
}
}
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