use crate::config::config_base::field_helpers::*;
use crate::config::config_base::ConfigType;
use crate::config::config_message::{ConfigData, ConfigValue, ScalarValue};
use crate::config::namespaces::Namespace;
use crate::config::profile_pic::ProfilePic;
pub const MAX_NAME_LENGTH: usize = 100;
#[derive(Debug, Clone, Default)]
pub struct UserProfile {
pub name: Option<String>,
pub profile_pic: ProfilePic,
pub reupload_pic: ProfilePic,
pub nts_priority: i32,
pub nts_expiry: u32,
pub blinded_msgreqs: Option<bool>,
pub profile_bitset: u64,
pub profile_updated: i64,
pub reupload_updated: i64,
pub pro_access_expiry_ms: Option<i64>,
}
impl UserProfile {
pub fn set_name(&mut self, name: &str) -> Result<(), String> {
if name.len() > MAX_NAME_LENGTH {
return Err("Invalid profile name: exceeds maximum length".into());
}
self.name = if name.is_empty() {
None
} else {
Some(name.to_string())
};
Ok(())
}
pub fn get_profile_pic(&self) -> &ProfilePic {
if self.reupload_updated > self.profile_updated && !self.reupload_pic.is_empty() {
&self.reupload_pic
} else {
&self.profile_pic
}
}
pub fn get_profile_updated(&self) -> i64 {
std::cmp::max(self.profile_updated, self.reupload_updated)
}
pub fn get_nts_expiry(&self) -> Option<u32> {
if self.nts_expiry > 0 {
Some(self.nts_expiry)
} else {
None
}
}
pub fn set_nts_expiry(&mut self, seconds: u32) {
self.nts_expiry = seconds;
}
}
impl ConfigType for UserProfile {
fn namespace() -> Namespace {
Namespace::UserProfile
}
fn encryption_domain() -> &'static str {
"UserProfile"
}
fn accepts_protobuf() -> bool {
true
}
fn load_from_data(&mut self, data: &ConfigData) {
self.name = get_string(data, b"n");
let t = get_int_or_zero(data, b"t");
let t_reupload = get_int_or_zero(data, b"T");
self.profile_pic = ProfilePic::default();
if let Some(url) = get_string(data, b"p") {
self.profile_pic.url = url;
}
if let Some(key) = get_bytes(data, b"q")
&& key.len() == 32 {
self.profile_pic.key = key;
}
self.reupload_pic = ProfilePic::default();
if let Some(url) = get_string(data, b"P") {
self.reupload_pic.url = url;
}
if let Some(key) = get_bytes(data, b"Q")
&& key.len() == 32 {
self.reupload_pic.key = key;
}
self.profile_updated = t;
self.reupload_updated = t_reupload;
self.nts_priority = get_int_or_zero(data, b"+") as i32;
self.nts_expiry = {
let e = get_int_or_zero(data, b"e");
if e > 0 { e as u32 } else { 0 }
};
self.blinded_msgreqs = get_int(data, b"M").map(|v| v != 0);
self.profile_bitset = match data.get(b"f".as_ref()) {
Some(ConfigValue::Set(items)) => {
let mut bits = 0u64;
for item in items {
if let ScalarValue::Integer(v) = item
&& *v >= 0 && *v < 64 {
bits |= 1 << *v;
}
}
bits
}
_ => 0,
};
self.pro_access_expiry_ms = get_int(data, b"E");
}
fn store_to_data(&self, data: &mut ConfigData) {
if let Some(ref name) = self.name {
set_nonempty_str(data, b"n", name);
} else {
data.remove(b"n".as_ref());
}
if !self.profile_pic.url.is_empty() && self.profile_pic.key.len() == 32 {
set_nonempty_str(data, b"p", &self.profile_pic.url);
set_nonempty_bytes(data, b"q", &self.profile_pic.key);
} else {
data.remove(b"p".as_ref());
data.remove(b"q".as_ref());
}
if !self.reupload_pic.url.is_empty() && self.reupload_pic.key.len() == 32 {
set_nonempty_str(data, b"P", &self.reupload_pic.url);
set_nonempty_bytes(data, b"Q", &self.reupload_pic.key);
} else {
data.remove(b"P".as_ref());
data.remove(b"Q".as_ref());
}
set_positive_int(data, b"t", self.profile_updated);
set_positive_int(data, b"T", self.reupload_updated);
set_nonzero_int(data, b"+", self.nts_priority as i64);
set_positive_int(data, b"e", self.nts_expiry as i64);
match self.blinded_msgreqs {
Some(v) => {
data.insert(b"M".to_vec(), ConfigValue::Integer(v as i64));
}
None => {
data.remove(b"M".as_ref());
}
}
if self.profile_bitset != 0 {
let mut items = Vec::new();
for bit in 0..64u64 {
if self.profile_bitset & (1 << bit) != 0 {
items.push(ScalarValue::Integer(bit as i64));
}
}
items.sort();
data.insert(b"f".to_vec(), ConfigValue::Set(items));
} else {
data.remove(b"f".as_ref());
}
if let Some(expiry) = self.pro_access_expiry_ms {
data.insert(b"E".to_vec(), ConfigValue::Integer(expiry));
} else {
data.remove(b"E".as_ref());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::config_base::ConfigBase;
#[test]
fn test_default_user_profile() {
let profile = UserProfile::default();
assert!(profile.name.is_none());
assert!(profile.profile_pic.is_empty());
assert_eq!(profile.nts_priority, 0);
assert_eq!(profile.nts_expiry, 0);
assert!(profile.blinded_msgreqs.is_none());
}
#[test]
fn test_set_name_valid() {
let mut profile = UserProfile::default();
profile.set_name("Alice").unwrap();
assert_eq!(profile.name.as_deref(), Some("Alice"));
}
#[test]
fn test_set_name_too_long() {
let mut profile = UserProfile::default();
let long_name = "x".repeat(MAX_NAME_LENGTH + 1);
assert!(profile.set_name(&long_name).is_err());
}
#[test]
fn test_set_name_empty_clears() {
let mut profile = UserProfile::default();
profile.set_name("Alice").unwrap();
profile.set_name("").unwrap();
assert!(profile.name.is_none());
}
#[test]
fn test_roundtrip_serialization() {
let mut profile = UserProfile::default();
profile.set_name("Test User").unwrap();
profile.nts_priority = 5;
profile.nts_expiry = 3600;
profile.blinded_msgreqs = Some(true);
profile.profile_bitset = 0b101;
profile.profile_updated = 1700000000;
let mut data = ConfigData::new();
profile.store_to_data(&mut data);
let mut loaded = UserProfile::default();
loaded.load_from_data(&data);
assert_eq!(loaded.name.as_deref(), Some("Test User"));
assert_eq!(loaded.nts_priority, 5);
assert_eq!(loaded.nts_expiry, 3600);
assert_eq!(loaded.blinded_msgreqs, Some(true));
assert_eq!(loaded.profile_bitset, 0b101);
assert_eq!(loaded.profile_updated, 1700000000);
}
#[test]
fn test_profile_pic_timestamp_precedence() {
let mut profile = UserProfile::default();
profile.profile_pic = ProfilePic {
url: "https://example.com/old.jpg".into(),
key: vec![0xAA; 32],
};
profile.profile_updated = 1000;
profile.reupload_pic = ProfilePic {
url: "https://example.com/new.jpg".into(),
key: vec![0xBB; 32],
};
profile.reupload_updated = 2000;
let pic = profile.get_profile_pic();
assert_eq!(pic.url, "https://example.com/new.jpg");
}
#[test]
fn test_config_base_with_user_profile() {
let seed = hex_literal::hex!(
"0123456789abcdef0123456789abcdef00000000000000000000000000000000"
);
let base: ConfigBase<UserProfile> = ConfigBase::new(&seed, None).unwrap();
assert!(base.get().name.is_none());
assert!(!base.needs_push());
}
#[test]
fn test_config_base_set_and_push() {
let seed = hex_literal::hex!(
"0123456789abcdef0123456789abcdef00000000000000000000000000000000"
);
let mut base: ConfigBase<UserProfile> = ConfigBase::new(&seed, None).unwrap();
base.get_mut().set_name("Test").unwrap();
assert!(base.needs_push());
let push_data = base.push();
assert_eq!(push_data.seqno, base.seqno());
assert!(!push_data.messages.is_empty());
}
}