use waproto::whatsapp as wa;
pub const NATIVE_CAMERA_DEEP_LINK_PREFIX: &str = "https://wa.me/settings/linked_devices#";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum CompanionWebClientType {
Chrome,
Edge,
Firefox,
Ie,
Opera,
Safari,
Electron,
Uwp,
#[default]
OtherWebClient,
AndroidTablet,
AndroidPhone,
AndroidAmbiguous,
}
impl CompanionWebClientType {
pub const fn wire_byte(self) -> u8 {
match self {
Self::Chrome => b'1',
Self::Edge => b'2',
Self::Firefox => b'3',
Self::Ie => b'4',
Self::Opera => b'5',
Self::Safari => b'6',
Self::Electron => b'7',
Self::Uwp => b'8',
Self::OtherWebClient => b'9',
Self::AndroidTablet => b'd',
Self::AndroidPhone => b'e',
Self::AndroidAmbiguous => b'f',
}
}
}
impl std::fmt::Display for CompanionWebClientType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.wire_byte() as char)
}
}
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::Electron
| CompanionWebClientType::Uwp
| CompanionWebClientType::OtherWebClient
| CompanionWebClientType::AndroidTablet
| CompanionWebClientType::AndroidPhone
| CompanionWebClientType::AndroidAmbiguous => "Chrome",
}
}
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::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::ANDROID_PHONE | P::ANDROID_TABLET | P::ANDROID_AMBIGUOUS => C::Chrome,
P::UNKNOWN
| P::IPAD
| P::OHANA
| P::ALOHA
| P::CATALINA
| P::TCL_TV
| P::IOS_PHONE
| P::IOS_CATALYST
| P::WEAR_OS
| P::AR_WRIST
| P::AR_DEVICE
| P::VR
| P::CLOUD_API
| P::SMARTGLASSES => C::OtherWebClient,
}
}
pub fn companion_web_client_type_for_props(props: &wa::DeviceProps) -> CompanionWebClientType {
props
.platform_type
.map(companion_web_client_type_for_platform)
.unwrap_or(CompanionWebClientType::OtherWebClient)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompanionOs {
Windows,
MacOs,
Linux,
Android,
Ios,
}
impl CompanionOs {
pub const fn wire_str(self) -> &'static str {
match self {
Self::Windows => "Windows",
Self::MacOs => "Mac OS",
Self::Linux => "Linux",
Self::Android => "Android",
Self::Ios => "iOS",
}
}
pub fn classify(os: &str) -> Option<Self> {
let os = os.trim().to_ascii_lowercase();
if os.is_empty() {
None
} else if os.contains("windows") {
Some(Self::Windows)
} else if os.contains("mac") || os.contains("osx") || os.contains("darwin") {
Some(Self::MacOs)
} else if os.contains("ipad")
|| os.contains("iphone")
|| os.split(|c: char| !c.is_ascii_alphanumeric())
.any(|tok| tok == "ios")
{
Some(Self::Ios)
} else if os.contains("android") {
Some(Self::Android)
} else if os.contains("linux")
|| os.contains("ubuntu")
|| os.contains("debian")
|| os.contains("fedora")
|| os.contains("chrome os")
|| os.contains("chromeos")
|| os.contains("chromium")
|| os.split(|c: char| !c.is_ascii_alphanumeric())
.any(|tok| tok == "arch" || tok == "cros")
{
Some(Self::Linux)
} else {
None
}
}
pub fn from_hint(os: &str) -> Self {
Self::classify(os).unwrap_or(Self::Linux)
}
}
pub fn companion_platform_display_raw(ct: CompanionWebClientType, os: &str) -> String {
use CompanionWebClientType as C;
match ct {
C::AndroidPhone | C::AndroidTablet | C::AndroidAmbiguous => {
format!("Android ({os})")
}
_ => format!("{} ({})", companion_browser_name(ct), os),
}
}
pub fn companion_platform_display(ct: CompanionWebClientType, os: &str) -> String {
companion_platform_display_raw(ct, CompanionOs::from_hint(os).wire_str())
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
#[test]
fn wire_byte_matches_wa_web() {
assert_eq!(CompanionWebClientType::Chrome.wire_byte(), b'1');
assert_eq!(CompanionWebClientType::Edge.wire_byte(), b'2');
assert_eq!(CompanionWebClientType::Firefox.wire_byte(), b'3');
assert_eq!(CompanionWebClientType::Ie.wire_byte(), b'4');
assert_eq!(CompanionWebClientType::Opera.wire_byte(), b'5');
assert_eq!(CompanionWebClientType::Safari.wire_byte(), b'6');
assert_eq!(CompanionWebClientType::Electron.wire_byte(), b'7');
assert_eq!(CompanionWebClientType::Uwp.wire_byte(), b'8');
assert_eq!(CompanionWebClientType::OtherWebClient.wire_byte(), b'9');
}
#[test]
fn wire_byte_matches_apk_for_mobile() {
assert_eq!(CompanionWebClientType::AndroidTablet.wire_byte(), b'd');
assert_eq!(CompanionWebClientType::AndroidPhone.wire_byte(), b'e');
assert_eq!(CompanionWebClientType::AndroidAmbiguous.wire_byte(), b'f');
}
#[test]
fn display_renders_wire_byte_as_char() {
assert_eq!(format!("{}", CompanionWebClientType::Chrome), "1");
assert_eq!(format!("{}", CompanionWebClientType::OtherWebClient), "9");
assert_eq!(format!("{}", CompanionWebClientType::AndroidPhone), "e");
assert_eq!(format!("{}", CompanionWebClientType::AndroidTablet), "d");
assert_eq!(format!("{}", CompanionWebClientType::AndroidAmbiguous), "f");
}
#[test]
fn default_is_other_web_client_nine() {
assert_eq!(
CompanionWebClientType::default(),
CompanionWebClientType::OtherWebClient,
);
assert_eq!(CompanionWebClientType::default().wire_byte(), b'9');
}
#[test]
fn browser_and_desktop_platform_types_map_to_their_variants() {
use CompanionWebClientType as C;
use wa::device_props::PlatformType as P;
for (pt, expected) in [
(P::CHROME, C::Chrome),
(P::FIREFOX, C::Firefox),
(P::EDGE, C::Edge),
(P::SAFARI, C::Safari),
(P::OPERA, C::Opera),
(P::IE, C::Ie),
(P::DESKTOP, C::Electron),
(P::UWP, C::Uwp),
] {
assert_eq!(
companion_web_client_type_for_platform(pt),
expected,
"{pt:?}"
);
}
}
#[test]
fn android_platform_types_map_to_chrome() {
use CompanionWebClientType as C;
use wa::device_props::PlatformType as P;
for pt in [P::ANDROID_PHONE, P::ANDROID_TABLET, P::ANDROID_AMBIGUOUS] {
assert_eq!(
companion_web_client_type_for_platform(pt),
C::Chrome,
"{pt:?}"
);
}
}
#[test]
fn unconfirmed_platform_types_collapse_to_other() {
use CompanionWebClientType as C;
use wa::device_props::PlatformType as P;
for pt in [
P::IPAD,
P::IOS_PHONE,
P::IOS_CATALYST,
P::WEAR_OS,
P::AR_WRIST,
P::AR_DEVICE,
P::VR,
P::OHANA,
P::ALOHA,
P::CATALINA,
P::TCL_TV,
P::CLOUD_API,
P::SMARTGLASSES,
] {
assert_eq!(
companion_web_client_type_for_platform(pt),
C::OtherWebClient,
"{pt:?}",
);
}
}
#[test]
fn proto_unknown_collapses_to_other_web_client() {
use CompanionWebClientType as C;
use wa::device_props::PlatformType as P;
assert_eq!(
companion_web_client_type_for_platform(P::UNKNOWN),
C::OtherWebClient,
);
}
#[test]
fn android_variants_still_emit_their_wire_bytes_when_used_directly() {
assert_eq!(CompanionWebClientType::AndroidPhone.wire_byte(), b'e');
assert_eq!(CompanionWebClientType::AndroidTablet.wire_byte(), b'd');
assert_eq!(CompanionWebClientType::AndroidAmbiguous.wire_byte(), b'f');
}
#[test]
fn for_props_reads_platform_type() {
let props = wa::DeviceProps {
platform_type: Some(wa::device_props::PlatformType::CHROME),
..Default::default()
};
assert_eq!(
companion_web_client_type_for_props(&props),
CompanionWebClientType::Chrome,
);
}
#[test]
fn for_props_missing_platform_type_is_other_web_client() {
let props = wa::DeviceProps::default();
assert_eq!(
companion_web_client_type_for_props(&props),
CompanionWebClientType::OtherWebClient,
);
}
#[test]
fn for_props_invalid_platform_type_is_other_web_client() {
use buffa::Message as _;
let props = wa::DeviceProps::decode_from_slice(&[0x18, 0x8f, 0x4e]).unwrap();
assert_eq!(
companion_web_client_type_for_props(&props),
CompanionWebClientType::OtherWebClient,
);
}
#[test]
fn browser_name_for_six_valid_browsers() {
use CompanionWebClientType as C;
for (ct, name) in [
(C::Chrome, "Chrome"),
(C::Edge, "Edge"),
(C::Firefox, "Firefox"),
(C::Ie, "IE"),
(C::Opera, "Opera"),
(C::Safari, "Safari"),
] {
assert_eq!(companion_browser_name(ct), name, "{ct:?}");
}
}
#[test]
fn browser_name_for_non_browser_falls_back_to_chrome() {
for ct in [
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 OS)"
);
}
#[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 OS)"
);
}
#[test]
fn companion_os_wire_str_matches_ua_parser_names() {
assert_eq!(CompanionOs::Windows.wire_str(), "Windows");
assert_eq!(CompanionOs::MacOs.wire_str(), "Mac OS");
assert_eq!(CompanionOs::Linux.wire_str(), "Linux");
assert_eq!(CompanionOs::Android.wire_str(), "Android");
assert_eq!(CompanionOs::Ios.wire_str(), "iOS");
}
#[test]
fn companion_os_classify_known_aliases() {
use CompanionOs as O;
for (hint, want) in [
("Windows", O::Windows),
("windows 11", O::Windows),
("Mac", O::MacOs),
("macOS", O::MacOs),
("Mac OS", O::MacOs),
("Mac OS X", O::MacOs),
("darwin", O::MacOs),
("Linux", O::Linux),
("Ubuntu", O::Linux),
("Fedora", O::Linux),
("Arch Linux", O::Linux),
("Arch", O::Linux),
("archlinux", O::Linux),
("CrOS", O::Linux),
("Chrome OS", O::Linux),
("ChromeOS", O::Linux),
("Chromium OS", O::Linux),
("Android", O::Android),
("android 14", O::Android),
("iOS", O::Ios),
("iOS 17", O::Ios),
("iPhone", O::Ios),
("iPad", O::Ios),
("iPadOS", O::Ios),
] {
assert_eq!(CompanionOs::classify(hint), Some(want), "{hint:?}");
}
}
#[test]
fn companion_os_branding_and_empty_are_unclassified_and_default_linux() {
for hint in [
"Veloz",
"Foobar123",
"KaiOS",
"March",
"Search",
"across",
"",
" ",
] {
assert_eq!(CompanionOs::classify(hint), None, "{hint:?}");
assert_eq!(CompanionOs::from_hint(hint), CompanionOs::Linux, "{hint:?}");
}
}
#[test]
fn platform_display_coerces_branding_os_to_linux() {
assert_eq!(
companion_platform_display(CompanionWebClientType::Chrome, "Veloz"),
"Chrome (Linux)"
);
}
#[test]
fn platform_display_canonicalizes_mac_aliases() {
for os in ["Mac", "macOS", "Mac OS", "Mac OS X", "darwin"] {
assert_eq!(
companion_platform_display(CompanionWebClientType::Chrome, os),
"Chrome (Mac OS)",
"{os:?}"
);
}
}
}