use anyhow::Result;
use colored::*;
use vrchatapi::apis;
pub fn format_user_status(status: &vrchatapi::models::UserStatus, use_color: bool) -> String {
let status_text = match status {
vrchatapi::models::UserStatus::Active => "Active",
vrchatapi::models::UserStatus::JoinMe => "Join me",
vrchatapi::models::UserStatus::AskMe => "Ask me",
vrchatapi::models::UserStatus::Busy => "Busy",
vrchatapi::models::UserStatus::Offline => "Offline",
};
if use_color {
match status {
vrchatapi::models::UserStatus::Active => status_text.green().to_string(),
vrchatapi::models::UserStatus::JoinMe => status_text.cyan().to_string(),
vrchatapi::models::UserStatus::AskMe => status_text.yellow().to_string(),
vrchatapi::models::UserStatus::Busy => status_text.red().to_string(),
vrchatapi::models::UserStatus::Offline => status_text.bright_black().to_string(),
}
} else {
status_text.to_string()
}
}
pub fn format_platform_short(platform: &str) -> String {
match platform {
"standalonewindows" => "PC".to_string(),
"android" => "Quest".to_string(),
"quest" => "Quest".to_string(),
"ios" => "iOS".to_string(),
"steamvr" => "SteamVR".to_string(),
"oculuspc" => "Oculus".to_string(),
platform
if platform.starts_with("2019.")
|| platform.starts_with("2020.")
|| platform.starts_with("2021.")
|| platform.starts_with("2022.")
|| platform.starts_with("2023.")
|| platform.starts_with("2024.") =>
{
if let Ok(year) = platform.chars().take(4).collect::<String>().parse::<u16>() {
format!("Unity{year}")
} else {
"Unity".to_string()
}
}
"unknownplatform" => "Unknown".to_string(),
"" => "Unknown".to_string(),
_ => {
if platform.len() > 8 {
let truncated = &platform[..5];
format!("{truncated}...")
} else {
platform.to_string()
}
}
}
}
pub fn format_text_with_width(text: &str, width: usize) -> String {
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
let display_width = text.width();
if display_width <= width {
let padding = width - display_width;
let spaces = " ".repeat(padding);
format!("{text}{spaces}")
} else {
let mut truncated = String::new();
let mut current_width = 0;
let available_width = width.saturating_sub(3);
for ch in text.chars() {
let char_width = ch.width().unwrap_or(0);
if current_width + char_width <= available_width {
truncated.push(ch);
current_width += char_width;
} else {
break;
}
}
let result = format!("{truncated}...");
let result_width = result.width();
if result_width < width {
let padding = width - result_width;
let spaces = " ".repeat(padding);
format!("{result}{spaces}")
} else {
result
}
}
}
pub fn is_valid_user_id(user_id: &str) -> bool {
if let Some(suffix) = user_id.strip_prefix("usr_") {
return suffix.len() >= 32; }
if user_id.len() == 8 {
let is_alphanumeric = user_id.chars().all(|c| c.is_ascii_alphanumeric());
if !is_alphanumeric {
return false;
}
let has_digit = user_id.chars().any(|c| c.is_ascii_digit());
let has_upper = user_id.chars().any(|c| c.is_ascii_uppercase());
let has_lower = user_id.chars().any(|c| c.is_ascii_lowercase());
return has_digit && (has_upper || has_lower);
}
false
}
pub async fn resolve_display_name_to_user_id(
api_config: &vrchatapi::apis::configuration::Configuration,
display_name: &str,
) -> Result<String> {
let search_results = match apis::users_api::search_users(
api_config,
Some(display_name),
None, Some(10), None, )
.await
{
Ok(results) => results,
Err(e) => {
return Err(anyhow::anyhow!(
"Failed to search for user '{}': {}",
display_name,
e
));
}
};
if search_results.is_empty() {
return Err(anyhow::anyhow!(
"No users found with display name '{}'",
display_name
));
}
let exact_match = search_results
.iter()
.find(|user| user.display_name.to_lowercase() == display_name.to_lowercase());
if let Some(user) = exact_match {
return Ok(user.id.clone());
}
let suggestions: Vec<String> = search_results
.iter()
.take(5)
.map(|user| {
let display_name = &user.display_name;
format!(" - {display_name}")
})
.collect();
Err(anyhow::anyhow!(
"No exact match found for display name '{}'. Similar users found:\n{}",
display_name,
suggestions.join("\n")
))
}
pub async fn resolve_user_identifier(
api_config: &vrchatapi::apis::configuration::Configuration,
identifier: &str,
) -> Result<String> {
if is_valid_user_id(identifier) {
return Ok(identifier.to_string());
}
resolve_display_name_to_user_id(api_config, identifier).await
}
#[cfg(test)]
mod tests {
use super::*;
use vrchatapi::models::UserStatus;
#[test]
fn test_format_user_status_without_color() {
assert_eq!(format_user_status(&UserStatus::Active, false), "Active");
assert_eq!(format_user_status(&UserStatus::JoinMe, false), "Join me");
assert_eq!(format_user_status(&UserStatus::AskMe, false), "Ask me");
assert_eq!(format_user_status(&UserStatus::Busy, false), "Busy");
assert_eq!(format_user_status(&UserStatus::Offline, false), "Offline");
}
#[test]
fn test_format_user_status_with_color() {
let active_colored = format_user_status(&UserStatus::Active, true);
assert!(active_colored.contains("Active"));
let busy_colored = format_user_status(&UserStatus::Busy, true);
assert!(busy_colored.contains("Busy"));
}
#[test]
fn test_format_platform_short() {
assert_eq!(format_platform_short("standalonewindows"), "PC");
assert_eq!(format_platform_short("android"), "Quest");
assert_eq!(format_platform_short("quest"), "Quest");
assert_eq!(format_platform_short("ios"), "iOS");
assert_eq!(format_platform_short("steamvr"), "SteamVR");
assert_eq!(format_platform_short("oculuspc"), "Oculus");
assert_eq!(format_platform_short("unknownplatform"), "Unknown");
assert_eq!(format_platform_short(""), "Unknown");
}
#[test]
fn test_format_platform_unity_versions() {
assert_eq!(format_platform_short("2019.4.31f1"), "Unity2019");
assert_eq!(format_platform_short("2022.3.22f1"), "Unity2022");
assert_eq!(format_platform_short("2024.1.0f1"), "Unity2024");
}
#[test]
fn test_format_platform_long_strings() {
assert_eq!(format_platform_short("verylongplatformname"), "veryl...");
assert_eq!(format_platform_short("12345678"), "12345678");
assert_eq!(format_platform_short("123456789"), "12345...");
}
#[test]
fn test_format_text_with_width_exact_fit() {
assert_eq!(format_text_with_width("hello", 5), "hello");
}
#[test]
fn test_format_text_with_width_padding() {
assert_eq!(format_text_with_width("hi", 5), "hi ");
}
#[test]
fn test_format_text_with_width_truncation() {
assert_eq!(format_text_with_width("hello world", 8), "hello...");
assert_eq!(format_text_with_width("test", 8), "test ");
}
#[test]
fn test_format_text_with_width_unicode() {
assert_eq!(format_text_with_width("こんにちは", 6), "こ... ");
}
#[test]
fn test_is_valid_user_id_modern_format() {
assert!(is_valid_user_id("usr_12345678-1234-1234-1234-123456789012"));
assert!(is_valid_user_id("usr_abcdef12-3456-7890-abcd-ef1234567890"));
assert!(!is_valid_user_id("usr_short"));
assert!(!is_valid_user_id("usr_"));
}
#[test]
fn test_is_valid_user_id_legacy_format() {
assert!(is_valid_user_id("Abc123Xy")); assert!(is_valid_user_id("Test1234")); assert!(!is_valid_user_id("Nekomasu")); assert!(!is_valid_user_id("12345678")); assert!(!is_valid_user_id("testname")); }
#[test]
fn test_is_valid_user_id_invalid_formats() {
assert!(!is_valid_user_id(""));
assert!(!is_valid_user_id("invalid"));
assert!(!is_valid_user_id("toolong123456789"));
assert!(!is_valid_user_id("short"));
assert!(!is_valid_user_id("special@chars"));
}
}