//! Friend presence, rights, offers, calling cards, and map tracking. #![allow(clippy::missing_errors_doc)] #![allow(clippy::must_use_candidate)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::inherent_to_string)] // Fixed mapped CLR ToString method. use crate::agent_manager::EventRegistry; use crate::network_manager::RawPacketReceivedEventArgs; use crate::packet_catalog::{GeneratedPacket, PacketType}; use crate::{Error, FriendRights, GridClient, InstantMessageDialog}; use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser}; use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription}; use libremetaverse_types::{FolderType, UUID, Vector3}; use std::collections::{BTreeMap, HashMap}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; const MAX_FRIENDS: usize = 100_000; fn mutex(value: &Mutex) -> std::sync::MutexGuard<'_, T> { value .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn read(value: &RwLock) -> std::sync::RwLockReadGuard<'_, T> { value .read() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn write(value: &RwLock) -> std::sync::RwLockWriteGuard<'_, T> { value .write() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn wire_string(value: &[u8]) -> String { String::from_utf8_lossy(value.split(|b| *b == 0).next().unwrap_or_default()).into_owned() } #[derive(Clone, Debug, Eq, PartialEq)] pub struct FriendInfo { id: UUID, name: String, online: bool, their_rights: FriendRights, my_rights: FriendRights, } impl FriendInfo { pub(crate) fn native_new( id: UUID, their_rights: FriendRights, my_rights: FriendRights, ) -> Self { Self { id, name: String::new(), online: false, their_rights, my_rights, } } pub fn uuid(&self) -> UUID { self.id } pub fn name(&self) -> String { self.name.clone() } pub fn set_name(&mut self, value: String) { self.name = value; } pub fn is_online(&self) -> bool { self.online } pub fn set_is_online(&mut self, value: bool) { self.online = value; } pub fn their_friend_rights(&self) -> FriendRights { self.their_rights } pub fn set_their_friend_rights(&mut self, value: FriendRights) { self.their_rights = value; } pub fn my_friend_rights(&self) -> FriendRights { self.my_rights } pub fn set_my_friend_rights(&mut self, value: FriendRights) { self.my_rights = value; } pub fn can_see_me_online(&self) -> bool { self.their_rights.0 & FriendRights::CAN_SEE_ONLINE.0 != 0 } pub fn set_can_see_me_online(&mut self, value: bool) { set_right(&mut self.their_rights, FriendRights::CAN_SEE_ONLINE, value); if !value { set_right(&mut self.their_rights, FriendRights::CAN_SEE_ON_MAP, false); } } pub fn can_see_me_on_map(&self) -> bool { self.their_rights.0 & FriendRights::CAN_SEE_ON_MAP.0 != 0 } pub fn set_can_see_me_on_map(&mut self, value: bool) { if self.can_see_me_online() { set_right(&mut self.their_rights, FriendRights::CAN_SEE_ON_MAP, value); } } pub fn can_modify_my_objects(&self) -> bool { self.their_rights.0 & FriendRights::CAN_MODIFY_OBJECTS.0 != 0 } pub fn set_can_modify_my_objects(&mut self, value: bool) { set_right( &mut self.their_rights, FriendRights::CAN_MODIFY_OBJECTS, value, ); } pub fn can_see_them_online(&self) -> bool { self.my_rights.0 & FriendRights::CAN_SEE_ONLINE.0 != 0 } pub fn set_can_see_them_online(&mut self, value: bool) { set_right(&mut self.my_rights, FriendRights::CAN_SEE_ONLINE, value); } pub fn can_see_them_on_map(&self) -> bool { self.my_rights.0 & FriendRights::CAN_SEE_ON_MAP.0 != 0 } pub fn set_can_see_them_on_map(&mut self, value: bool) { set_right(&mut self.my_rights, FriendRights::CAN_SEE_ON_MAP, value); } pub fn can_modify_their_objects(&self) -> bool { self.my_rights.0 & FriendRights::CAN_MODIFY_OBJECTS.0 != 0 } pub fn set_can_modify_their_objects(&mut self, value: bool) { set_right(&mut self.my_rights, FriendRights::CAN_MODIFY_OBJECTS, value); } pub fn to_string(&self) -> String { let who = if self.name.is_empty() { self.id.to_string() } else { self.name.clone() }; format!( "{who} (Their Rights: {}, My Rights: {})", self.their_rights.0, self.my_rights.0 ) } } fn set_right(rights: &mut FriendRights, flag: FriendRights, enabled: bool) { if enabled { rights.0 |= flag.0; } else { rights.0 &= !flag.0; } } macro_rules! simple_event { ($name:ident { $($field:ident : $ty:ty => $getter:ident),+ $(,)? }) => { #[derive(Clone)] pub struct $name { $( $field: $ty ),+ } impl $name { pub fn new($($field: $ty),+) -> Result { Ok(Self { $($field),+ }) } $(pub fn $getter(&self) -> $ty { self.$field.clone() })+ } }; } simple_event!(CallingCardOfferedEventArgs { agent_id: UUID => agent_id, agent_name: String => agent_name, transaction_id: UUID => transaction_id }); simple_event!(FriendFoundReplyEventArgs { agent_id: UUID => agent_id, region_handle: u64 => region_handle, location: Vector3 => location }); simple_event!(FriendInfoEventArgs { friend: FriendInfo => friend }); simple_event!(FriendNamesEventArgs { names: HashMap => names }); simple_event!(FriendsReadyEventArgs { count: i32 => count }); simple_event!(FriendshipOfferedEventArgs { agent_id: UUID => agent_id, agent_name: String => agent_name, im_session_id: UUID => session_id }); simple_event!(FriendshipResponseEventArgs { agent_id: UUID => agent_id, agent_name: String => agent_name, accepted: bool => accepted }); simple_event!(FriendshipTerminatedEventArgs { agent_id: UUID => agent_id, agent_name: String => agent_name }); #[derive(Default)] struct FriendEvents { calling_card: EventRegistry, found: EventRegistry, names: EventRegistry, offline: EventRegistry, online: EventRegistry, rights: EventRegistry, offered: EventRegistry, response: EventRegistry, terminated: EventRegistry, ready: EventRegistry, } pub(crate) struct FriendsManagerInner { client: Arc, friends: RwLock>, requests: RwLock>, events: FriendEvents, subscriptions: Mutex>, login_callback: Mutex>, disposed: AtomicBool, } #[derive(Clone)] pub struct FriendsManager { pub(crate) inner: Arc, } impl FriendsManager { pub(crate) fn native_new(client: Arc) -> Result { if let Some(inner) = client.cached_friends_manager_inner() { return Ok(Self { inner }); } let inner = Arc::new(FriendsManagerInner { client: Arc::clone(&client), friends: RwLock::new(HashMap::new()), requests: RwLock::new(HashMap::new()), events: FriendEvents::default(), subscriptions: Mutex::new(Vec::new()), login_callback: Mutex::new(None), disposed: AtomicBool::new(false), }); let weak = Arc::downgrade(&inner); mutex(&inner.subscriptions).push(client.network().subscribe_raw_packet(Arc::new( move |e| { if let Some(inner) = weak.upgrade() { inner.handle_packet(e); } }, ))); let weak = Arc::downgrade(&inner); let mut callback_client = (*client).clone(); mutex(&inner.subscriptions).push(callback_client.self_().subscribe_im(Arc::new( move |e| { if let Some(inner) = weak.upgrade() { inner.handle_im(e); } }, ))); let weak = Arc::downgrade(&inner); let callback = crate::NetworkManagerLoginResponseCallback::from_handler( move |success, _, _, _, response| { if success && let Some(inner) = weak.upgrade() { inner.load_login(response); } }, ); client.network().native_register_login_response_callback( callback.clone(), Some(vec!["buddy-list".into()]), )?; *mutex(&inner.login_callback) = Some(callback); Ok(Self { inner: client.install_friends_manager_inner(inner), }) } fn ids(&self) -> (UUID, UUID) { let mut c = (*self.inner.client).clone(); let a = c.self_(); (a.agent_id(), a.session_id()) } fn send(&self, p: &T, ty: PacketType) -> Result<(), Error> { if self.inner.disposed.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } let sim = self .inner .client .network() .current_sim() .ok_or(Error::InvalidOperation)?; let bytes = p.encode_packet()?; let result = sim.native_send_packet_data( bytes.clone(), i32::try_from(bytes.len()).map_err(|_| Error::Argument)?, ty, false, ); match result { Err(Error::InvalidOperation) if !sim.connected() => Ok(()), result => result, } } pub fn friend_list(&self) -> HashMap { read(&self.inner.friends).clone() } pub fn friend_requests(&self) -> HashMap { read(&self.inner.requests).clone() } pub fn dispose(&self) -> Result<(), Error> { self.inner.dispose(); Ok(()) } pub fn subscribe_calling_card_offered( &self, h: EventHandler, ) -> Subscription { self.inner.events.calling_card.subscribe(h) } pub fn subscribe_friend_found_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.found.subscribe(h) } pub fn subscribe_friend_names(&self, h: EventHandler) -> Subscription { self.inner.events.names.subscribe(h) } pub fn subscribe_friend_offline(&self, h: EventHandler) -> Subscription { self.inner.events.offline.subscribe(h) } pub fn subscribe_friend_online(&self, h: EventHandler) -> Subscription { self.inner.events.online.subscribe(h) } pub fn subscribe_friend_rights_update( &self, h: EventHandler, ) -> Subscription { self.inner.events.rights.subscribe(h) } pub fn subscribe_friendship_offered( &self, h: EventHandler, ) -> Subscription { self.inner.events.offered.subscribe(h) } pub fn subscribe_friendship_response( &self, h: EventHandler, ) -> Subscription { self.inner.events.response.subscribe(h) } pub fn subscribe_friendship_terminated( &self, h: EventHandler, ) -> Subscription { self.inner.events.terminated.subscribe(h) } pub fn subscribe_friends_list_ready( &self, h: EventHandler, ) -> Subscription { self.inner.events.ready.subscribe(h) } pub fn accept_friendship(&self, from: UUID, transaction: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut p = crate::packets::AcceptFriendshipPacket::new_with_constructor()?; p.agent_data.agent_id = agent; p.agent_data.session_id = session; p.transaction_block.transaction_id = transaction; let mut folder = crate::packets::AcceptFriendshipPacketFolderDataBlock::new_with_constructor()?; folder.folder_id = self .inner .client .inventory() .find_folder_for_type_with_folder_type(FolderType::CallingCard)?; p.folder_data.push(folder); self.send(&p, PacketType::AcceptFriendship)?; self.inner.accept(from); Ok(()) } pub fn decline_friendship(&self, from: UUID, transaction: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut p = crate::packets::DeclineFriendshipPacket::new_with_constructor()?; p.agent_data.agent_id = agent; p.agent_data.session_id = session; p.transaction_block.transaction_id = transaction; self.send(&p, PacketType::DeclineFriendship)?; write(&self.inner.requests).remove(&from); Ok(()) } pub async fn accept_friendship_via_cap( &self, from: UUID, ct: Option, ) -> Result<(), Error> { self.friend_cap("AcceptFriendship", "POST", from, ct) .await?; self.inner.accept(from); Ok(()) } pub async fn decline_friendship_via_cap( &self, from: UUID, ct: Option, ) -> Result<(), Error> { self.friend_cap("DeclineFriendship", "DELETE", from, ct) .await?; write(&self.inner.requests).remove(&from); Ok(()) } async fn friend_cap( &self, cap: &str, method: &str, from: UUID, ct: Option, ) -> Result<(), Error> { let token = ct.unwrap_or_else(|| self.inner.client.cancellation_token()); token.throw_if_cancellation_requested()?; let caps = self .inner .client .network() .current_sim() .and_then(|s| s.native_caps()) .ok_or(Error::InvalidOperation)?; let uri = caps .capability_uri(cap.into())? .ok_or(Error::InvalidOperation)?; let mut url = reqwest::Url::parse(&uri.0).map_err(|_| Error::Argument)?; url.query_pairs_mut().append_pair("from", &from.to_string()); let http = self.inner.client.native_http_caps_client(); let (response, bytes) = if method == "POST" { http.post_with_uri_osd_format_osd_cancellation_token_i_progress( libremetaverse_types::compat::Uri(url.into()), OSDFormat::Xml, OSD::Undefined, token, None, ) .await? } else { http.send_custom( "DELETE", libremetaverse_types::compat::Uri(url.into()), BTreeMap::new(), None, Vec::new(), token, ) .await? }; if !response.is_success_status_code() || bytes.len() > 1024 * 1024 { return Err(Error::InvalidOperation); } if !bytes.is_empty() { let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else { return Err(Error::Argument); }; if !map .get("success") .and_then(|v| v.as_boolean().ok()) .unwrap_or(false) { return Err(Error::InvalidOperation); } } Ok(()) } pub fn offer_friendship_with_uuid(&self, id: UUID) -> Result<(), Error> { self.offer_friendship_with_uuid_string(id, "Do ya wanna be my buddy?".into()) } pub fn offer_friendship_with_uuid_string( &self, id: UUID, message: String, ) -> Result<(), Error> { if message.len() > 1024 { return Err(Error::Argument); } let mut client = (*self.inner.client).clone(); let region = client .network() .current_sim() .map_or_else(UUID::zero, |s| s.id); let me = client.self_(); me.native_instant_message_full( me.name(), id, message, UUID::random()?, InstantMessageDialog::FriendshipOffered, crate::InstantMessageOnline::Offline, me.sim_position(), region, Vec::new(), ) } pub fn terminate_friendship(&self, id: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut p = crate::packets::TerminateFriendshipPacket::new_with_constructor()?; p.agent_data.agent_id = agent; p.agent_data.session_id = session; p.ex_block.other_id = id; self.send(&p, PacketType::TerminateFriendship)?; write(&self.inner.friends).remove(&id); Ok(()) } pub fn grant_rights(&self, id: UUID, rights: FriendRights) -> Result<(), Error> { let (agent, session) = self.ids(); let mut p = crate::packets::GrantUserRightsPacket::new_with_constructor()?; p.agent_data.agent_id = agent; p.agent_data.session_id = session; let mut b = crate::packets::GrantUserRightsPacketRightsBlock::new_with_constructor()?; b.agent_related = id; b.related_rights = rights.0; p.rights.push(b); self.send(&p, PacketType::GrantUserRights) } pub fn map_friend(&self, id: UUID) -> Result<(), Error> { let (agent, _) = self.ids(); let mut p = crate::packets::FindAgentPacket::new_with_constructor()?; p.agent_block.hunter = agent; p.agent_block.prey = id; let b = crate::packets::FindAgentPacketLocationBlockBlock::new_with_constructor()?; p.location_block.push(b); self.send(&p, PacketType::FindAgent) } pub fn track_friend(&self, id: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut p = crate::packets::TrackAgentPacket::new_with_constructor()?; p.agent_data.agent_id = agent; p.agent_data.session_id = session; p.target_data.prey_id = id; self.send(&p, PacketType::TrackAgent) } pub fn request_online_notification(&self, id: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut p = crate::packets::GenericMessagePacket::new_with_constructor()?; p.agent_data.agent_id = agent; p.agent_data.session_id = session; p.method_data.method = b"requestonlinenotification".to_vec(); let mut b = crate::packets::GenericMessagePacketParamListBlock::new_with_constructor()?; b.parameter = id.to_string().into_bytes(); p.param_list.push(b); self.send(&p, PacketType::GenericMessage) } pub fn give_calling_card(&self, id: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut p = crate::packets::OfferCallingCardPacket::new_with_constructor()?; p.agent_data.agent_id = agent; p.agent_data.session_id = session; p.agent_block.dest_id = id; p.agent_block.transaction_id = UUID::random()?; self.send(&p, PacketType::OfferCallingCard) } pub fn accept_calling_card(&self, _from: UUID, transaction: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut p = crate::packets::AcceptCallingCardPacket::new_with_constructor()?; p.agent_data.agent_id = agent; p.agent_data.session_id = session; p.transaction_block.transaction_id = transaction; let mut b = crate::packets::AcceptCallingCardPacketFolderDataBlock::new_with_constructor()?; b.folder_id = self .inner .client .inventory() .find_folder_for_type_with_folder_type(FolderType::CallingCard)?; p.folder_data.push(b); self.send(&p, PacketType::AcceptCallingCard) } pub fn decline_calling_card(&self, _from: UUID, transaction: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut p = crate::packets::DeclineCallingCardPacket::new_with_constructor()?; p.agent_data.agent_id = agent; p.agent_data.session_id = session; p.transaction_block.transaction_id = transaction; self.send(&p, PacketType::DeclineCallingCard) } pub fn on_find_agent_reply_handler( &self, _sender: Option, e: crate::PacketReceivedEventArgs, ) -> Result<(), Error> { // The native network pipeline dispatches the payload-bearing raw packet // before its metadata-only compatibility callback. `handle_packet` // performs the decode and event emission; this public callback retains // the upstream type guard without emitting a duplicate event. if e.packet().type_ == PacketType::FindAgent { Ok(()) } else { Err(Error::Argument) } } } impl FriendsManagerInner { fn accept(&self, id: UUID) { let mut friends = write(&self.friends); if friends.len() < MAX_FRIENDS || friends.contains_key(&id) { friends.entry(id).or_insert_with(|| { FriendInfo::native_new( id, FriendRights::CAN_SEE_ONLINE, FriendRights::CAN_SEE_ONLINE, ) }); } drop(friends); write(&self.requests).remove(&id); } fn dispose(&self) { if self.disposed.swap(true, Ordering::AcqRel) { return; } mutex(&self.subscriptions).clear(); if let Some(cb) = mutex(&self.login_callback).take() { let _ = self .client .network() .native_unregister_login_response_callback(&cb); } } fn load_login(&self, response: Option) { let Some(response) = response else { return }; let mut friends = write(&self.friends); for b in response.buddy_list().into_iter().take(MAX_FRIENDS) { if let Ok(id) = UUID::parse(b.buddy_id.chars().take(36).collect::()) { friends.entry(id).or_insert_with(|| { FriendInfo::native_new( id, FriendRights(b.buddy_rights_given), FriendRights(b.buddy_rights_has), ) }); } } let count = i32::try_from(friends.len()).unwrap_or(i32::MAX); drop(friends); self.events.ready.emit(FriendsReadyEventArgs { count }); } fn handle_im(&self, e: crate::InstantMessageEventArgs) { let im = e.im(); match im.dialog { InstantMessageDialog::FriendshipOffered => { let mut requests = write(&self.requests); if requests.len() < MAX_FRIENDS || requests.contains_key(&im.from_agent_id) { requests.insert(im.from_agent_id, im.im_session_id); } drop(requests); self.events.offered.emit(FriendshipOfferedEventArgs { agent_id: im.from_agent_id, agent_name: im.from_agent_name, im_session_id: im.im_session_id, }); } InstantMessageDialog::FriendshipAccepted => { let mut f = FriendInfo::native_new( im.from_agent_id, FriendRights::CAN_SEE_ONLINE, FriendRights::CAN_SEE_ONLINE, ); f.name.clone_from(&im.from_agent_name); f.online = true; write(&self.friends).insert(f.id, f.clone()); self.events.response.emit(FriendshipResponseEventArgs { agent_id: f.id, agent_name: f.name, accepted: true, }); } InstantMessageDialog::FriendshipDeclined => { self.events.response.emit(FriendshipResponseEventArgs { agent_id: im.from_agent_id, agent_name: im.from_agent_name, accepted: false, }); } _ => {} } } #[allow( clippy::too_many_lines, clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss )] fn handle_packet(&self, e: RawPacketReceivedEventArgs) { if self.disposed.load(Ordering::Acquire) || e.data.len() > 8 * 1024 * 1024 { return; } let mut pos = 0; match e.packet_type { PacketType::OnlineNotification | PacketType::OfflineNotification => { let online = e.packet_type == PacketType::OnlineNotification; let ids = if online { crate::packets::OnlineNotificationPacket::new_from_bytes(&e.data, &mut pos).map( |p| { p.agent_block .into_iter() .map(|b| b.agent_id) .collect::>() }, ) } else { crate::packets::OfflineNotificationPacket::new_from_bytes(&e.data, &mut pos) .map(|p| { p.agent_block .into_iter() .map(|b| b.agent_id) .collect::>() }) }; if let Ok(ids) = ids { for id in ids.into_iter().take(MAX_FRIENDS) { let event = { let mut fs = write(&self.friends); let f = fs.entry(id).or_insert_with(|| { FriendInfo::native_new( id, FriendRights::CAN_SEE_ONLINE, FriendRights::CAN_SEE_ONLINE, ) }); let changed = f.online != online; f.online = online; changed.then(|| f.clone()) }; if let Some(friend) = event { if online { self.events.online.emit(FriendInfoEventArgs { friend }); } else { self.events.offline.emit(FriendInfoEventArgs { friend }); } } } } } PacketType::TerminateFriendship => { if let Ok(p) = crate::packets::TerminateFriendshipPacket::new_from_bytes(&e.data, &mut pos) { let id = p.ex_block.other_id; let name = write(&self.friends) .remove(&id) .map_or_else(String::new, |f| f.name); self.events.terminated.emit(FriendshipTerminatedEventArgs { agent_id: id, agent_name: name, }); } } PacketType::ChangeUserRights => { if let Ok(p) = crate::packets::ChangeUserRightsPacket::new_from_bytes(&e.data, &mut pos) { let self_id = { let mut client = (*self.client).clone(); client.self_().agent_id() }; for b in p.rights.into_iter().take(MAX_FRIENDS) { let id = if b.agent_related == self_id { p.agent_data.agent_id } else { b.agent_related }; let event = { let mut fs = write(&self.friends); fs.get_mut(&id).map(|f| { if b.agent_related == id { f.their_rights = FriendRights(b.related_rights); } else { f.my_rights = FriendRights(b.related_rights); } f.clone() }) }; if let Some(friend) = event { self.events.rights.emit(FriendInfoEventArgs { friend }); } } } } PacketType::FindAgent => { if let Ok(p) = crate::packets::FindAgentPacket::new_from_bytes(&e.data, &mut pos) && let Some(loc) = p.location_block.first() { let gx = loc.global_x.max(0.0) as u32; let gy = loc.global_y.max(0.0) as u32; let handle = (u64::from(gx & !255) << 32) | u64::from(gy & !255); self.events.found.emit(FriendFoundReplyEventArgs { agent_id: p.agent_block.prey, region_handle: handle, location: Vector3 { x: (gx & 255) as f32, y: (gy & 255) as f32, z: 0.0, }, }); } } PacketType::OfferCallingCard => { if let Ok(p) = crate::packets::OfferCallingCardPacket::new_from_bytes(&e.data, &mut pos) { let id = p.agent_data.agent_id; let name = read(&self.friends).get(&id).map_or_else( || id.to_string(), |f| { if f.name.is_empty() { id.to_string() } else { f.name.clone() } }, ); self.events.calling_card.emit(CallingCardOfferedEventArgs { agent_id: id, agent_name: name, transaction_id: p.agent_block.transaction_id, }); } } PacketType::UUIDNameReply => { if let Ok(p) = crate::packets::UUIDNameReplyPacket::new_from_bytes(&e.data, &mut pos) { let mut changed = HashMap::new(); { let mut fs = write(&self.friends); for b in p.uuid_name_block.into_iter().take(MAX_FRIENDS) { if let Some(f) = fs.get_mut(&b.id) { let name = format!( "{} {}", wire_string(&b.first_name), wire_string(&b.last_name) ) .trim() .to_owned(); if f.name != name { f.name.clone_from(&name); changed.insert(b.id, name); } } } } if !changed.is_empty() { self.events .names .emit(FriendNamesEventArgs { names: changed }); } } } _ => {} } } } #[cfg(test)] mod tests { use super::*; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; #[test] fn rights_enforce_online_before_map() { let mut f = FriendInfo::native_new(UUID::zero(), FriendRights::NONE, FriendRights::NONE); f.set_can_see_me_on_map(true); assert!(!f.can_see_me_on_map()); f.set_can_see_me_online(true); f.set_can_see_me_on_map(true); assert!(f.can_see_me_on_map()); f.set_can_see_me_online(false); assert!(!f.can_see_me_on_map()); } #[test] fn presence_packet_updates_state_before_callback() { let client = GridClient::new().unwrap(); let manager = FriendsManager::native_new(Arc::new(client.clone())).unwrap(); let friend_id = UUID::new_with_string("77777777-7777-7777-7777-777777777777".into()).unwrap(); write(&manager.inner.friends).insert( friend_id, FriendInfo::native_new(friend_id, FriendRights::NONE, FriendRights::NONE), ); let callback_manager = manager.clone(); let observed = Arc::new(AtomicBool::new(false)); let observed_copy = Arc::clone(&observed); let _subscription = manager.subscribe_friend_online(Arc::new(move |event| { assert!(callback_manager.friend_list()[&event.friend().uuid()].is_online()); observed_copy.store(true, Ordering::Release); })); let mut packet = crate::packets::OnlineNotificationPacket::new_with_constructor().unwrap(); let mut row = crate::packets::OnlineNotificationPacketAgentBlockBlock::new_with_constructor() .unwrap(); row.agent_id = friend_id; packet.agent_block = vec![row]; let simulator = crate::Simulator::native_new( client, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 13973), 0, Some(256), Some(256), ) .unwrap(); manager.inner.handle_packet(RawPacketReceivedEventArgs { packet_type: PacketType::OnlineNotification, data: packet.encode_packet().unwrap(), simulator, }); assert!(observed.load(Ordering::Acquire)); } }