Implement social and group managers (#72)
All checks were successful
Native code generation / deterministic (push) Successful in 15m31s
Imaging and meshing gate / native (push) Successful in 5m21s
Native Rust workspace compile / compile (push) Successful in 5m17s

This commit is contained in:
2026-08-10 18:35:09 +00:00
parent 66fbb87b3f
commit c22eef5d0e
8 changed files with 2852 additions and 1345 deletions

View File

@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
| Assembly | Types | Members | Status | | Assembly | Types | Members | Status |
|---|---:|---:|---| |---|---:|---:|---|
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 326 types / 16,464 members; remaining surface is callable failure-only shims | | `LibreMetaverse` | 2,711 | 27,281 | native implementation: 351 types / 16,636 members; remaining surface is callable failure-only shims |
| `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain | | `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain |
| `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain |
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |

View File

@@ -126,6 +126,8 @@ struct ClientRuntime {
grid_manager: Mutex<Option<Arc<crate::grid_manager::GridManagerInner>>>, grid_manager: Mutex<Option<Arc<crate::grid_manager::GridManagerInner>>>,
directory_manager: Mutex<Option<Arc<crate::directory_manager::DirectoryManagerInner>>>, directory_manager: Mutex<Option<Arc<crate::directory_manager::DirectoryManagerInner>>>,
interest_list_manager: Mutex<Option<Arc<crate::interest_list::InterestListManagerInner>>>, interest_list_manager: Mutex<Option<Arc<crate::interest_list::InterestListManagerInner>>>,
friends_manager: Mutex<Option<Arc<crate::friends_manager::FriendsManagerInner>>>,
group_manager: Mutex<Option<Arc<crate::group_manager::GroupManagerInner>>>,
appearance_manager: Mutex<Option<Arc<crate::appearance_manager::AppearanceManagerInner>>>, appearance_manager: Mutex<Option<Arc<crate::appearance_manager::AppearanceManagerInner>>>,
avatar_manager: Mutex<Option<Arc<crate::avatar_manager::AvatarManagerInner>>>, avatar_manager: Mutex<Option<Arc<crate::avatar_manager::AvatarManagerInner>>>,
animesh_manager: Mutex<Option<Arc<crate::animesh_runtime::AnimeshManagerInner>>>, animesh_manager: Mutex<Option<Arc<crate::animesh_runtime::AnimeshManagerInner>>>,
@@ -168,6 +170,8 @@ impl ClientRuntime {
grid_manager: Mutex::new(None), grid_manager: Mutex::new(None),
directory_manager: Mutex::new(None), directory_manager: Mutex::new(None),
interest_list_manager: Mutex::new(None), interest_list_manager: Mutex::new(None),
friends_manager: Mutex::new(None),
group_manager: Mutex::new(None),
appearance_manager: Mutex::new(None), appearance_manager: Mutex::new(None),
avatar_manager: Mutex::new(None), avatar_manager: Mutex::new(None),
animesh_manager: Mutex::new(None), animesh_manager: Mutex::new(None),
@@ -270,6 +274,14 @@ impl ClientRuntime {
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) .unwrap_or_else(std::sync::PoisonError::into_inner)
.take(); .take();
self.friends_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.group_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.http_caps_client self.http_caps_client
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) .unwrap_or_else(std::sync::PoisonError::into_inner)
@@ -977,6 +989,82 @@ impl GridClient {
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner); .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
} }
pub(crate) fn cached_friends_manager_inner(
&self,
) -> Option<Arc<crate::friends_manager::FriendsManagerInner>> {
self.runtime
.friends_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_friends_manager_inner(
&self,
candidate: Arc<crate::friends_manager::FriendsManagerInner>,
) -> Arc<crate::friends_manager::FriendsManagerInner> {
let mut cached = self
.runtime
.friends_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_friends(&self) -> Result<crate::FriendsManager, crate::Error> {
if let Some(inner) = self.cached_friends_manager_inner() {
Ok(crate::FriendsManager { inner })
} else {
crate::friends_manager::FriendsManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_friends(&self, value: crate::FriendsManager) {
*self
.runtime
.friends_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_group_manager_inner(
&self,
) -> Option<Arc<crate::group_manager::GroupManagerInner>> {
self.runtime
.group_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_group_manager_inner(
&self,
candidate: Arc<crate::group_manager::GroupManagerInner>,
) -> Arc<crate::group_manager::GroupManagerInner> {
let mut cached = self
.runtime
.group_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_groups(&self) -> crate::GroupManager {
if let Some(inner) = self.cached_group_manager_inner() {
crate::GroupManager { inner }
} else {
crate::group_manager::GroupManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_groups(&self, value: crate::GroupManager) {
*self
.runtime
.group_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn native_set_caps_rate_limiter( pub(crate) fn native_set_caps_rate_limiter(
&mut self, &mut self,

View File

@@ -0,0 +1,886 @@
//! 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<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
value
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn read<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
value
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write<T>(value: &RwLock<T>) -> 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<Self, Error> { 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<UUID, String> => 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<CallingCardOfferedEventArgs>,
found: EventRegistry<FriendFoundReplyEventArgs>,
names: EventRegistry<FriendNamesEventArgs>,
offline: EventRegistry<FriendInfoEventArgs>,
online: EventRegistry<FriendInfoEventArgs>,
rights: EventRegistry<FriendInfoEventArgs>,
offered: EventRegistry<FriendshipOfferedEventArgs>,
response: EventRegistry<FriendshipResponseEventArgs>,
terminated: EventRegistry<FriendshipTerminatedEventArgs>,
ready: EventRegistry<FriendsReadyEventArgs>,
}
pub(crate) struct FriendsManagerInner {
client: Arc<GridClient>,
friends: RwLock<HashMap<UUID, FriendInfo>>,
requests: RwLock<HashMap<UUID, UUID>>,
events: FriendEvents,
subscriptions: Mutex<Vec<Subscription>>,
login_callback: Mutex<Option<crate::NetworkManagerLoginResponseCallback>>,
disposed: AtomicBool,
}
#[derive(Clone)]
pub struct FriendsManager {
pub(crate) inner: Arc<FriendsManagerInner>,
}
impl FriendsManager {
pub(crate) fn native_new(client: Arc<GridClient>) -> Result<Self, Error> {
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<T: GeneratedPacket>(&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<UUID, FriendInfo> {
read(&self.inner.friends).clone()
}
pub fn friend_requests(&self) -> HashMap<UUID, UUID> {
read(&self.inner.requests).clone()
}
pub fn dispose(&self) -> Result<(), Error> {
self.inner.dispose();
Ok(())
}
pub fn subscribe_calling_card_offered(
&self,
h: EventHandler<CallingCardOfferedEventArgs>,
) -> Subscription {
self.inner.events.calling_card.subscribe(h)
}
pub fn subscribe_friend_found_reply(
&self,
h: EventHandler<FriendFoundReplyEventArgs>,
) -> Subscription {
self.inner.events.found.subscribe(h)
}
pub fn subscribe_friend_names(&self, h: EventHandler<FriendNamesEventArgs>) -> Subscription {
self.inner.events.names.subscribe(h)
}
pub fn subscribe_friend_offline(&self, h: EventHandler<FriendInfoEventArgs>) -> Subscription {
self.inner.events.offline.subscribe(h)
}
pub fn subscribe_friend_online(&self, h: EventHandler<FriendInfoEventArgs>) -> Subscription {
self.inner.events.online.subscribe(h)
}
pub fn subscribe_friend_rights_update(
&self,
h: EventHandler<FriendInfoEventArgs>,
) -> Subscription {
self.inner.events.rights.subscribe(h)
}
pub fn subscribe_friendship_offered(
&self,
h: EventHandler<FriendshipOfferedEventArgs>,
) -> Subscription {
self.inner.events.offered.subscribe(h)
}
pub fn subscribe_friendship_response(
&self,
h: EventHandler<FriendshipResponseEventArgs>,
) -> Subscription {
self.inner.events.response.subscribe(h)
}
pub fn subscribe_friendship_terminated(
&self,
h: EventHandler<FriendshipTerminatedEventArgs>,
) -> Subscription {
self.inner.events.terminated.subscribe(h)
}
pub fn subscribe_friends_list_ready(
&self,
h: EventHandler<FriendsReadyEventArgs>,
) -> 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<CancellationToken>,
) -> 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<CancellationToken>,
) -> 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<CancellationToken>,
) -> 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<libremetaverse_types::compat::Object>,
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<crate::LoginResponseData>) {
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::<String>()) {
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::<Vec<_>>()
},
)
} else {
crate::packets::OfflineNotificationPacket::new_from_bytes(&e.data, &mut pos)
.map(|p| {
p.agent_block
.into_iter()
.map(|b| b.agent_id)
.collect::<Vec<_>>()
})
};
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));
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,9 @@ mod current_outfit;
mod download_manager; mod download_manager;
mod environment_manager; mod environment_manager;
mod event_queue; mod event_queue;
mod friends_manager;
mod gesture; mod gesture;
mod group_manager;
mod initial_outfit; mod initial_outfit;
mod inventory; mod inventory;
mod inventory_ais; mod inventory_ais;

9
docs/social.md Normal file
View File

@@ -0,0 +1,9 @@
# Social managers
`GridClient::friends()` and `GridClient::groups()` are client-owned native Rust managers. Repeated property access returns handles to the same synchronized state, and client shutdown releases packet, CAPS, login, and instant-message subscriptions.
The friendship manager maintains the login buddy list, presence, names, bidirectional rights, pending offers, calling cards, map requests, and friendship lifecycle operations. Friendship acceptance and rejection use the simulator capabilities when requested and retain LLUDP fallbacks. Capability calls honor the supplied cancellation token and reject oversized or malformed responses.
The group manager maintains current memberships and supports profiles, titles, members, roles, role-member mappings, invitations, notices, account summaries, proposals, bans, and membership mutations. Fragmented reply packets are correlated by request UUID, bounded, and deduplicated before one completed event is emitted. State is committed and locks are released before callbacks run.
Group-chat session membership and offline instant-message retrieval remain part of `AgentManager`, because both are instant-message session concerns. Session CAPS events update membership before callbacks, and teardown removes session state deterministically. Social message debug output reports metadata and lengths rather than message bodies, binary buckets, or capability URLs.

View File

@@ -182,6 +182,31 @@ NATIVE_TYPES = {
"T:LibreMetaverse.PlacesReplyEventArgs": "crate::directory_manager::PlacesReplyEventArgs", "T:LibreMetaverse.PlacesReplyEventArgs": "crate::directory_manager::PlacesReplyEventArgs",
"T:LibreMetaverse.InterestListManager": "crate::interest_list::InterestListManager", "T:LibreMetaverse.InterestListManager": "crate::interest_list::InterestListManager",
"T:LibreMetaverse.Messages.Linden.InterestListMessage": "crate::interest_list::InterestListMessage", "T:LibreMetaverse.Messages.Linden.InterestListMessage": "crate::interest_list::InterestListMessage",
"T:LibreMetaverse.CallingCardOfferedEventArgs": "crate::friends_manager::CallingCardOfferedEventArgs",
"T:LibreMetaverse.FriendFoundReplyEventArgs": "crate::friends_manager::FriendFoundReplyEventArgs",
"T:LibreMetaverse.FriendInfo": "crate::friends_manager::FriendInfo",
"T:LibreMetaverse.FriendInfoEventArgs": "crate::friends_manager::FriendInfoEventArgs",
"T:LibreMetaverse.FriendNamesEventArgs": "crate::friends_manager::FriendNamesEventArgs",
"T:LibreMetaverse.FriendsManager": "crate::friends_manager::FriendsManager",
"T:LibreMetaverse.FriendsReadyEventArgs": "crate::friends_manager::FriendsReadyEventArgs",
"T:LibreMetaverse.FriendshipOfferedEventArgs": "crate::friends_manager::FriendshipOfferedEventArgs",
"T:LibreMetaverse.FriendshipResponseEventArgs": "crate::friends_manager::FriendshipResponseEventArgs",
"T:LibreMetaverse.FriendshipTerminatedEventArgs": "crate::friends_manager::FriendshipTerminatedEventArgs",
"T:LibreMetaverse.BannedAgentsEventArgs": "crate::group_manager::BannedAgentsEventArgs",
"T:LibreMetaverse.CurrentGroupsEventArgs": "crate::group_manager::CurrentGroupsEventArgs",
"T:LibreMetaverse.GroupAccountSummaryReplyEventArgs": "crate::group_manager::GroupAccountSummaryReplyEventArgs",
"T:LibreMetaverse.GroupCreatedReplyEventArgs": "crate::group_manager::GroupCreatedReplyEventArgs",
"T:LibreMetaverse.GroupDroppedEventArgs": "crate::group_manager::GroupDroppedEventArgs",
"T:LibreMetaverse.GroupInvitationEventArgs": "crate::group_manager::GroupInvitationEventArgs",
"T:LibreMetaverse.GroupManager": "crate::group_manager::GroupManager",
"T:LibreMetaverse.GroupMembersReplyEventArgs": "crate::group_manager::GroupMembersReplyEventArgs",
"T:LibreMetaverse.GroupNamesEventArgs": "crate::group_manager::GroupNamesEventArgs",
"T:LibreMetaverse.GroupNoticesListReplyEventArgs": "crate::group_manager::GroupNoticesListReplyEventArgs",
"T:LibreMetaverse.GroupOperationEventArgs": "crate::group_manager::GroupOperationEventArgs",
"T:LibreMetaverse.GroupProfileEventArgs": "crate::group_manager::GroupProfileEventArgs",
"T:LibreMetaverse.GroupRolesDataReplyEventArgs": "crate::group_manager::GroupRolesDataReplyEventArgs",
"T:LibreMetaverse.GroupRolesMembersReplyEventArgs": "crate::group_manager::GroupRolesMembersReplyEventArgs",
"T:LibreMetaverse.GroupTitlesReplyEventArgs": "crate::group_manager::GroupTitlesReplyEventArgs",
"T:LibreMetaverse.TeleportEventArgs": "crate::agent_movement::TeleportEventArgs", "T:LibreMetaverse.TeleportEventArgs": "crate::agent_movement::TeleportEventArgs",
"T:LibreMetaverse.TerrainCompressor": "crate::terrain_codec::TerrainCompressor", "T:LibreMetaverse.TerrainCompressor": "crate::terrain_codec::TerrainCompressor",
"T:LibreMetaverse.TerrainPatch": "crate::terrain_codec::TerrainPatch", "T:LibreMetaverse.TerrainPatch": "crate::terrain_codec::TerrainPatch",
@@ -446,6 +471,17 @@ NATIVE_MEMBER_BODIES = {
"P:LibreMetaverse.GridClient.Directory#set": "self.native_set_directory(value)", "P:LibreMetaverse.GridClient.Directory#set": "self.native_set_directory(value)",
"P:LibreMetaverse.GridClient.InterestList": "self.native_interest_list().unwrap_or_else(|_| panic!(\"failed to construct InterestListManager\"))", "P:LibreMetaverse.GridClient.InterestList": "self.native_interest_list().unwrap_or_else(|_| panic!(\"failed to construct InterestListManager\"))",
"P:LibreMetaverse.GridClient.InterestList#set": "self.native_set_interest_list(value)", "P:LibreMetaverse.GridClient.InterestList#set": "self.native_set_interest_list(value)",
"P:LibreMetaverse.GridClient.Friends": "self.native_friends().unwrap_or_else(|_| panic!(\"failed to construct FriendsManager\"))",
"P:LibreMetaverse.GridClient.Friends#set": "self.native_set_friends(value)",
"P:LibreMetaverse.GridClient.Groups": "self.native_groups()",
"P:LibreMetaverse.GridClient.Groups#set": "self.native_set_groups(value)",
"M:LibreMetaverse.Group.Equals(LibreMetaverse.Group)": "self.native_equals_group(&other)",
"M:LibreMetaverse.Group.Equals(System.Object)": "self.native_equals_object(obj)",
"M:LibreMetaverse.Group.GetHashCode": "self.native_hash_code()",
"M:LibreMetaverse.Group.ToString": "self.native_to_string()",
"M:LibreMetaverse.GroupNotice.SerializeAttachment": "self.native_serialize_attachment()",
"M:LibreMetaverse.GroupRole.ToString": "self.native_to_string()",
"M:LibreMetaverse.GroupTitle.ToString": "self.native_to_string()",
"M:LibreMetaverse.Simulator.IsParcelMapFull": "Ok(self.native_is_parcel_map_full())", "M:LibreMetaverse.Simulator.IsParcelMapFull": "Ok(self.native_is_parcel_map_full())",
"M:LibreMetaverse.Simulator.TerrainHeightAtPoint(System.Int32,System.Int32,System.Single@)": "self.native_terrain_height_at_point(x, y, height)", "M:LibreMetaverse.Simulator.TerrainHeightAtPoint(System.Int32,System.Int32,System.Single@)": "self.native_terrain_height_at_point(x, y, height)",
"P:LibreMetaverse.Simulator.ParcelMap": "self.native_parcel_map_snapshot()", "P:LibreMetaverse.Simulator.ParcelMap": "self.native_parcel_map_snapshot()",
@@ -2699,6 +2735,14 @@ VALUE_DERIVES = {
"T:LibreMetaverse.ConnectionSettings": "Clone, Eq, PartialEq", "T:LibreMetaverse.ConnectionSettings": "Clone, Eq, PartialEq",
"T:LibreMetaverse.LoggingSettings": "Clone, Debug, Eq, PartialEq", "T:LibreMetaverse.LoggingSettings": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.HomeInfo": "Clone, Copy, Debug, PartialEq", "T:LibreMetaverse.HomeInfo": "Clone, Copy, Debug, PartialEq",
"T:LibreMetaverse.Group": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.GroupAccountSummary": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.GroupMember": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.GroupNotice": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.GroupNoticesListEntry": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.GroupProposal": "Clone, Debug, PartialEq",
"T:LibreMetaverse.GroupRole": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.GroupTitle": "Clone, Debug, Eq, PartialEq",
"T:LibreMetaverse.InventoryBase": "Clone, Debug", "T:LibreMetaverse.InventoryBase": "Clone, Debug",
"T:LibreMetaverse.InventoryFolder": "Clone, Debug", "T:LibreMetaverse.InventoryFolder": "Clone, Debug",
"T:LibreMetaverse.Matrix4": "Clone, Copy, Debug", "T:LibreMetaverse.Matrix4": "Clone, Copy, Debug",