Add more inventory-related utilities

This commit is contained in:
Salad Dais
2025-06-05 00:46:22 +00:00
parent 46e598cded
commit bb0e88e460
3 changed files with 117 additions and 5 deletions

View File

@@ -384,16 +384,17 @@ class InventorySaleInfo(InventoryBase):
sale_price: int = schema_field(SchemaInt) 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 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. a particular serialization order, as would happen if this was present on InventoryNodeBase.
""" """
name: str name: str
type: AssetType
@dataclasses.dataclass @dataclasses.dataclass
class InventoryNodeBase(InventoryBase, _HasName): class InventoryNodeBase(InventoryBase, _HasBaseNodeAttrs):
ID_ATTR: ClassVar[str] ID_ATTR: ClassVar[str]
parent_id: Optional[UUID] = schema_field(SchemaUUID) parent_id: Optional[UUID] = schema_field(SchemaUUID)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import gzip import gzip
import itertools import itertools
import logging import logging
@@ -9,10 +10,10 @@ from typing import Union, List, Tuple, Set
from hippolyzer.lib.base import llsd from hippolyzer.lib.base import llsd
from hippolyzer.lib.base.datatypes import UUID from hippolyzer.lib.base.datatypes import UUID
from hippolyzer.lib.base.inventory import InventoryModel, InventoryCategory, InventoryItem from hippolyzer.lib.base.inventory import InventoryModel, InventoryCategory, InventoryItem
from hippolyzer.lib.base.message.message import Message from hippolyzer.lib.base.message.message import Message, Block
from hippolyzer.lib.base.templates import AssetType, FolderType from hippolyzer.lib.base.templates import AssetType, FolderType, InventoryType
from hippolyzer.lib.client.state import BaseClientSession from hippolyzer.lib.client.state import BaseClientSession
from hippolyzer.lib.base.templates import WearableType
LOG = logging.getLogger(__name__) 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("UpdateCreateInventoryItem", self._handle_update_create_inventory_item)
self._session.message_handler.subscribe("RemoveInventoryItem", self._handle_remove_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("MoveInventoryItem", self._handle_move_inventory_item)
self._session.message_handler.subscribe("MoveInventoryFolder", self._handle_move_inventory_folder)
def _load_skeleton(self): def _load_skeleton(self):
assert not self.model.nodes assert not self.model.nodes
@@ -173,6 +175,14 @@ class InventoryManager:
node.name = str(inventory_block["NewName"]) node.name = str(inventory_block["NewName"])
node.parent_id = inventory_block['FolderID'] 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): def process_aisv3_response(self, payload: dict):
if "name" in payload: if "name" in payload:
# Just a rough guess. Assume this response is updating something if there's # 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(): for link_llsd in embedded_dict.get("links", {}).values():
self.model.upsert(InventoryItem.from_llsd(link_llsd, flavor="ais")) 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 # Get rid of anything we were asked to
for node_id in itertools.chain( for node_id in itertools.chain(
payload.get("_broken_links_removed", ()), payload.get("_broken_links_removed", ()),
@@ -206,3 +221,77 @@ class InventoryManager:
if node: if node:
# Presumably this list is exhaustive, so don't unlink children. # Presumably this list is exhaustive, so don't unlink children.
self.model.unlink(node, single_only=True) 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

View File

@@ -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.client.inventory_manager import InventoryManager
from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow
from hippolyzer.lib.proxy.viewer_settings import iter_viewer_cache_dirs 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: if TYPE_CHECKING:
from hippolyzer.lib.proxy.sessions import Session 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._wrap_with_cache_defer(
self._handle_move_inventory_item 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 = self._wrap_with_cache_defer(
self.process_aisv3_response self.process_aisv3_response
) )
@@ -105,3 +111,19 @@ class ProxyInventoryManager(InventoryManager):
# Try and add anything from the response into the model # Try and add anything from the response into the model
self.process_aisv3_response(llsd.parse(flow.response.content)) 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