//! Group membership, roles, notices, invitations, and profile synchronization. #![allow(clippy::missing_errors_doc)] #![allow(clippy::must_use_candidate)] #![allow(clippy::needless_pass_by_value)] use crate::agent_manager::EventRegistry; use crate::network_manager::RawPacketReceivedEventArgs; use crate::packet_catalog::{GeneratedPacket, PacketType}; use crate::{ Error, GridClient, Group, GroupAccountSummary, GroupBanAction, GroupMember, GroupNotice, GroupNoticesListEntry, GroupProposal, GroupRole, GroupRoleChangeType, GroupRoleUpdate, GroupTitle, InstantMessageDialog, }; use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser}; use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription}; use libremetaverse_types::{AssetType, UUID, Vector3}; use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::{Arc, Mutex, RwLock}; const MAX_GROUPS: usize = 10_000; const MAX_GROUP_ROWS: usize = 100_000; const MAX_PENDING_REQUESTS: usize = 4_096; 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() } macro_rules! event_args { ($name:ident { $($field:ident : $ty:ty => $getter:ident),+ $(,)? }) => { #[derive(Clone, Debug)] pub struct $name { $( $field: $ty ),+ } impl $name { pub fn new($($field: $ty),+) -> Result { Ok(Self { $($field),+ }) } $(pub fn $getter(&self) -> $ty { self.$field.clone() })+ } }; } event_args!(BannedAgentsEventArgs { group_id: UUID => group_id, success: bool => success, banned_agents: Option> => banned_agents }); event_args!(CurrentGroupsEventArgs { groups: HashMap => groups }); event_args!(GroupAccountSummaryReplyEventArgs { group_id: UUID => group_id, summary: GroupAccountSummary => summary }); event_args!(GroupCreatedReplyEventArgs { group_id: UUID => group_id, success: bool => success, message: String => message }); event_args!(GroupDroppedEventArgs { group_id: UUID => group_id }); event_args!(GroupMembersReplyEventArgs { request_id: UUID => request_id, group_id: UUID => group_id, members: HashMap => members }); event_args!(GroupNamesEventArgs { group_names: HashMap => group_names }); event_args!(GroupNoticesListReplyEventArgs { group_id: UUID => group_id, notices: Vec => notices }); event_args!(GroupOperationEventArgs { group_id: UUID => group_id, success: bool => success }); event_args!(GroupProfileEventArgs { group: Group => group }); event_args!(GroupRolesDataReplyEventArgs { request_id: UUID => request_id, group_id: UUID => group_id, roles: HashMap => roles }); event_args!(GroupRolesMembersReplyEventArgs { request_id: UUID => request_id, group_id: UUID => group_id, roles_members: Vec<(UUID, UUID)> => roles_members }); event_args!(GroupTitlesReplyEventArgs { request_id: UUID => request_id, group_id: UUID => group_id, titles: HashMap => titles }); #[derive(Clone)] pub struct GroupInvitationEventArgs { simulator: crate::Simulator, agent_id: UUID, from_name: String, message: String, accept: Arc, } impl GroupInvitationEventArgs { pub fn new( simulator: crate::Simulator, agent_id: UUID, agent_name: String, message: String, ) -> Result { Ok(Self { simulator, agent_id, from_name: agent_name, message, accept: Arc::new(AtomicBool::new(false)), }) } pub fn accept(&self) -> bool { self.accept.load(Ordering::Acquire) } pub fn set_accept(&mut self, value: bool) { self.accept.store(value, Ordering::Release); } pub fn agent_id(&self) -> UUID { self.agent_id } pub fn from_name(&self) -> String { self.from_name.clone() } pub fn message(&self) -> String { self.message.clone() } pub fn simulator(&self) -> crate::Simulator { self.simulator.clone() } } #[derive(Default)] struct GroupEvents { banned: EventRegistry, current: EventRegistry, account: EventRegistry, created: EventRegistry, dropped: EventRegistry, invitation: EventRegistry, joined: EventRegistry, left: EventRegistry, ejected: EventRegistry, members: EventRegistry, names: EventRegistry, notices: EventRegistry, profile: EventRegistry, roles: EventRegistry, role_members: EventRegistry, titles: EventRegistry, } #[derive(Default)] struct PendingRows { members: HashMap)>, roles: HashMap)>, role_members: HashMap, titles: HashMap, accounts: HashMap, } type PendingRoleMembers = (UUID, usize, Vec<(UUID, UUID)>, HashSet<(UUID, UUID)>); pub(crate) struct GroupManagerInner { client: Arc, groups: RwLock>, group_count: AtomicI32, pending: Mutex, events: GroupEvents, subscriptions: Mutex>, disposed: AtomicBool, } #[derive(Clone)] pub struct GroupManager { pub(crate) inner: Arc, } impl GroupManager { pub(crate) fn native_new(client: Arc) -> Self { if let Some(inner) = client.cached_group_manager_inner() { return Self { inner }; } let inner = Arc::new(GroupManagerInner { client: Arc::clone(&client), groups: RwLock::new(HashMap::new()), group_count: AtomicI32::new(-1), pending: Mutex::new(PendingRows::default()), events: GroupEvents::default(), subscriptions: Mutex::new(Vec::new()), disposed: AtomicBool::new(false), }); let weak = Arc::downgrade(&inner); mutex(&inner.subscriptions).push(client.network().subscribe_raw_packet(Arc::new( move |event| { if let Some(inner) = weak.upgrade() { inner.handle_packet(event); } }, ))); let weak = Arc::downgrade(&inner); mutex(&inner.subscriptions).push(client.network().subscribe_caps( "AgentGroupDataUpdate".into(), crate::CapsEventQueueCallback::from_handler(move |_, message, _| { let Some(inner) = weak.upgrade() else { return; }; let Ok(map) = message.serialize() else { return; }; let Ok(mut decoded) = crate::messages::linden::AgentGroupDataUpdateMessage::new() else { return; }; if decoded.deserialize(map).is_ok() { inner.handle_group_update_caps(decoded); } }), )); let weak = Arc::downgrade(&inner); mutex(&inner.subscriptions).push(client.network().subscribe_caps( "AgentDropGroup".into(), crate::CapsEventQueueCallback::from_handler(move |_, message, _| { let Some(inner) = weak.upgrade() else { return; }; let Ok(map) = message.serialize() else { return; }; let Ok(mut decoded) = crate::messages::linden::AgentDropGroupMessage::new() else { return; }; if decoded.deserialize(map).is_ok() { for row in decoded.agent_data_block.into_iter().take(MAX_GROUPS) { inner.drop_group(row.group_id); } } }), )); let weak = Arc::downgrade(&inner); let mut callback_client = (*client).clone(); mutex(&inner.subscriptions).push(callback_client.self_().subscribe_im(Arc::new( move |event| { if let Some(inner) = weak.upgrade() { inner.handle_im(event); } }, ))); Self { inner: client.install_group_manager_inner(inner), } } pub fn new(client: GridClient) -> Result { Ok(Self::native_new(Arc::new(client))) } fn ids(&self) -> (UUID, UUID) { let mut client = (*self.inner.client).clone(); let agent = client.self_(); (agent.agent_id(), agent.session_id()) } fn send(&self, packet: &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 = packet.encode_packet()?; match sim.native_send_packet_data( bytes.clone(), i32::try_from(bytes.len()).map_err(|_| Error::Argument)?, ty, false, ) { Err(Error::InvalidOperation) if !sim.connected() => Ok(()), result => result, } } pub fn dispose(&self) -> Result<(), Error> { self.inner.dispose(); Ok(()) } pub fn current_group_count(&self) -> i32 { self.inner.group_count.load(Ordering::Acquire) } /// Returns a stable snapshot of the groups currently reported for this agent. /// /// The cache is replaced atomically whenever an `AgentGroupDataUpdate` arrives, /// so callers never observe a partially decoded group list. #[must_use] pub fn current_groups(&self) -> HashMap { read(&self.inner.groups).clone() } pub fn group_membership_limit(&self) -> i32 { let mut client = (*self.inner.client).clone(); client.self_().benefits().group_membership_limit() } pub fn can_join_more_groups(&self) -> Option> { let count = self.current_group_count(); if count < 0 { Some(None) } else { Some(Some(count < self.group_membership_limit())) } } pub fn subscribe_banned_agents(&self, h: EventHandler) -> Subscription { self.inner.events.banned.subscribe(h) } pub fn subscribe_current_groups( &self, h: EventHandler, ) -> Subscription { self.inner.events.current.subscribe(h) } pub fn subscribe_group_account_summary_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.account.subscribe(h) } pub fn subscribe_group_created_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.created.subscribe(h) } pub fn subscribe_group_dropped(&self, h: EventHandler) -> Subscription { self.inner.events.dropped.subscribe(h) } pub fn subscribe_group_invitation( &self, h: EventHandler, ) -> Subscription { self.inner.events.invitation.subscribe(h) } pub fn subscribe_group_joined_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.joined.subscribe(h) } pub fn subscribe_group_leave_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.left.subscribe(h) } pub fn subscribe_group_member_ejected( &self, h: EventHandler, ) -> Subscription { self.inner.events.ejected.subscribe(h) } pub fn subscribe_group_members_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.members.subscribe(h) } pub fn subscribe_group_names_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.names.subscribe(h) } pub fn subscribe_group_notices_list_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.notices.subscribe(h) } pub fn subscribe_group_profile(&self, h: EventHandler) -> Subscription { self.inner.events.profile.subscribe(h) } pub fn subscribe_group_role_data_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.roles.subscribe(h) } pub fn subscribe_group_role_members_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.role_members.subscribe(h) } pub fn subscribe_group_titles_reply( &self, h: EventHandler, ) -> Subscription { self.inner.events.titles.subscribe(h) } } impl GroupManagerInner { fn dispose(&self) { if !self.disposed.swap(true, Ordering::AcqRel) { mutex(&self.subscriptions).clear(); mutex(&self.pending).members.clear(); mutex(&self.pending).roles.clear(); mutex(&self.pending).role_members.clear(); mutex(&self.pending).titles.clear(); mutex(&self.pending).accounts.clear(); } } fn drop_group(&self, group_id: UUID) { let count = { let mut groups = write(&self.groups); groups.remove(&group_id); groups.len() }; if self.group_count.load(Ordering::Acquire) >= 0 { self.group_count .store(i32::try_from(count).unwrap_or(i32::MAX), Ordering::Release); } self.events.dropped.emit(GroupDroppedEventArgs { group_id }); } fn handle_group_update_caps( &self, message: crate::messages::linden::AgentGroupDataUpdateMessage, ) { let mut groups = HashMap::new(); for (index, row) in message .group_data_block .into_iter() .take(MAX_GROUPS) .enumerate() { groups.insert( row.group_id, Group { accept_notices: row.accept_notices, allow_publish: false, charter: String::new(), contribution: row.contribution, founder_id: UUID::zero(), group_membership_count: 0, group_roles_count: 0, id: row.group_id, insignia_id: row.group_insignia_id, list_in_profile: message .new_group_data_block .get(index) .is_some_and(|item| item.list_in_profile), mature_publish: false, member_title: row.group_title, membership_fee: 0, money: 0, name: row.group_name, open_enrollment: false, owner_role: UUID::zero(), powers: row.group_powers, show_in_list: false, }, ); } write(&self.groups).clone_from(&groups); self.group_count.store( i32::try_from(groups.len()).unwrap_or(i32::MAX), Ordering::Release, ); self.events.current.emit(CurrentGroupsEventArgs { groups }); } fn handle_im(&self, event: crate::InstantMessageEventArgs) { let im = event.im(); if im.dialog != InstantMessageDialog::GroupInvitation { return; } let Some(simulator) = event.simulator() else { return; }; let args = GroupInvitationEventArgs { simulator, agent_id: im.from_agent_id, from_name: im.from_agent_name, message: im.message, accept: Arc::new(AtomicBool::new(false)), }; self.events.invitation.emit(args.clone()); let mut client = (*self.client).clone(); let _ = client.self_().native_group_invite_respond( im.from_agent_id, im.im_session_id, args.accept(), ); } #[allow(clippy::too_many_lines)] fn handle_packet(&self, event: RawPacketReceivedEventArgs) { if self.disposed.load(Ordering::Acquire) || event.data.len() > 8 * 1024 * 1024 { return; } let mut position = 0; match event.packet_type { PacketType::AgentGroupDataUpdate => { if let Ok(packet) = crate::packets::AgentGroupDataUpdatePacket::new_from_bytes( &event.data, &mut position, ) { let groups = packet .group_data .into_iter() .take(MAX_GROUPS) .map(|row| { let group = Group { accept_notices: row.accept_notices, allow_publish: false, charter: String::new(), contribution: row.contribution, founder_id: UUID::zero(), group_membership_count: 0, group_roles_count: 0, id: row.group_id, insignia_id: row.group_insignia_id, list_in_profile: false, mature_publish: false, member_title: String::new(), membership_fee: 0, money: 0, name: wire_string(&row.group_name), open_enrollment: false, owner_role: UUID::zero(), powers: crate::GroupPowers(row.group_powers), show_in_list: false, }; (group.id, group) }) .collect::>(); write(&self.groups).clone_from(&groups); self.group_count.store( i32::try_from(groups.len()).unwrap_or(i32::MAX), Ordering::Release, ); self.events.current.emit(CurrentGroupsEventArgs { groups }); } } PacketType::AgentDropGroup => { if let Ok(packet) = crate::packets::AgentDropGroupPacket::new_from_bytes(&event.data, &mut position) { self.drop_group(packet.agent_data.group_id); } } PacketType::GroupProfileReply => { if let Ok(packet) = crate::packets::GroupProfileReplyPacket::new_from_bytes( &event.data, &mut position, ) { let row = packet.group_data; let group = Group { accept_notices: false, allow_publish: row.allow_publish, charter: wire_string(&row.charter), contribution: 0, founder_id: row.founder_id, group_membership_count: row.group_membership_count, group_roles_count: row.group_roles_count, id: row.group_id, insignia_id: row.insignia_id, list_in_profile: false, mature_publish: row.mature_publish, member_title: wire_string(&row.member_title), membership_fee: row.membership_fee, money: row.money, name: wire_string(&row.name), open_enrollment: row.open_enrollment, owner_role: row.owner_role, powers: crate::GroupPowers(row.powers_mask), show_in_list: row.show_in_list, }; write(&self.groups).insert(group.id, group.clone()); self.events.profile.emit(GroupProfileEventArgs { group }); } } PacketType::UUIDGroupNameReply => { if let Ok(packet) = crate::packets::UUIDGroupNameReplyPacket::new_from_bytes( &event.data, &mut position, ) { let names = packet .uuid_name_block .into_iter() .take(MAX_GROUPS) .map(|row| (row.id, wire_string(&row.group_name))) .collect(); self.events .names .emit(GroupNamesEventArgs { group_names: names }); } } PacketType::GroupNoticesListReply => { if let Ok(packet) = crate::packets::GroupNoticesListReplyPacket::new_from_bytes( &event.data, &mut position, ) { let notices = packet .data .into_iter() .take(MAX_GROUP_ROWS) .map(|row| GroupNoticesListEntry { asset_type: asset_type(row.asset_type), from_name: wire_string(&row.from_name), has_attachment: row.has_attachment, notice_id: row.notice_id, subject: wire_string(&row.subject), timestamp: row.timestamp, }) .collect(); self.events.notices.emit(GroupNoticesListReplyEventArgs { group_id: packet.agent_data.group_id, notices, }); } } PacketType::GroupTitlesReply => { if let Ok(packet) = crate::packets::GroupTitlesReplyPacket::new_from_bytes( &event.data, &mut position, ) { let group_id = packet.agent_data.group_id; if mutex(&self.pending) .titles .remove(&packet.agent_data.request_id) != Some(group_id) { return; } let titles = packet .group_data .into_iter() .take(MAX_GROUP_ROWS) .map(|row| { ( row.role_id, GroupTitle { group_id, role_id: row.role_id, selected: row.selected, title: wire_string(&row.title), }, ) }) .collect(); self.events.titles.emit(GroupTitlesReplyEventArgs { request_id: packet.agent_data.request_id, group_id, titles, }); } } PacketType::GroupMembersReply => self.handle_members(&event.data, &mut position), PacketType::GroupRoleDataReply => self.handle_roles(&event.data, &mut position), PacketType::GroupRoleMembersReply => { self.handle_role_members(&event.data, &mut position); } PacketType::GroupAccountSummaryReply => self.handle_account(&event.data, &mut position), PacketType::CreateGroupReply => { if let Ok(packet) = crate::packets::CreateGroupReplyPacket::new_from_bytes( &event.data, &mut position, ) { self.events.created.emit(GroupCreatedReplyEventArgs { group_id: packet.reply_data.group_id, success: packet.reply_data.success, message: wire_string(&packet.reply_data.message), }); } } PacketType::JoinGroupReply => { if let Ok(packet) = crate::packets::JoinGroupReplyPacket::new_from_bytes(&event.data, &mut position) { self.events.joined.emit(GroupOperationEventArgs { group_id: packet.group_data.group_id, success: packet.group_data.success, }); } } PacketType::LeaveGroupReply => { if let Ok(packet) = crate::packets::LeaveGroupReplyPacket::new_from_bytes( &event.data, &mut position, ) { if packet.group_data.success { let count = { let mut groups = write(&self.groups); groups.remove(&packet.group_data.group_id); groups.len() }; if self.group_count.load(Ordering::Acquire) >= 0 { self.group_count .store(i32::try_from(count).unwrap_or(i32::MAX), Ordering::Release); } } self.events.left.emit(GroupOperationEventArgs { group_id: packet.group_data.group_id, success: packet.group_data.success, }); } } PacketType::EjectGroupMemberReply => { if let Ok(packet) = crate::packets::EjectGroupMemberReplyPacket::new_from_bytes( &event.data, &mut position, ) { self.events.ejected.emit(GroupOperationEventArgs { group_id: packet.group_data.group_id, success: packet.eject_data.success, }); } } _ => {} } } fn handle_members(&self, data: &[u8], position: &mut i32) { let Ok(packet) = crate::packets::GroupMembersReplyPacket::new_from_bytes(data, position) else { return; }; let Ok(expected) = usize::try_from(packet.group_data.member_count) else { return; }; if expected > MAX_GROUP_ROWS { mutex(&self.pending) .members .remove(&packet.group_data.request_id); return; } let complete = { let mut pending = mutex(&self.pending); let Some((group_id, count, rows)) = pending.members.get_mut(&packet.group_data.request_id) else { return; }; if *group_id != packet.group_data.group_id { return; } *count = expected; for row in packet.member_data.into_iter().take(MAX_GROUP_ROWS) { rows.entry(row.agent_id).or_insert_with(|| GroupMember { contribution: row.contribution, id: row.agent_id, is_owner: row.is_owner, online_status: wire_string(&row.online_status), powers: crate::GroupPowers(row.agent_powers), title: wire_string(&row.title), }); } if rows.len() >= *count { pending .members .remove(&packet.group_data.request_id) .map(|(_, _, rows)| rows) } else { None } }; if let Some(members) = complete { self.events.members.emit(GroupMembersReplyEventArgs { request_id: packet.group_data.request_id, group_id: packet.group_data.group_id, members, }); } } fn handle_roles(&self, data: &[u8], position: &mut i32) { let Ok(packet) = crate::packets::GroupRoleDataReplyPacket::new_from_bytes(data, position) else { return; }; let Ok(expected) = usize::try_from(packet.group_data.role_count) else { return; }; if expected > MAX_GROUP_ROWS { mutex(&self.pending) .roles .remove(&packet.group_data.request_id); return; } let complete = { let mut pending = mutex(&self.pending); let Some((group_id, count, rows)) = pending.roles.get_mut(&packet.group_data.request_id) else { return; }; if *group_id != packet.group_data.group_id { return; } *count = expected; for row in packet.role_data.into_iter().take(MAX_GROUP_ROWS) { rows.entry(row.role_id).or_insert_with(|| GroupRole { description: wire_string(&row.description), group_id: packet.group_data.group_id, id: row.role_id, members: row.members, name: wire_string(&row.name), powers: crate::GroupPowers(row.powers), title: wire_string(&row.title), }); } if rows.len() >= *count { pending .roles .remove(&packet.group_data.request_id) .map(|(_, _, rows)| rows) } else { None } }; if let Some(roles) = complete { self.events.roles.emit(GroupRolesDataReplyEventArgs { request_id: packet.group_data.request_id, group_id: packet.group_data.group_id, roles, }); } } fn handle_role_members(&self, data: &[u8], position: &mut i32) { let Ok(packet) = crate::packets::GroupRoleMembersReplyPacket::new_from_bytes(data, position) else { return; }; let Ok(expected) = usize::try_from(packet.agent_data.total_pairs) else { return; }; if expected > MAX_GROUP_ROWS { mutex(&self.pending) .role_members .remove(&packet.agent_data.request_id); return; } let complete = { let mut pending = mutex(&self.pending); let Some((group_id, count, rows, seen)) = pending.role_members.get_mut(&packet.agent_data.request_id) else { return; }; if *group_id != packet.agent_data.group_id { return; } *count = expected; for row in packet.member_data.into_iter().take(MAX_GROUP_ROWS) { let pair = (row.role_id, row.member_id); if seen.insert(pair) { rows.push(pair); } } if rows.len() >= *count { pending .role_members .remove(&packet.agent_data.request_id) .map(|(_, _, rows, _)| rows) } else { None } }; if let Some(roles_members) = complete { self.events .role_members .emit(GroupRolesMembersReplyEventArgs { request_id: packet.agent_data.request_id, group_id: packet.agent_data.group_id, roles_members, }); } } fn handle_account(&self, data: &[u8], position: &mut i32) { let Ok(packet) = crate::packets::GroupAccountSummaryReplyPacket::new_from_bytes(data, position) else { return; }; let row = packet.money_data; if mutex(&self.pending).accounts.remove(&row.request_id) != Some(packet.agent_data.group_id) { return; } let summary = GroupAccountSummary { balance: row.balance, current_interval: row.current_interval, group_tax_current: row.group_tax_current, group_tax_estimate: row.group_tax_estimate, interval_days: row.interval_days, land_tax_current: row.land_tax_current, land_tax_estimate: row.land_tax_estimate, last_tax_date: wire_string(&row.last_tax_date), light_tax_current: row.light_tax_current, light_tax_estimate: row.light_tax_estimate, non_exempt_members: row.non_exempt_members, object_tax_current: row.object_tax_current, object_tax_estimate: row.object_tax_estimate, parcel_dir_fee_current: row.parcel_dir_fee_current, parcel_dir_fee_estimate: row.parcel_dir_fee_estimate, start_date: wire_string(&row.start_date), tax_date: wire_string(&row.tax_date), total_credits: row.total_credits, total_debits: row.total_debits, }; self.events.account.emit(GroupAccountSummaryReplyEventArgs { group_id: packet.agent_data.group_id, summary, }); } } fn asset_type(value: u8) -> AssetType { match value.cast_signed() { 0 => AssetType::Texture, 1 => AssetType::Sound, 2 => AssetType::CallingCard, 3 => AssetType::Landmark, 4 => AssetType::Script, 5 => AssetType::Clothing, 6 => AssetType::Object, 7 => AssetType::Notecard, 8 => AssetType::Folder, 10 => AssetType::LSLText, 11 => AssetType::LSLBytecode, 12 => AssetType::TextureTGA, 13 => AssetType::Bodypart, 17 => AssetType::SoundWAV, 18 => AssetType::ImageTGA, 19 => AssetType::ImageJPEG, 20 => AssetType::Animation, 21 => AssetType::Gesture, 22 => AssetType::Simstate, 24 => AssetType::Link, 25 => AssetType::LinkFolder, 40 => AssetType::Widget, 45 => AssetType::Person, 49 => AssetType::Mesh, 56 => AssetType::Settings, 57 => AssetType::Material, _ => AssetType::Unknown, } } impl GroupManager { pub fn request_current_groups(&self) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::AgentDataUpdateRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; self.send(&packet, PacketType::AgentDataUpdateRequest) } pub fn request_group_name(&self, group_id: UUID) -> Result<(), Error> { self.request_group_names(vec![group_id]) } pub fn request_group_names(&self, group_i_ds: Vec) -> Result<(), Error> { if group_i_ds.is_empty() { return Ok(()); } if group_i_ds.len() > MAX_GROUPS { return Err(Error::Argument); } let mut packet = crate::packets::UUIDGroupNameRequestPacket::new_with_constructor()?; packet.uuid_name_block = group_i_ds .into_iter() .map(|id| crate::packets::UUIDGroupNameRequestPacketUUIDNameBlockBlock { id }) .collect(); self.send(&packet, PacketType::UUIDGroupNameRequest) } pub fn request_group_profile(&self, group: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::GroupProfileRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.group_id = group; self.send(&packet, PacketType::GroupProfileRequest) } pub fn request_group_members(&self, group: UUID) -> Result { let request_id = UUID::random()?; let (agent, session) = self.ids(); { let mut pending = mutex(&self.inner.pending); if pending.members.len() >= MAX_PENDING_REQUESTS { return Err(Error::InvalidOperation); } pending .members .insert(request_id, (group, usize::MAX, HashMap::new())); } let mut packet = crate::packets::GroupMembersRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.group_id = group; packet.group_data.request_id = request_id; if let Err(error) = self.send(&packet, PacketType::GroupMembersRequest) { mutex(&self.inner.pending).members.remove(&request_id); return Err(error); } Ok(request_id) } pub fn request_group_roles(&self, group: UUID) -> Result { let request_id = UUID::random()?; let (agent, session) = self.ids(); { let mut pending = mutex(&self.inner.pending); if pending.roles.len() >= MAX_PENDING_REQUESTS { return Err(Error::InvalidOperation); } pending .roles .insert(request_id, (group, usize::MAX, HashMap::new())); } let mut packet = crate::packets::GroupRoleDataRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.group_id = group; packet.group_data.request_id = request_id; if let Err(error) = self.send(&packet, PacketType::GroupRoleDataRequest) { mutex(&self.inner.pending).roles.remove(&request_id); return Err(error); } Ok(request_id) } pub fn request_group_roles_members(&self, group: UUID) -> Result { let request_id = UUID::random()?; let (agent, session) = self.ids(); { let mut pending = mutex(&self.inner.pending); if pending.role_members.len() >= MAX_PENDING_REQUESTS { return Err(Error::InvalidOperation); } pending .role_members .insert(request_id, (group, usize::MAX, Vec::new(), HashSet::new())); } let mut packet = crate::packets::GroupRoleMembersRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.group_id = group; packet.group_data.request_id = request_id; if let Err(error) = self.send(&packet, PacketType::GroupRoleMembersRequest) { mutex(&self.inner.pending).role_members.remove(&request_id); return Err(error); } Ok(request_id) } pub fn request_group_titles(&self, group: UUID) -> Result { let request_id = UUID::random()?; let (agent, session) = self.ids(); { let mut pending = mutex(&self.inner.pending); if pending.titles.len() >= MAX_PENDING_REQUESTS { return Err(Error::InvalidOperation); } pending.titles.insert(request_id, group); } let mut packet = crate::packets::GroupTitlesRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.agent_data.group_id = group; packet.agent_data.request_id = request_id; if let Err(error) = self.send(&packet, PacketType::GroupTitlesRequest) { mutex(&self.inner.pending).titles.remove(&request_id); return Err(error); } Ok(request_id) } pub fn request_group_account_summary( &self, group: UUID, interval_days: i32, current_interval: i32, ) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::GroupAccountSummaryRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.agent_data.group_id = group; let request_id = UUID::random()?; { let mut pending = mutex(&self.inner.pending); if pending.accounts.len() >= MAX_PENDING_REQUESTS { return Err(Error::InvalidOperation); } pending.accounts.insert(request_id, group); } packet.money_data.request_id = request_id; packet.money_data.current_interval = current_interval; packet.money_data.interval_days = interval_days; if let Err(error) = self.send(&packet, PacketType::GroupAccountSummaryRequest) { mutex(&self.inner.pending).accounts.remove(&request_id); Err(error) } else { Ok(()) } } pub fn request_group_notices_list(&self, group: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::GroupNoticesListRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.data.group_id = group; self.send(&packet, PacketType::GroupNoticesListRequest) } pub fn request_group_notice(&self, notice_id: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::GroupNoticeRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.data.group_notice_id = notice_id; self.send(&packet, PacketType::GroupNoticeRequest) } pub fn activate_group(&self, id: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::ActivateGroupPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.agent_data.group_id = id; self.send(&packet, PacketType::ActivateGroup) } pub fn activate_title(&self, group: UUID, role: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::GroupTitleUpdatePacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.agent_data.group_id = group; packet.agent_data.title_role_id = role; self.send(&packet, PacketType::GroupTitleUpdate) } pub fn set_group_contribution(&self, group: UUID, contribution: i32) -> Result<(), Error> { if contribution < 0 { return Err(Error::Argument); } let (agent, session) = self.ids(); let mut packet = crate::packets::SetGroupContributionPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.data.group_id = group; packet.data.contribution = contribution; self.send(&packet, PacketType::SetGroupContribution) } pub fn set_group_accept_notices( &self, group_id: UUID, accept_notices: bool, list_in_profile: bool, ) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::SetGroupAcceptNoticesPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.data.group_id = group_id; packet.data.accept_notices = accept_notices; packet.new_data.list_in_profile = list_in_profile; self.send(&packet, PacketType::SetGroupAcceptNotices) } pub fn request_join_group(&self, id: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::JoinGroupRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.group_id = id; self.send(&packet, PacketType::JoinGroupRequest) } pub fn leave_group(&self, group_id: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::LeaveGroupRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.group_id = group_id; self.send(&packet, PacketType::LeaveGroupRequest) } pub fn invite( &self, group: UUID, roles: Vec, requested_agent: UUID, ) -> Result<(), Error> { if roles.is_empty() || roles.len() > 256 { return Err(Error::Argument); } let (agent, session) = self.ids(); let mut packet = crate::packets::InviteGroupRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.group_id = group; packet.invite_data = roles .into_iter() .map( |role_id| crate::packets::InviteGroupRequestPacketInviteDataBlock { invitee_id: requested_agent, role_id, }, ) .collect(); self.send(&packet, PacketType::InviteGroupRequest) } pub fn request_create_group(&self, group: Group) -> Result<(), Error> { validate_group(&group)?; let (agent, session) = self.ids(); let mut packet = crate::packets::CreateGroupRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.allow_publish = group.allow_publish; packet.group_data.charter = group.charter.into_bytes(); packet.group_data.insignia_id = group.insignia_id; packet.group_data.mature_publish = group.mature_publish; packet.group_data.membership_fee = group.membership_fee; packet.group_data.name = group.name.into_bytes(); packet.group_data.open_enrollment = group.open_enrollment; packet.group_data.show_in_list = group.show_in_list; self.send(&packet, PacketType::CreateGroupRequest) } pub fn update_group(&self, id: UUID, group: Group) -> Result<(), Error> { validate_group(&group)?; let (agent, session) = self.ids(); let mut packet = crate::packets::UpdateGroupInfoPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.group_id = id; packet.group_data.allow_publish = group.allow_publish; packet.group_data.charter = group.charter.into_bytes(); packet.group_data.insignia_id = group.insignia_id; packet.group_data.mature_publish = group.mature_publish; packet.group_data.membership_fee = group.membership_fee; packet.group_data.open_enrollment = group.open_enrollment; packet.group_data.show_in_list = group.show_in_list; self.send(&packet, PacketType::UpdateGroupInfo) } pub fn eject_user(&self, group: UUID, member: UUID) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::EjectGroupMemberRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.group_data.group_id = group; let block = crate::packets::EjectGroupMemberRequestPacketEjectDataBlock { ejectee_id: member }; packet.eject_data = vec![block]; self.send(&packet, PacketType::EjectGroupMemberRequest) } pub fn update_role(&self, role: GroupRole) -> Result<(), Error> { self.send_role_update(role.group_id, role, GroupRoleUpdate::UpdateAll) } pub fn create_role(&self, group: UUID, mut role: GroupRole) -> Result<(), Error> { role.id = UUID::random()?; self.send_role_update(group, role, GroupRoleUpdate::Create) } pub fn delete_role(&self, group: UUID, role_id: UUID) -> Result<(), Error> { let role = GroupRole { description: String::new(), group_id: group, id: role_id, members: 0, name: String::new(), powers: crate::GroupPowers::NONE, title: String::new(), }; self.send_role_update(group, role, GroupRoleUpdate::Delete) } fn send_role_update( &self, group: UUID, role: GroupRole, update: GroupRoleUpdate, ) -> Result<(), Error> { if role.name.len() > 256 || role.title.len() > 256 || role.description.len() > 1024 { return Err(Error::Argument); } let (agent, session) = self.ids(); let mut packet = crate::packets::GroupRoleUpdatePacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.agent_data.group_id = group; let block = crate::packets::GroupRoleUpdatePacketRoleDataBlock { role_id: role.id, name: role.name.into_bytes(), description: role.description.into_bytes(), powers: role.powers.0, title: role.title.into_bytes(), update_type: update as u8, }; packet.role_data = vec![block]; self.send(&packet, PacketType::GroupRoleUpdate) } pub fn add_to_role(&self, group: UUID, role: UUID, member: UUID) -> Result<(), Error> { self.change_role(group, role, member, GroupRoleChangeType::AddToRole) } pub fn remove_from_role(&self, group: UUID, role: UUID, member: UUID) -> Result<(), Error> { self.change_role(group, role, member, GroupRoleChangeType::RemoveFromRole) } fn change_role( &self, group: UUID, role: UUID, member: UUID, change: GroupRoleChangeType, ) -> Result<(), Error> { let (agent, session) = self.ids(); let mut packet = crate::packets::GroupRoleChangesPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.agent_data.group_id = group; let block = crate::packets::GroupRoleChangesPacketRoleChangeBlock { member_id: member, role_id: role, change: change as u32, }; packet.role_change = vec![block]; self.send(&packet, PacketType::GroupRoleChanges) } pub fn start_proposal(&self, group: UUID, prop: GroupProposal) -> Result<(), Error> { if prop.vote_text.len() > 1024 || prop.duration <= 0 || prop.quorum < 0 || !(0.0..=1.0).contains(&prop.majority) { return Err(Error::Argument); } let (agent, session) = self.ids(); let mut packet = crate::packets::StartGroupProposalPacket::new_with_constructor()?; packet.agent_data.agent_id = agent; packet.agent_data.session_id = session; packet.proposal_data.group_id = group; packet.proposal_data.proposal_text = prop.vote_text.into_bytes(); packet.proposal_data.quorum = prop.quorum; packet.proposal_data.majority = prop.majority; packet.proposal_data.duration = prop.duration; self.send(&packet, PacketType::StartGroupProposal) } pub fn send_group_notice(&self, group: UUID, notice: GroupNotice) -> Result<(), Error> { if notice.subject.len() > 256 || notice.message.len() > 4096 { return Err(Error::Argument); } let bucket = serialize_notice_attachment(¬ice)?; let mut client = (*self.inner.client).clone(); let region = client .network() .current_sim() .map_or_else(UUID::zero, |sim| sim.id); let agent = client.self_(); agent.native_instant_message_full( agent.name(), group, format!("{}|{}", notice.subject, notice.message), UUID::zero(), InstantMessageDialog::GroupNotice, crate::InstantMessageOnline::Online, Vector3::zero(), region, bucket, ) } pub async fn request_banned_agents( &self, group_id: UUID, cancellation_token: Option, ) -> Result<(), Error> { let token = cancellation_token.unwrap_or_else(|| self.inner.client.cancellation_token()); let result = self.group_api_uri(group_id).map(|uri| (uri, token.clone())); let Ok((uri, token)) = result else { self.inner.events.banned.emit(BannedAgentsEventArgs { group_id, success: false, banned_agents: None, }); return result.map(|_| ()); }; token.throw_if_cancellation_requested()?; let response = self .inner .client .native_http_caps_client() .get(uri, token, None) .await; let parsed = response.and_then(|(response, bytes)| { if !response.is_success_status_code() || bytes.len() > 4 * 1024 * 1024 { return Err(Error::InvalidOperation); } let OSD::Map(root) = OSDParser::deserialize_with_bytes(bytes)? else { return Err(Error::Argument); }; let mut agents = HashMap::new(); if let Some(OSD::Map(list)) = root.get("ban_list") { if list.len() > MAX_GROUP_ROWS { return Err(Error::Argument); } for (id, value) in list { let Ok(id) = UUID::new_with_string(id.clone()) else { continue; }; let OSD::Map(fields) = value else { continue }; if let Some(date) = fields.get("ban_date").and_then(|date| date.as_date().ok()) { agents.insert(id, date); } } } Ok(agents) }); match parsed { Ok(agents) => { self.inner.events.banned.emit(BannedAgentsEventArgs { group_id, success: true, banned_agents: Some(agents), }); Ok(()) } Err(error) => { self.inner.events.banned.emit(BannedAgentsEventArgs { group_id, success: false, banned_agents: None, }); Err(error) } } } pub async fn request_ban_action( &self, group_id: UUID, action: GroupBanAction, agents: Vec, cancellation_token: Option, ) -> Result<(), Error> { if agents.is_empty() || agents.len() > MAX_GROUP_ROWS { return Err(Error::Argument); } let token = cancellation_token.unwrap_or_else(|| self.inner.client.cancellation_token()); token.throw_if_cancellation_requested()?; let payload = OSD::Map(HashMap::from([ ("ban_action".into(), OSD::Integer(action as i32)), ( "ban_ids".into(), OSD::Array(agents.into_iter().map(OSD::UUID).collect()), ), ])); let (response, bytes) = self .inner .client .native_http_caps_client() .post_with_uri_osd_format_osd_cancellation_token_i_progress( self.group_api_uri(group_id)?, OSDFormat::Xml, payload, token, None, ) .await?; if !response.is_success_status_code() || bytes.len() > 1024 * 1024 { Err(Error::InvalidOperation) } else { Ok(()) } } fn group_api_uri(&self, group_id: UUID) -> Result { let caps = self .inner .client .network() .current_sim() .and_then(|sim| sim.native_caps()) .ok_or(Error::InvalidOperation)?; let uri = caps .capability_uri("GroupAPIv1".into())? .ok_or(Error::InvalidOperation)?; let mut url = reqwest::Url::parse(&uri.0).map_err(|_| Error::Argument)?; url.query_pairs_mut() .append_pair("group_id", &group_id.to_string()); Ok(libremetaverse_types::compat::Uri(url.into())) } } fn validate_group(group: &Group) -> Result<(), Error> { if group.name.is_empty() || group.name.len() > 256 || group.charter.len() > 4096 || group.membership_fee < 0 { Err(Error::Argument) } else { Ok(()) } } pub(crate) fn serialize_notice_attachment(notice: &GroupNotice) -> Result, Error> { if notice.owner_id == UUID::zero() || notice.attachment_id == UUID::zero() { return Ok(Vec::new()); } OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([ ("item_id".into(), OSD::UUID(notice.attachment_id)), ("owner_id".into(), OSD::UUID(notice.owner_id)), ]))) } impl Group { pub(crate) fn native_equals_group(&self, other: &Self) -> bool { self == other } pub(crate) fn native_equals_object( &self, other: Option, ) -> bool { other .as_ref() .and_then(libremetaverse_types::compat::Object::downcast_ref::) == Some(self) } pub(crate) fn native_hash_code(&self) -> i32 { let Ok(bytes) = self.id.get_bytes() else { return 0; }; i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) } pub(crate) fn native_to_string(&self) -> String { format!("{} (Members: {})", self.name, self.group_membership_count) } } impl GroupNotice { pub(crate) fn native_serialize_attachment(&self) -> Result, Error> { serialize_notice_attachment(self) } } impl GroupRole { pub(crate) fn native_to_string(&self) -> String { format!("{} ({})", self.name, self.title) } } impl GroupTitle { pub(crate) fn native_to_string(&self) -> String { self.title.clone() } } #[cfg(test)] mod tests { use super::*; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; fn id(value: &str) -> UUID { UUID::new_with_string(value.into()).unwrap() } fn simulator(client: &GridClient) -> crate::Simulator { crate::Simulator::native_new( client.clone(), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 13972), 0, Some(256), Some(256), ) .unwrap() } fn member_packet(group_id: UUID, request_id: UUID, member_id: UUID) -> Vec { let mut packet = crate::packets::GroupMembersReplyPacket::new_with_constructor().unwrap(); packet.group_data.group_id = group_id; packet.group_data.request_id = request_id; packet.group_data.member_count = 2; let mut member = crate::packets::GroupMembersReplyPacketMemberDataBlock::new_with_constructor().unwrap(); member.agent_id = member_id; member.title = b"Builder".to_vec(); member.online_status = b"Online".to_vec(); member.agent_powers = crate::GroupPowers::MEMBER_VISIBLE.0; packet.member_data = vec![member]; packet.encode_packet().unwrap() } #[test] fn fragmented_members_are_correlated_deduplicated_and_emitted_after_unlock() { let client = GridClient::new().unwrap(); let manager = GroupManager::native_new(Arc::new(client.clone())); let simulator = simulator(&client); let group_id = id("11111111-1111-1111-1111-111111111111"); let request_id = id("22222222-2222-2222-2222-222222222222"); let first = id("33333333-3333-3333-3333-333333333333"); let second = id("44444444-4444-4444-4444-444444444444"); mutex(&manager.inner.pending) .members .insert(request_id, (group_id, usize::MAX, HashMap::new())); let received = Arc::new(Mutex::new(Vec::new())); let received_copy = Arc::clone(&received); let inner = Arc::clone(&manager.inner); let _subscription = manager.subscribe_group_members_reply(Arc::new(move |event| { assert!( !mutex(&inner.pending) .members .contains_key(&event.request_id()) ); received_copy.lock().unwrap().push(event.members()); })); for member in [first, first, second] { manager.inner.handle_packet(RawPacketReceivedEventArgs { packet_type: PacketType::GroupMembersReply, data: member_packet(group_id, request_id, member), simulator: simulator.clone(), }); } let values = received.lock().unwrap(); assert_eq!(values.len(), 1); assert_eq!(values[0].len(), 2); assert_eq!(values[0][&first].title, "Builder"); } #[test] fn notice_attachment_is_structured_and_empty_without_both_ids() { let empty = GroupNotice { attachment_id: UUID::zero(), message: String::new(), owner_id: UUID::zero(), subject: String::new(), }; assert!(serialize_notice_attachment(&empty).unwrap().is_empty()); let notice = GroupNotice { attachment_id: id("55555555-5555-5555-5555-555555555555"), owner_id: id("66666666-6666-6666-6666-666666666666"), ..empty }; let OSD::Map(map) = OSDParser::deserialize_with_bytes(serialize_notice_attachment(¬ice).unwrap()) .unwrap() else { panic!("attachment must be an LLSD map") }; assert_eq!(map["item_id"].as_uuid().unwrap(), notice.attachment_id); assert_eq!(map["owner_id"].as_uuid().unwrap(), notice.owner_id); } #[test] fn dispose_releases_callbacks_and_pending_requests() { let client = GridClient::new().unwrap(); let manager = GroupManager::native_new(Arc::new(client)); mutex(&manager.inner.pending) .members .insert(UUID::zero(), (UUID::zero(), 1, HashMap::new())); manager.dispose().unwrap(); assert!(manager.inner.disposed.load(Ordering::Acquire)); assert!(mutex(&manager.inner.pending).members.is_empty()); assert!(mutex(&manager.inner.subscriptions).is_empty()); } }