1625 lines
64 KiB
Rust
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(¬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<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(¬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());
|
|
}
|
|
}
|