Files
MetaCrate/crates/libremetaverse/src/group_manager.rs
Chili Palmer 21f85a1b58
Some checks failed
Native code generation / deterministic (push) Successful in 18m19s
Imaging and meshing gate / native (push) Failing after 4m28s
Native Rust workspace compile / compile (push) Failing after 12m58s
Implement remaining TestClient service commands (#94)
2026-08-11 13:10:27 +00:00

1625 lines
64 KiB
Rust

//! 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<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()
}
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<Self, Error> { 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<HashMap<UUID, std::time::SystemTime>> => banned_agents });
event_args!(CurrentGroupsEventArgs { groups: HashMap<UUID, Group> => 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<UUID, GroupMember> => members });
event_args!(GroupNamesEventArgs { group_names: HashMap<UUID, String> => group_names });
event_args!(GroupNoticesListReplyEventArgs { group_id: UUID => group_id, notices: Vec<GroupNoticesListEntry> => 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<UUID, GroupRole> => 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<UUID, GroupTitle> => titles });
#[derive(Clone)]
pub struct GroupInvitationEventArgs {
simulator: crate::Simulator,
agent_id: UUID,
from_name: String,
message: String,
accept: Arc<AtomicBool>,
}
impl GroupInvitationEventArgs {
pub fn new(
simulator: crate::Simulator,
agent_id: UUID,
agent_name: String,
message: String,
) -> Result<Self, Error> {
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<BannedAgentsEventArgs>,
current: EventRegistry<CurrentGroupsEventArgs>,
account: EventRegistry<GroupAccountSummaryReplyEventArgs>,
created: EventRegistry<GroupCreatedReplyEventArgs>,
dropped: EventRegistry<GroupDroppedEventArgs>,
invitation: EventRegistry<GroupInvitationEventArgs>,
joined: EventRegistry<GroupOperationEventArgs>,
left: EventRegistry<GroupOperationEventArgs>,
ejected: EventRegistry<GroupOperationEventArgs>,
members: EventRegistry<GroupMembersReplyEventArgs>,
names: EventRegistry<GroupNamesEventArgs>,
notices: EventRegistry<GroupNoticesListReplyEventArgs>,
profile: EventRegistry<GroupProfileEventArgs>,
roles: EventRegistry<GroupRolesDataReplyEventArgs>,
role_members: EventRegistry<GroupRolesMembersReplyEventArgs>,
titles: EventRegistry<GroupTitlesReplyEventArgs>,
}
#[derive(Default)]
struct PendingRows {
members: HashMap<UUID, (UUID, usize, HashMap<UUID, GroupMember>)>,
roles: HashMap<UUID, (UUID, usize, HashMap<UUID, GroupRole>)>,
role_members: HashMap<UUID, PendingRoleMembers>,
titles: HashMap<UUID, UUID>,
accounts: HashMap<UUID, UUID>,
}
type PendingRoleMembers = (UUID, usize, Vec<(UUID, UUID)>, HashSet<(UUID, UUID)>);
pub(crate) struct GroupManagerInner {
client: Arc<GridClient>,
groups: RwLock<HashMap<UUID, Group>>,
group_count: AtomicI32,
pending: Mutex<PendingRows>,
events: GroupEvents,
subscriptions: Mutex<Vec<Subscription>>,
disposed: AtomicBool,
}
#[derive(Clone)]
pub struct GroupManager {
pub(crate) inner: Arc<GroupManagerInner>,
}
impl GroupManager {
pub(crate) fn native_new(client: Arc<GridClient>) -> 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<Self, Error> {
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<T: GeneratedPacket>(&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<UUID, Group> {
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<Option<bool>> {
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<BannedAgentsEventArgs>) -> Subscription {
self.inner.events.banned.subscribe(h)
}
pub fn subscribe_current_groups(
&self,
h: EventHandler<CurrentGroupsEventArgs>,
) -> Subscription {
self.inner.events.current.subscribe(h)
}
pub fn subscribe_group_account_summary_reply(
&self,
h: EventHandler<GroupAccountSummaryReplyEventArgs>,
) -> Subscription {
self.inner.events.account.subscribe(h)
}
pub fn subscribe_group_created_reply(
&self,
h: EventHandler<GroupCreatedReplyEventArgs>,
) -> Subscription {
self.inner.events.created.subscribe(h)
}
pub fn subscribe_group_dropped(&self, h: EventHandler<GroupDroppedEventArgs>) -> Subscription {
self.inner.events.dropped.subscribe(h)
}
pub fn subscribe_group_invitation(
&self,
h: EventHandler<GroupInvitationEventArgs>,
) -> Subscription {
self.inner.events.invitation.subscribe(h)
}
pub fn subscribe_group_joined_reply(
&self,
h: EventHandler<GroupOperationEventArgs>,
) -> Subscription {
self.inner.events.joined.subscribe(h)
}
pub fn subscribe_group_leave_reply(
&self,
h: EventHandler<GroupOperationEventArgs>,
) -> Subscription {
self.inner.events.left.subscribe(h)
}
pub fn subscribe_group_member_ejected(
&self,
h: EventHandler<GroupOperationEventArgs>,
) -> Subscription {
self.inner.events.ejected.subscribe(h)
}
pub fn subscribe_group_members_reply(
&self,
h: EventHandler<GroupMembersReplyEventArgs>,
) -> Subscription {
self.inner.events.members.subscribe(h)
}
pub fn subscribe_group_names_reply(
&self,
h: EventHandler<GroupNamesEventArgs>,
) -> Subscription {
self.inner.events.names.subscribe(h)
}
pub fn subscribe_group_notices_list_reply(
&self,
h: EventHandler<GroupNoticesListReplyEventArgs>,
) -> Subscription {
self.inner.events.notices.subscribe(h)
}
pub fn subscribe_group_profile(&self, h: EventHandler<GroupProfileEventArgs>) -> Subscription {
self.inner.events.profile.subscribe(h)
}
pub fn subscribe_group_role_data_reply(
&self,
h: EventHandler<GroupRolesDataReplyEventArgs>,
) -> Subscription {
self.inner.events.roles.subscribe(h)
}
pub fn subscribe_group_role_members_reply(
&self,
h: EventHandler<GroupRolesMembersReplyEventArgs>,
) -> Subscription {
self.inner.events.role_members.subscribe(h)
}
pub fn subscribe_group_titles_reply(
&self,
h: EventHandler<GroupTitlesReplyEventArgs>,
) -> 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::<HashMap<_, _>>();
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<UUID>) -> 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<UUID, Error> {
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<UUID, Error> {
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<UUID, Error> {
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<UUID, Error> {
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<UUID>,
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(&notice)?;
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<CancellationToken>,
) -> 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<UUID>,
cancellation_token: Option<CancellationToken>,
) -> 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<libremetaverse_types::compat::Uri, Error> {
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<Vec<u8>, 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<libremetaverse_types::compat::Object>,
) -> bool {
other
.as_ref()
.and_then(libremetaverse_types::compat::Object::downcast_ref::<Self>)
== 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<Vec<u8>, 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<u8> {
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(&notice).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());
}
}