From bb0e88e460b83ff657ddd9457573d31f232af9fa Mon Sep 17 00:00:00 2001 From: Salad Dais Date: Thu, 5 Jun 2025 00:46:22 +0000 Subject: [PATCH] Add more inventory-related utilities --- hippolyzer/lib/base/inventory.py | 5 +- hippolyzer/lib/client/inventory_manager.py | 95 +++++++++++++++++++++- hippolyzer/lib/proxy/inventory_manager.py | 22 +++++ 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/hippolyzer/lib/base/inventory.py b/hippolyzer/lib/base/inventory.py index 74c2e71..7ac5430 100644 --- a/hippolyzer/lib/base/inventory.py +++ b/hippolyzer/lib/base/inventory.py @@ -384,16 +384,17 @@ class InventorySaleInfo(InventoryBase): sale_price: int = schema_field(SchemaInt) -class _HasName(abc.ABC): +class _HasBaseNodeAttrs(abc.ABC): """ Only exists so that we can assert that all subclasses should have this without forcing a particular serialization order, as would happen if this was present on InventoryNodeBase. """ name: str + type: AssetType @dataclasses.dataclass -class InventoryNodeBase(InventoryBase, _HasName): +class InventoryNodeBase(InventoryBase, _HasBaseNodeAttrs): ID_ATTR: ClassVar[str] parent_id: Optional[UUID] = schema_field(SchemaUUID) diff --git a/hippolyzer/lib/client/inventory_manager.py b/hippolyzer/lib/client/inventory_manager.py index ddb0640..ac15f13 100644 --- a/hippolyzer/lib/client/inventory_manager.py +++ b/hippolyzer/lib/client/inventory_manager.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import gzip import itertools import logging @@ -9,10 +10,10 @@ from typing import Union, List, Tuple, Set from hippolyzer.lib.base import llsd from hippolyzer.lib.base.datatypes import UUID from hippolyzer.lib.base.inventory import InventoryModel, InventoryCategory, InventoryItem -from hippolyzer.lib.base.message.message import Message -from hippolyzer.lib.base.templates import AssetType, FolderType +from hippolyzer.lib.base.message.message import Message, Block +from hippolyzer.lib.base.templates import AssetType, FolderType, InventoryType from hippolyzer.lib.client.state import BaseClientSession - +from hippolyzer.lib.base.templates import WearableType LOG = logging.getLogger(__name__) @@ -26,6 +27,7 @@ class InventoryManager: self._session.message_handler.subscribe("UpdateCreateInventoryItem", self._handle_update_create_inventory_item) self._session.message_handler.subscribe("RemoveInventoryItem", self._handle_remove_inventory_item) self._session.message_handler.subscribe("MoveInventoryItem", self._handle_move_inventory_item) + self._session.message_handler.subscribe("MoveInventoryFolder", self._handle_move_inventory_folder) def _load_skeleton(self): assert not self.model.nodes @@ -173,6 +175,14 @@ class InventoryManager: node.name = str(inventory_block["NewName"]) node.parent_id = inventory_block['FolderID'] + def _handle_move_inventory_folder(self, msg: Message): + for inventory_block in msg["InventoryData"]: + node = self.model.get(inventory_block["FolderID"]) + if not node: + LOG.warning(f"Missing inventory folder {inventory_block['FolderID']}") + continue + node.parent_id = inventory_block['ParentID'] + def process_aisv3_response(self, payload: dict): if "name" in payload: # Just a rough guess. Assume this response is updating something if there's @@ -195,6 +205,11 @@ class InventoryManager: for link_llsd in embedded_dict.get("links", {}).values(): self.model.upsert(InventoryItem.from_llsd(link_llsd, flavor="ais")) + for cat_id, version in payload.get("_updated_category_versions", {}).items(): + cat_node: InventoryCategory = self.model.get(cat_id) # type: ignore + if cat_node: + cat_node.version = version + # Get rid of anything we were asked to for node_id in itertools.chain( payload.get("_broken_links_removed", ()), @@ -206,3 +221,77 @@ class InventoryManager: if node: # Presumably this list is exhaustive, so don't unlink children. self.model.unlink(node, single_only=True) + + async def create_folder( + self, + parent: InventoryCategory | UUID, + name: str, + type: int = -1, + cat_id: UUID | None = None + ) -> InventoryCategory: + if isinstance(parent, InventoryCategory): + parent_id = parent.cat_id + else: + parent_id = parent + + caps_client = self._session.main_region.caps_client + transaction_id = UUID.random() + params = {"tid": transaction_id} + payload = { + "categories": [ + { + "category_id": cat_id, + "name": name, + "type_default": type, + "parent_id": parent_id + } + ] + } + async with caps_client.post("InventoryAPIv3", path=f"/category/{parent_id}", params=params, llsd=payload) as resp: + resp.raise_for_status() + self.process_aisv3_response(await resp.read_llsd()) + parent_cat: InventoryCategory = self.model.get(parent_id) # type: ignore + return [x for x in parent_cat.children if x.name == name][0] # type: ignore + + async def create_item( + self, + parent: UUID | InventoryCategory, + name: str, + type: AssetType, + inv_type: InventoryType, + wearable_type: WearableType, + perms: int, + description: str = '', + ) -> InventoryItem: + if isinstance(parent, InventoryCategory): + parent_id = parent.cat_id + else: + parent_id = parent + + transaction_id = UUID.random() + with self._session.main_region.message_handler.subscribe_async( + ("UpdateCreateInventoryItem",), + predicate=lambda x: x["AgentData"]["TransactionID"] == transaction_id, + take=False, + ) as get_msg: + await self._session.main_region.circuit.send_reliable( + Message( + 'CreateInventoryItem', + Block('AgentData', AgentID=self._session.agent_id, SessionID=self._session.id), + Block( + 'InventoryBlock', + CallbackID=0, + FolderID=parent_id, + TransactionID=transaction_id, + NextOwnerMask=perms, + Type=type, + InvType=inv_type, + WearableType=wearable_type, + Name=name, + Description=description, + ) + ) + ) + msg = await asyncio.wait_for(get_msg(), 5.0) + self._handle_update_create_inventory_item(msg) + return self.model.get(msg["InventoryData"]["ItemID"]) # type: ignore diff --git a/hippolyzer/lib/proxy/inventory_manager.py b/hippolyzer/lib/proxy/inventory_manager.py index de2e2d1..e6f5d52 100644 --- a/hippolyzer/lib/proxy/inventory_manager.py +++ b/hippolyzer/lib/proxy/inventory_manager.py @@ -9,6 +9,9 @@ from hippolyzer.lib.base.helpers import get_mtime, create_logged_task from hippolyzer.lib.client.inventory_manager import InventoryManager from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow from hippolyzer.lib.proxy.viewer_settings import iter_viewer_cache_dirs +from hippolyzer.lib.base.datatypes import UUID +from hippolyzer.lib.base.inventory import InventoryCategory +from hippolyzer.lib.base.message.message import Message, Block if TYPE_CHECKING: from hippolyzer.lib.proxy.sessions import Session @@ -39,6 +42,9 @@ class ProxyInventoryManager(InventoryManager): self._handle_move_inventory_item = self._wrap_with_cache_defer( self._handle_move_inventory_item ) + self._handle_move_inventory_folder = self._wrap_with_cache_defer( + self._handle_move_inventory_folder + ) self.process_aisv3_response = self._wrap_with_cache_defer( self.process_aisv3_response ) @@ -105,3 +111,19 @@ class ProxyInventoryManager(InventoryManager): # Try and add anything from the response into the model self.process_aisv3_response(llsd.parse(flow.response.content)) + + async def create_folder( + self, + parent: InventoryCategory | UUID, + name: str, + type: int = -1, + cat_id: UUID | None = None + ) -> InventoryCategory: + cat = await super().create_folder(parent, name, type, cat_id) + # We need to tell the client about the new folder via an injected eq event + self._session.main_region.eq_manager.inject_message(Message( + "BulkUpdateInventory", + Block("AgentData", AgentID=self._session.agent_id, TransactionID=UUID.random()), + cat.to_folder_data() + )) + return cat