From 3bb4fb06404e064ceadb268eb4e7f39e4a8d8257 Mon Sep 17 00:00:00 2001 From: Salad Dais Date: Fri, 19 Jan 2024 04:37:14 +0000 Subject: [PATCH] Basic AIS response handling in proxy --- hippolyzer/lib/base/inventory.py | 25 +++++++++++++--- hippolyzer/lib/client/inventory_manager.py | 34 +++++++++++++++++++++- hippolyzer/lib/proxy/inventory_manager.py | 23 +++++++++++++-- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/hippolyzer/lib/base/inventory.py b/hippolyzer/lib/base/inventory.py index b33b3e8..74c2e71 100644 --- a/hippolyzer/lib/base/inventory.py +++ b/hippolyzer/lib/base/inventory.py @@ -234,7 +234,7 @@ class InventoryModel(InventoryBase): 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}") + LOG.warning(f"Unknown object type {obj_dict!r}") return model @property @@ -498,6 +498,8 @@ class InventoryObject(InventoryContainerBase): @dataclasses.dataclass class InventoryCategory(InventoryContainerBase): ID_ATTR: ClassVar[str] = "cat_id" + # AIS calls this something else... + ID_ATTR_AIS: ClassVar[str] = "category_id" SCHEMA_NAME: ClassVar[str] = "inv_category" VERSION_NONE: ClassVar[int] = -1 @@ -528,12 +530,24 @@ class InventoryCategory(InventoryContainerBase): type=AssetType.CATEGORY, ) + @classmethod + def from_llsd(cls, inv_dict: Dict, flavor: str = "legacy"): + if flavor == "ais" and "type" not in inv_dict: + inv_dict = inv_dict.copy() + inv_dict["type"] = AssetType.CATEGORY + return super().from_llsd(inv_dict, flavor) + + def to_llsd(self, flavor: str = "legacy"): + payload = super().to_llsd(flavor) + if flavor == "ais": + # AIS already knows the inventory type is category + payload.pop("type", None) + return payload + @classmethod def _get_fields_dict(cls, llsd_flavor: Optional[str] = None): fields = super()._get_fields_dict(llsd_flavor) if llsd_flavor == "ais": - # AIS is smart enough to know that all categories are asset type category... - fields.pop("type") # These have different names though fields["type_default"] = fields.pop("preferred_type") fields["agent_id"] = fields.pop("owner_id") @@ -644,7 +658,7 @@ class InventoryItem(InventoryNodeBase): @classmethod def from_llsd(cls, inv_dict: Dict, flavor: str = "legacy"): - if flavor == "ais" and inv_dict["type"] == AssetType.LINK: + if flavor == "ais" and "linked_id" in inv_dict: # Links get represented differently than other items for whatever reason. # This is incredibly annoying, under *NIX there's nothing really special about symlinks. inv_dict = inv_dict.copy() @@ -666,6 +680,9 @@ class InventoryItem(InventoryNodeBase): sale_type=SaleType.NOT, sale_price=0, ).to_llsd("ais") + if "type" not in inv_dict: + inv_dict["type"] = AssetType.LINK + # In the context of symlinks, asset id means linked item ID. # This is also how indra stores symlinks. Why the asymmetry in AIS if none of the # consumers actually want it? Who knows. diff --git a/hippolyzer/lib/client/inventory_manager.py b/hippolyzer/lib/client/inventory_manager.py index a92cdc1..ddb0640 100644 --- a/hippolyzer/lib/client/inventory_manager.py +++ b/hippolyzer/lib/client/inventory_manager.py @@ -1,6 +1,7 @@ from __future__ import annotations import gzip +import itertools import logging from pathlib import Path from typing import Union, List, Tuple, Set @@ -173,4 +174,35 @@ class InventoryManager: node.parent_id = inventory_block['FolderID'] def process_aisv3_response(self, payload: dict): - pass + if "name" in payload: + # Just a rough guess. Assume this response is updating something if there's + # a "name" key. + if InventoryCategory.ID_ATTR_AIS in payload: + if (cat_node := InventoryCategory.from_llsd(payload, flavor="ais")) is not None: + self.model.upsert(cat_node) + elif InventoryItem.ID_ATTR in payload: + if (item_node := InventoryItem.from_llsd(payload, flavor="ais")) is not None: + self.model.upsert(item_node) + else: + LOG.warning(f"Unknown node type in AIS payload: {payload!r}") + + # Parse the embedded stuff + embedded_dict = payload.get("_embedded", {}) + for category_llsd in embedded_dict.get("categories", {}).values(): + self.model.upsert(InventoryCategory.from_llsd(category_llsd, flavor="ais")) + for item_llsd in embedded_dict.get("items", {}).values(): + self.model.upsert(InventoryItem.from_llsd(item_llsd, flavor="ais")) + for link_llsd in embedded_dict.get("links", {}).values(): + self.model.upsert(InventoryItem.from_llsd(link_llsd, flavor="ais")) + + # Get rid of anything we were asked to + for node_id in itertools.chain( + payload.get("_broken_links_removed", ()), + payload.get("_removed_items", ()), + payload.get("_category_items_removed", ()), + payload.get("_categories_removed", ()), + ): + node = self.model.get(node_id) + if node: + # Presumably this list is exhaustive, so don't unlink children. + self.model.unlink(node, single_only=True) diff --git a/hippolyzer/lib/proxy/inventory_manager.py b/hippolyzer/lib/proxy/inventory_manager.py index 6d362b8..fdbd292 100644 --- a/hippolyzer/lib/proxy/inventory_manager.py +++ b/hippolyzer/lib/proxy/inventory_manager.py @@ -4,17 +4,23 @@ import functools import logging from typing import * +from hippolyzer.lib.base import llsd from hippolyzer.lib.base.helpers import get_mtime, create_logged_task from hippolyzer.lib.client.inventory_manager import InventoryManager -from hippolyzer.lib.client.state import BaseClientSession +from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow from hippolyzer.lib.proxy.viewer_settings import iter_viewer_cache_dirs +if TYPE_CHECKING: + from hippolyzer.lib.proxy.sessions import Session + LOG = logging.getLogger(__name__) class ProxyInventoryManager(InventoryManager): - def __init__(self, session: BaseClientSession): + _session: "Session" + + def __init__(self, session: "Session"): # These handlers all need their processing deferred until the cache has been loaded. # Since cache is loaded asynchronously, the viewer may get ahead of us due to parsing # the cache faster and start requesting inventory details we can't do anything with yet. @@ -41,6 +47,7 @@ class ProxyInventoryManager(InventoryManager): # be wrapped before we call they're registered. Handlers are registered by method reference, # not by name! super().__init__(session) + session.http_message_handler.subscribe("InventoryAPIv3", self._handle_aisv3_flow) newest_cache = None newest_timestamp = dt.datetime(year=1970, month=1, day=1, tzinfo=dt.timezone.utc) # So consumers know when the inventory should be complete @@ -85,3 +92,15 @@ class ProxyInventoryManager(InventoryManager): else: func(*inner_args) return wrapped + + def _handle_aisv3_flow(self, flow: HippoHTTPFlow): + if flow.response.status_code < 200 or flow.response.status_code > 300: + # Probably not a success + return + content_type = flow.response.headers.get("Content-Type", "") + if "llsd" not in content_type: + # Okay, probably still some kind of error... + return + + # Try and add anything from the response into the model + self.process_aisv3_response(llsd.parse(flow.response.content))