Update inventory handling code

This commit is contained in:
Salad Dais
2025-06-05 16:33:26 +00:00
parent 4916bdc543
commit 7cbef457cf
4 changed files with 119 additions and 56 deletions

View File

@@ -229,12 +229,14 @@ class InventoryModel(InventoryBase):
def from_llsd(cls, llsd_val: List[Dict], flavor: str = "legacy") -> InventoryModel:
model = cls()
for obj_dict in llsd_val:
obj = None
for inv_type in INVENTORY_TYPES:
if inv_type.ID_ATTR in obj_dict:
if (obj := inv_type.from_llsd(obj_dict, flavor)) is not None:
model.add(obj)
break
LOG.warning(f"Unknown object type {obj_dict!r}")
if obj is None:
LOG.warning(f"Unknown object type {obj_dict!r}")
return model
@property
@@ -258,7 +260,7 @@ class InventoryModel(InventoryBase):
def all_items(self) -> Iterable[InventoryItem]:
for node in self.nodes.values():
if not isinstance(node, InventoryContainerBase):
yield node
yield node # type: ignore
def __eq__(self, other):
if not isinstance(other, InventoryModel):
@@ -584,9 +586,9 @@ class InventoryItem(InventoryNodeBase):
return self.asset_id
return self.shadow_id ^ MAGIC_ID
def to_inventory_data(self) -> Block:
def to_inventory_data(self, block_name: str = "InventoryData") -> Block:
return Block(
"InventoryData",
block_name,
ItemID=self.item_id,
FolderID=self.parent_id,
CallbackID=0,
@@ -641,7 +643,7 @@ class InventoryItem(InventoryNodeBase):
),
name=block["Name"],
desc=block["Description"],
creation_date=block["CreationDate"],
creation_date=SchemaDate.from_llsd(block["CreationDate"], "legacy"),
)
def to_llsd(self, flavor: str = "legacy"):

View File

@@ -18,6 +18,11 @@ from hippolyzer.lib.base.templates import WearableType
LOG = logging.getLogger(__name__)
class CannotMoveError(Exception):
def __init__(self):
pass
class InventoryManager:
def __init__(self, session: BaseClientSession):
self._session = session
@@ -222,6 +227,28 @@ class InventoryManager:
# Presumably this list is exhaustive, so don't unlink children.
self.model.unlink(node, single_only=True)
async def make_ais_request(
self,
method: str,
path: str,
params: dict,
payload: dict,
) -> dict:
caps_client = self._session.main_region.caps_client
async with caps_client.request(method, "InventoryAPIv3", path=path, params=params, llsd=payload) as resp:
if resp.ok or resp.status == 400:
data = await resp.read_llsd()
if err_desc := data.get("error_description", ""):
err_desc: str
if err_desc.startswith("Cannot change parent_id."):
raise CannotMoveError()
resp.raise_for_status()
self.process_aisv3_response(data)
else:
resp.raise_for_status()
return data
async def create_folder(
self,
parent: InventoryCategory | UUID,
@@ -234,9 +261,6 @@ class InventoryManager:
else:
parent_id = parent
caps_client = self._session.main_region.caps_client
transaction_id = UUID.random()
params = {"tid": transaction_id}
payload = {
"categories": [
{
@@ -247,11 +271,8 @@ class InventoryManager:
}
]
}
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
data = await self.make_ais_request("POST", f"/category/{parent_id}", {"tid": UUID.random()}, payload)
return self.model.get(data["_created_categories"][0]) # type: ignore
async def create_item(
self,
@@ -260,7 +281,8 @@ class InventoryManager:
type: AssetType,
inv_type: InventoryType,
wearable_type: WearableType,
perms: int,
transaction_id: UUID,
perms: int = 0x7FffFFff,
description: str = '',
) -> InventoryItem:
if isinstance(parent, InventoryCategory):
@@ -268,7 +290,6 @@ class InventoryManager:
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,
@@ -293,5 +314,6 @@ class InventoryManager:
)
)
msg = await asyncio.wait_for(get_msg(), 5.0)
# Handle this synchronously.
self._handle_update_create_inventory_item(msg)
return self.model.get(msg["InventoryData"]["ItemID"]) # type: ignore

View File

@@ -12,6 +12,9 @@ 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
from hippolyzer.lib.base.inventory import InventoryItem
from hippolyzer.lib.base.templates import AssetType, InventoryType, WearableType
from hippolyzer.lib.base.network.transport import Direction
if TYPE_CHECKING:
from hippolyzer.lib.proxy.sessions import Session
@@ -121,9 +124,40 @@ class ProxyInventoryManager(InventoryManager):
) -> 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(
self._session.main_region.circuit.send(Message(
"BulkUpdateInventory",
Block("AgentData", AgentID=self._session.agent_id, TransactionID=UUID.random()),
cat.to_folder_data()
cat.to_folder_data(),
direction=Direction.IN
))
return cat
async def create_item(
self,
parent: UUID | InventoryCategory,
name: str,
type: AssetType,
inv_type: InventoryType,
wearable_type: WearableType,
transaction_id: UUID,
perms: int = 0x7FffFFff,
description: str = '',
) -> InventoryItem:
item = await super().create_item(
parent=parent,
name=name,
type=type,
inv_type=inv_type,
wearable_type=wearable_type,
transaction_id=transaction_id,
perms=perms,
description=description,
)
# We need to tell the client about the new folder via an injected eq event
self._session.main_region.circuit.send(Message(
"BulkUpdateInventory",
Block("AgentData", AgentID=self._session.agent_id, TransactionID=UUID.random()),
item.to_inventory_data("ItemData"),
direction=Direction.IN
))
return item

View File

@@ -1,8 +1,9 @@
import copy
import datetime as dt
import unittest
from hippolyzer.lib.base.datatypes import *
from hippolyzer.lib.base.inventory import InventoryModel, SaleType
from hippolyzer.lib.base.inventory import InventoryModel, SaleType, InventoryItem
from hippolyzer.lib.base.wearables import Wearable, VISUAL_PARAMS
SIMPLE_INV = """\tinv_object\t0
@@ -47,6 +48,42 @@ SIMPLE_INV = """\tinv_object\t0
\t}
"""
SIMPLE_INV_PARSED = [
{
'name': 'Contents',
'obj_id': UUID('f4d91477-def1-487a-b4f3-6fa201c17376'),
'parent_id': UUID('00000000-0000-0000-0000-000000000000'),
'type': 'category'
},
{
'asset_id': UUID('00000000-0000-0000-0000-000000000000'),
'created_at': 1587367239,
'desc': '2020-04-20 04:20:39 lsl2 script',
'flags': b'\x00\x00\x00\x00',
'inv_type': 'script',
'item_id': UUID('dd163122-946b-44df-99f6-a6030e2b9597'),
'name': 'New Script',
'metadata': {"experience": UUID("a2e76fcd-9360-4f6d-a924-000000000003")},
'parent_id': UUID('f4d91477-def1-487a-b4f3-6fa201c17376'),
'permissions': {
'base_mask': 2147483647,
'creator_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
'everyone_mask': 0,
'group_id': UUID('00000000-0000-0000-0000-000000000000'),
'group_mask': 0,
'last_owner_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
'next_owner_mask': 581632,
'owner_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
'owner_mask': 2147483647,
},
'sale_info': {
'sale_price': 10,
'sale_type': 'not'
},
'type': 'lsltext'
}
]
INV_CATEGORY = """\tinv_category\t0
\t{
\t\tcat_id\tf4d91477-def1-487a-b4f3-6fa201c17376
@@ -122,44 +159,12 @@ class TestLegacyInv(unittest.TestCase):
self.assertEqual(item, item_copy)
def test_llsd_serialization(self):
self.assertEqual(
self.model.to_llsd(),
[
{
'name': 'Contents',
'obj_id': UUID('f4d91477-def1-487a-b4f3-6fa201c17376'),
'parent_id': UUID('00000000-0000-0000-0000-000000000000'),
'type': 'category'
},
{
'asset_id': UUID('00000000-0000-0000-0000-000000000000'),
'created_at': 1587367239,
'desc': '2020-04-20 04:20:39 lsl2 script',
'flags': b'\x00\x00\x00\x00',
'inv_type': 'script',
'item_id': UUID('dd163122-946b-44df-99f6-a6030e2b9597'),
'name': 'New Script',
'metadata': {"experience": UUID("a2e76fcd-9360-4f6d-a924-000000000003")},
'parent_id': UUID('f4d91477-def1-487a-b4f3-6fa201c17376'),
'permissions': {
'base_mask': 2147483647,
'creator_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
'everyone_mask': 0,
'group_id': UUID('00000000-0000-0000-0000-000000000000'),
'group_mask': 0,
'last_owner_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
'next_owner_mask': 581632,
'owner_id': UUID('a2e76fcd-9360-4f6d-a924-000000000003'),
'owner_mask': 2147483647,
},
'sale_info': {
'sale_price': 10,
'sale_type': 'not'
},
'type': 'lsltext'
}
]
)
self.assertEqual(self.model.to_llsd(), SIMPLE_INV_PARSED)
def test_llsd_date_parsing(self):
model = InventoryModel.from_llsd(SIMPLE_INV_PARSED)
item: InventoryItem = model.nodes.get(UUID("dd163122-946b-44df-99f6-a6030e2b9597")) # type: ignore
self.assertEqual(item.creation_date, dt.datetime(2020, 4, 20, 7, 20, 39, tzinfo=dt.timezone.utc))
def test_llsd_serialization_ais(self):
model = InventoryModel.from_str(INV_CATEGORY)