diff --git a/hippolyzer/lib/base/inventory.py b/hippolyzer/lib/base/inventory.py index 7ac5430..d0d442e 100644 --- a/hippolyzer/lib/base/inventory.py +++ b/hippolyzer/lib/base/inventory.py @@ -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"): diff --git a/hippolyzer/lib/client/inventory_manager.py b/hippolyzer/lib/client/inventory_manager.py index ac15f13..bf17e01 100644 --- a/hippolyzer/lib/client/inventory_manager.py +++ b/hippolyzer/lib/client/inventory_manager.py @@ -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 diff --git a/hippolyzer/lib/proxy/inventory_manager.py b/hippolyzer/lib/proxy/inventory_manager.py index e6f5d52..0c17880 100644 --- a/hippolyzer/lib/proxy/inventory_manager.py +++ b/hippolyzer/lib/proxy/inventory_manager.py @@ -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 diff --git a/tests/base/test_legacy_schema.py b/tests/base/test_legacy_schema.py index eb976c2..40d61c5 100644 --- a/tests/base/test_legacy_schema.py +++ b/tests/base/test_legacy_schema.py @@ -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)