refactor(browser-utils): improve robustness and type safety
- Add pre-computed BROWSER_TO_PLATFORM_ID map for better performance - Implement getPlatformName() helper with try-catch for error handling - Add normalizeBrowserKey() for input validation (handles non-string inputs) - Add safeRelease() wrapper with fallback for OS release detection - Export isValidBrowserPreset() type guard for external validation - Add comprehensive JSDoc documentation with examples - Add 25 unit tests covering all edge cases and robustness scenarios Improvements over original code: - Handles undefined/null/invalid input types gracefully - Returns default values instead of crashing on invalid input - Platform map is alphabetically sorted and deduplicated - Constants use 'as const' for better type inference - Module-level caching avoids repeated proto.DeviceProps access Inspired by PR #2303 from WhiskeySockets/Baileys https://claude.ai/code/session_018XbFWEYwCfeeXioFDLvSXV
This commit is contained in:
+161
-16
@@ -2,30 +2,175 @@ import { platform, release } from 'os'
|
||||
import { proto } from '../../WAProto/index.js'
|
||||
import type { BrowsersMap } from '../Types'
|
||||
|
||||
const PLATFORM_MAP = {
|
||||
// ============================================================
|
||||
// Constants
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Default platform name when OS detection fails or returns unsupported value
|
||||
*/
|
||||
const DEFAULT_PLATFORM_NAME = 'Ubuntu' as const
|
||||
|
||||
/**
|
||||
* Default platform ID (Chrome = 1) for unknown browser mappings
|
||||
* @see proto.DeviceProps.PlatformType
|
||||
*/
|
||||
const DEFAULT_PLATFORM_ID = '1' as const
|
||||
|
||||
/**
|
||||
* Maps Node.js platform identifiers to WhatsApp-recognized platform names.
|
||||
* Values of `undefined` will fall back to DEFAULT_PLATFORM_NAME.
|
||||
*/
|
||||
const PLATFORM_MAP: Readonly<Record<NodeJS.Platform, string | undefined>> = {
|
||||
aix: 'AIX',
|
||||
darwin: 'Mac OS',
|
||||
win32: 'Windows',
|
||||
android: 'Android',
|
||||
cygwin: undefined,
|
||||
darwin: 'Mac OS',
|
||||
freebsd: 'FreeBSD',
|
||||
haiku: undefined,
|
||||
linux: undefined,
|
||||
netbsd: undefined,
|
||||
openbsd: 'OpenBSD',
|
||||
sunos: 'Solaris',
|
||||
linux: undefined,
|
||||
haiku: undefined,
|
||||
cygwin: undefined,
|
||||
netbsd: undefined
|
||||
win32: 'Windows',
|
||||
} as const
|
||||
|
||||
// ============================================================
|
||||
// Platform Type Resolution
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Pre-computed map of browser names to platform IDs.
|
||||
* Built once at module load to avoid repeated proto access.
|
||||
*/
|
||||
const BROWSER_TO_PLATFORM_ID: ReadonlyMap<string, string> = (() => {
|
||||
const platformType = proto.DeviceProps?.PlatformType
|
||||
if (!platformType || typeof platformType !== 'object') {
|
||||
return new Map<string, string>()
|
||||
}
|
||||
|
||||
const entries: Array<[string, string]> = []
|
||||
for (const [key, value] of Object.entries(platformType)) {
|
||||
// PlatformType has both numeric values and reverse mappings
|
||||
// We only want the string keys with numeric values
|
||||
if (typeof value === 'number' && typeof key === 'string' && !/^\d+$/.test(key)) {
|
||||
entries.push([key.toUpperCase(), value.toString()])
|
||||
}
|
||||
}
|
||||
return new Map(entries)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
// Helper Functions
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Resolves the current platform name for WhatsApp device identification.
|
||||
* Uses the OS platform detection with a fallback to Ubuntu.
|
||||
*
|
||||
* @returns Platform name string (never undefined)
|
||||
*/
|
||||
const getPlatformName = (): string => {
|
||||
try {
|
||||
const currentPlatform = platform()
|
||||
return PLATFORM_MAP[currentPlatform] ?? DEFAULT_PLATFORM_NAME
|
||||
} catch {
|
||||
return DEFAULT_PLATFORM_NAME
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a browser identifier for platform type lookup.
|
||||
*
|
||||
* @param browser - The browser identifier to normalize
|
||||
* @returns Uppercase trimmed string, or null if input is invalid
|
||||
*/
|
||||
const normalizeBrowserKey = (browser: unknown): string | null => {
|
||||
if (typeof browser !== 'string') {
|
||||
return null
|
||||
}
|
||||
const normalized = browser.trim()
|
||||
return normalized.length > 0 ? normalized.toUpperCase() : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely gets the OS release version with fallback
|
||||
*
|
||||
* @returns OS release string or empty string on failure
|
||||
*/
|
||||
const safeRelease = (): string => {
|
||||
try {
|
||||
return release()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Exported Constants
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Browser configuration presets for WhatsApp device registration.
|
||||
* Each factory returns a tuple of [platform, browser, version].
|
||||
*
|
||||
* @example
|
||||
* // Use Ubuntu preset
|
||||
* const config = Browsers.ubuntu('Chrome')
|
||||
* // Returns: ['Ubuntu', 'Chrome', '22.04.4']
|
||||
*
|
||||
* @example
|
||||
* // Use automatic platform detection
|
||||
* const config = Browsers.appropriate('MyApp')
|
||||
* // Returns: ['Mac OS', 'MyApp', '23.1.0'] (on macOS)
|
||||
*/
|
||||
export const Browsers: BrowsersMap = {
|
||||
ubuntu: browser => ['Ubuntu', browser, '22.04.4'],
|
||||
macOS: browser => ['Mac OS', browser, '14.4.1'],
|
||||
baileys: browser => ['Baileys', browser, '6.5.0'],
|
||||
windows: browser => ['Windows', browser, '10.0.22631'],
|
||||
/** The appropriate browser based on your OS & release */
|
||||
appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
|
||||
ubuntu: (browser: string): [string, string, string] => ['Ubuntu', browser, '22.04.4'],
|
||||
macOS: (browser: string): [string, string, string] => ['Mac OS', browser, '14.4.1'],
|
||||
windows: (browser: string): [string, string, string] => ['Windows', browser, '10.0.22631'],
|
||||
baileys: (browser: string): [string, string, string] => ['Baileys', browser, '6.5.0'],
|
||||
appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, safeRelease()],
|
||||
} as const
|
||||
|
||||
// ============================================================
|
||||
// Exported Functions
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Resolves the platform type ID for a given browser name.
|
||||
* Uses the WhatsApp protocol buffer definitions for mapping.
|
||||
*
|
||||
* @param browser - Browser identifier (e.g., 'chrome', 'firefox', 'safari')
|
||||
* @returns Platform type ID as string (defaults to '1' for Chrome)
|
||||
*
|
||||
* @example
|
||||
* getPlatformId('chrome') // Returns '1'
|
||||
* getPlatformId('CHROME') // Returns '1' (case-insensitive)
|
||||
* getPlatformId('firefox') // Returns platform-specific ID
|
||||
* getPlatformId('') // Returns '1' (default)
|
||||
* getPlatformId(undefined) // Returns '1' (default, handles invalid input)
|
||||
*/
|
||||
export const getPlatformId = (browser: string): string => {
|
||||
const key = normalizeBrowserKey(browser)
|
||||
if (key === null) {
|
||||
return DEFAULT_PLATFORM_ID
|
||||
}
|
||||
|
||||
return BROWSER_TO_PLATFORM_ID.get(key) ?? DEFAULT_PLATFORM_ID
|
||||
}
|
||||
|
||||
export const getPlatformId = (browser: string) => {
|
||||
const platformType = proto.DeviceProps.PlatformType[browser.toUpperCase() as any]
|
||||
return platformType ? platformType.toString() : '1' //chrome
|
||||
/**
|
||||
* Type guard to check if a platform string is a valid browser preset key.
|
||||
* Useful for external validation before using Browsers presets.
|
||||
*
|
||||
* @param value - Value to check
|
||||
* @returns True if value is a valid browser preset key
|
||||
*
|
||||
* @example
|
||||
* if (isValidBrowserPreset(userInput)) {
|
||||
* const config = Browsers[userInput]('MyApp')
|
||||
* }
|
||||
*/
|
||||
export const isValidBrowserPreset = (value: unknown): value is keyof BrowsersMap => {
|
||||
return typeof value === 'string' && value in Browsers
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { Browsers, getPlatformId, isValidBrowserPreset } from '../../Utils/browser-utils'
|
||||
|
||||
describe('browser-utils', () => {
|
||||
describe('Browsers', () => {
|
||||
describe('ubuntu', () => {
|
||||
it('should return correct tuple for Ubuntu', () => {
|
||||
const result = Browsers.ubuntu('Chrome')
|
||||
expect(result).toEqual(['Ubuntu', 'Chrome', '22.04.4'])
|
||||
})
|
||||
|
||||
it('should handle empty browser string', () => {
|
||||
const result = Browsers.ubuntu('')
|
||||
expect(result).toEqual(['Ubuntu', '', '22.04.4'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('macOS', () => {
|
||||
it('should return correct tuple for macOS', () => {
|
||||
const result = Browsers.macOS('Safari')
|
||||
expect(result).toEqual(['Mac OS', 'Safari', '14.4.1'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('windows', () => {
|
||||
it('should return correct tuple for Windows', () => {
|
||||
const result = Browsers.windows('Edge')
|
||||
expect(result).toEqual(['Windows', 'Edge', '10.0.22631'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('baileys', () => {
|
||||
it('should return correct tuple for Baileys', () => {
|
||||
const result = Browsers.baileys('Baileys')
|
||||
expect(result).toEqual(['Baileys', 'Baileys', '6.5.0'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('appropriate', () => {
|
||||
it('should return a valid tuple with platform detection', () => {
|
||||
const result = Browsers.appropriate('MyApp')
|
||||
expect(result).toHaveLength(3)
|
||||
expect(typeof result[0]).toBe('string')
|
||||
expect(result[1]).toBe('MyApp')
|
||||
expect(typeof result[2]).toBe('string')
|
||||
})
|
||||
|
||||
it('should never return undefined for platform', () => {
|
||||
const result = Browsers.appropriate('Test')
|
||||
expect(result[0]).toBeDefined()
|
||||
expect(result[0]).not.toBe('')
|
||||
// Should be a valid platform name
|
||||
expect(['Ubuntu', 'Mac OS', 'Windows', 'AIX', 'Android', 'FreeBSD', 'OpenBSD', 'Solaris']).toContain(result[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPlatformId', () => {
|
||||
it('should return platform ID for valid browser string', () => {
|
||||
const result = getPlatformId('chrome')
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
it('should be case-insensitive', () => {
|
||||
const lowercase = getPlatformId('chrome')
|
||||
const uppercase = getPlatformId('CHROME')
|
||||
const mixed = getPlatformId('Chrome')
|
||||
expect(lowercase).toBe(uppercase)
|
||||
expect(uppercase).toBe(mixed)
|
||||
})
|
||||
|
||||
it('should return default ID for empty string', () => {
|
||||
const result = getPlatformId('')
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
it('should return default ID for whitespace-only string', () => {
|
||||
const result = getPlatformId(' ')
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
it('should return default ID for unknown browser', () => {
|
||||
const result = getPlatformId('unknown_browser_xyz')
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
it('should handle string with leading/trailing whitespace', () => {
|
||||
const result = getPlatformId(' chrome ')
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
it('should return default ID for undefined input', () => {
|
||||
// @ts-expect-error - Testing runtime behavior with invalid input
|
||||
const result = getPlatformId(undefined)
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
it('should return default ID for null input', () => {
|
||||
// @ts-expect-error - Testing runtime behavior with invalid input
|
||||
const result = getPlatformId(null)
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
it('should return default ID for number input', () => {
|
||||
// @ts-expect-error - Testing runtime behavior with invalid input
|
||||
const result = getPlatformId(123)
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
it('should return default ID for object input', () => {
|
||||
// @ts-expect-error - Testing runtime behavior with invalid input
|
||||
const result = getPlatformId({ browser: 'chrome' })
|
||||
expect(result).toBe('1')
|
||||
})
|
||||
|
||||
it('should always return a string', () => {
|
||||
const validResult = getPlatformId('chrome')
|
||||
const invalidResult = getPlatformId('nonexistent')
|
||||
expect(typeof validResult).toBe('string')
|
||||
expect(typeof invalidResult).toBe('string')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidBrowserPreset', () => {
|
||||
it('should return true for valid preset keys', () => {
|
||||
expect(isValidBrowserPreset('ubuntu')).toBe(true)
|
||||
expect(isValidBrowserPreset('macOS')).toBe(true)
|
||||
expect(isValidBrowserPreset('windows')).toBe(true)
|
||||
expect(isValidBrowserPreset('baileys')).toBe(true)
|
||||
expect(isValidBrowserPreset('appropriate')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for invalid preset keys', () => {
|
||||
expect(isValidBrowserPreset('invalid')).toBe(false)
|
||||
expect(isValidBrowserPreset('UBUNTU')).toBe(false)
|
||||
expect(isValidBrowserPreset('Ubuntu')).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for non-string values', () => {
|
||||
expect(isValidBrowserPreset(undefined)).toBe(false)
|
||||
expect(isValidBrowserPreset(null)).toBe(false)
|
||||
expect(isValidBrowserPreset(123)).toBe(false)
|
||||
expect(isValidBrowserPreset({})).toBe(false)
|
||||
expect(isValidBrowserPreset([])).toBe(false)
|
||||
})
|
||||
|
||||
it('should work as type guard', () => {
|
||||
const input: unknown = 'ubuntu'
|
||||
if (isValidBrowserPreset(input)) {
|
||||
// TypeScript should allow this after the type guard
|
||||
const config = Browsers[input]('Test')
|
||||
expect(config).toHaveLength(3)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases and robustness', () => {
|
||||
it('should not throw on any Browsers method call', () => {
|
||||
expect(() => Browsers.ubuntu('test')).not.toThrow()
|
||||
expect(() => Browsers.macOS('test')).not.toThrow()
|
||||
expect(() => Browsers.windows('test')).not.toThrow()
|
||||
expect(() => Browsers.baileys('test')).not.toThrow()
|
||||
expect(() => Browsers.appropriate('test')).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw on getPlatformId with any input', () => {
|
||||
expect(() => getPlatformId('valid')).not.toThrow()
|
||||
expect(() => getPlatformId('')).not.toThrow()
|
||||
// @ts-expect-error - Testing runtime behavior
|
||||
expect(() => getPlatformId(undefined)).not.toThrow()
|
||||
// @ts-expect-error - Testing runtime behavior
|
||||
expect(() => getPlatformId(null)).not.toThrow()
|
||||
// @ts-expect-error - Testing runtime behavior
|
||||
expect(() => getPlatformId({})).not.toThrow()
|
||||
})
|
||||
|
||||
it('all Browsers methods should return readonly-compatible tuples', () => {
|
||||
const presets = ['ubuntu', 'macOS', 'windows', 'baileys', 'appropriate'] as const
|
||||
for (const preset of presets) {
|
||||
const result = Browsers[preset]('Test')
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect(result.length).toBe(3)
|
||||
expect(result.every(item => typeof item === 'string')).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user