Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f87ec8725 | ||
|
|
fb885d8eec | ||
|
|
78281ed12b | ||
|
|
4087eaa3c6 | ||
|
|
32428941d7 | ||
|
|
0cc3397402 | ||
|
|
0c2dfd3213 | ||
|
|
e119181e3f | ||
|
|
64c7265578 | ||
|
|
eb652152f5 | ||
|
|
cd03dd4fdd | ||
|
|
056e142347 |
2
.github/workflows/bundle_windows.yml
vendored
2
.github/workflows/bundle_windows.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
pip install -e .[gui]
|
||||
pip install cx_freeze
|
||||
|
||||
- name: Bundle with cx_Freeze
|
||||
|
||||
2
.github/workflows/pytest.yml
vendored
2
.github/workflows/pytest.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-test.txt
|
||||
sudo apt-get install libopenjp2-7
|
||||
pip install -e .
|
||||
pip install -e .[gui]
|
||||
- name: Run Flake8
|
||||
run: |
|
||||
flake8 .
|
||||
|
||||
@@ -35,7 +35,9 @@ with low-level SL details. See the [Local Animation addon example](https://githu
|
||||
* Activate the virtualenv by running the appropriate activation script
|
||||
* * Under Linux this would be something like `source <virtualenv_dir>/bin/activate`
|
||||
* * Under Windows it's `<virtualenv_dir>\Scripts\activate.bat`
|
||||
* Run `pip install hippolyzer`, or run `pip install -e .` in a cloned repo to install an editable version
|
||||
* Run `pip install hippolyzer[gui]` for a full install, or run `pip install -e .[gui]` in a cloned repo to install an editable version
|
||||
* * If you only want the core library without proxy or GUI support, use `pip install hippolyzer` or `pip install -e .`
|
||||
* * If you only want proxy/CLI support without the GUI, use `pip install hippolyzer[proxy]` or `pip install -e .[proxy]`
|
||||
|
||||
### Binary Windows Builds
|
||||
|
||||
|
||||
94
addon_examples/appearance_delay_tracker.py
Normal file
94
addon_examples/appearance_delay_tracker.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Try and diagnose very slow avatar appearance loads when the avatars first come on the scene
|
||||
|
||||
I guess use LEAP or something to detect when things _actually_ declouded.
|
||||
"""
|
||||
from typing import *
|
||||
|
||||
import dataclasses
|
||||
import datetime as dt
|
||||
|
||||
from hippolyzer.lib.base.datatypes import UUID
|
||||
from hippolyzer.lib.base.message.message import Message
|
||||
from hippolyzer.lib.base.objects import Object
|
||||
from hippolyzer.lib.base.templates import PCode
|
||||
from hippolyzer.lib.proxy.addon_utils import BaseAddon, GlobalProperty
|
||||
from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow
|
||||
from hippolyzer.lib.proxy.region import ProxiedRegion
|
||||
from hippolyzer.lib.proxy.sessions import Session, SessionManager
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AvatarBakeRequest:
|
||||
requested: dt.datetime
|
||||
received: Optional[dt.datetime] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AvatarAppearanceRecord:
|
||||
object_received: dt.datetime
|
||||
"""When we learned about the agent as an object"""
|
||||
appearance_received: Optional[dt.datetime] = None
|
||||
"""When AvatarAppearance was first received"""
|
||||
bake_requests: Dict[str, AvatarBakeRequest] = dataclasses.field(default_factory=dict)
|
||||
"""Layer name -> request / response details"""
|
||||
|
||||
|
||||
class AppearanceDelayTrackerAddon(BaseAddon):
|
||||
# Should be able to access this in the REPL
|
||||
# Normally we'd use a session property, but we may not have a proper session context for some requests
|
||||
av_appearance_data: Dict[UUID, AvatarAppearanceRecord] = GlobalProperty(dict)
|
||||
|
||||
def handle_object_updated(self, session: Session, region: ProxiedRegion,
|
||||
obj: Object, updated_props: Set[str], msg: Optional[Message]):
|
||||
if obj.PCode == PCode.AVATAR and obj.FullID not in self.av_appearance_data:
|
||||
self.av_appearance_data[obj.FullID] = AvatarAppearanceRecord(object_received=dt.datetime.now())
|
||||
|
||||
def handle_lludp_message(self, session: Session, region: ProxiedRegion, message: Message):
|
||||
if message.name != "AvatarAppearance":
|
||||
return
|
||||
agent_id = message["Sender"]["ID"]
|
||||
appearance_data = self.av_appearance_data.get(agent_id)
|
||||
if not appearance_data:
|
||||
print(f"Got appearance for {agent_id} without knowing about object?")
|
||||
return
|
||||
|
||||
if appearance_data.appearance_received:
|
||||
return
|
||||
appearance_data.appearance_received = dt.datetime.now()
|
||||
|
||||
def handle_http_request(self, session_manager: SessionManager, flow: HippoHTTPFlow):
|
||||
if not flow.cap_data:
|
||||
return
|
||||
if flow.cap_data.cap_name != "AppearanceService":
|
||||
return
|
||||
|
||||
agent_id = UUID(flow.request.url.split('/')[-3])
|
||||
slot_name = flow.request.url.split('/')[-2]
|
||||
appearance_data = self.av_appearance_data.get(agent_id)
|
||||
if not appearance_data:
|
||||
print(f"Got AppearanceService req for {agent_id} without knowing about object?")
|
||||
return
|
||||
if slot_name in appearance_data.bake_requests:
|
||||
# We already requested this slot before
|
||||
return
|
||||
appearance_data.bake_requests[slot_name] = AvatarBakeRequest(requested=dt.datetime.now())
|
||||
|
||||
def handle_http_response(self, session_manager: SessionManager, flow: HippoHTTPFlow):
|
||||
if not flow.cap_data:
|
||||
return
|
||||
if flow.cap_data.cap_name != "AppearanceService":
|
||||
return
|
||||
|
||||
agent_id = UUID(flow.request.url.split('/')[-3])
|
||||
slot_name = flow.request.url.split('/')[-2]
|
||||
appearance_data = self.av_appearance_data.get(agent_id)
|
||||
if not appearance_data:
|
||||
return
|
||||
slot_details = appearance_data.bake_requests.get(slot_name)
|
||||
if not slot_details:
|
||||
return
|
||||
slot_details.received = dt.datetime.now()
|
||||
|
||||
|
||||
addons = [AppearanceDelayTrackerAddon()]
|
||||
@@ -719,7 +719,9 @@ class MessageBuilderWindow(QtWidgets.QMainWindow):
|
||||
transport = None
|
||||
off_circuit = self.checkOffCircuit.isChecked()
|
||||
if off_circuit:
|
||||
transport = SocketUDPTransport(socket.socket(socket.AF_INET, socket.SOCK_DGRAM))
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(("0.0.0.0", 0))
|
||||
transport = SocketUDPTransport(sock)
|
||||
region.circuit.send(msg, transport=transport)
|
||||
if off_circuit:
|
||||
transport.close()
|
||||
|
||||
@@ -485,6 +485,25 @@ class InventoryContainerBase(InventoryNodeBase):
|
||||
if x.parent_id == self.node_id
|
||||
)
|
||||
|
||||
@property
|
||||
def descendents(self) -> List[InventoryNodeBase]:
|
||||
new_children: List[InventoryNodeBase] = [self]
|
||||
descendents = []
|
||||
while new_children:
|
||||
to_check = new_children[:]
|
||||
new_children.clear()
|
||||
for obj in to_check:
|
||||
if isinstance(obj, InventoryContainerBase):
|
||||
for child in obj.children:
|
||||
if child in descendents:
|
||||
continue
|
||||
new_children.append(child)
|
||||
descendents.append(child)
|
||||
else:
|
||||
if obj not in descendents:
|
||||
descendents.append(obj)
|
||||
return descendents
|
||||
|
||||
def __getitem__(self, item: Union[int, str]) -> InventoryNodeBase:
|
||||
if isinstance(item, int):
|
||||
return self.children[item]
|
||||
@@ -607,6 +626,9 @@ class InventoryItem(InventoryNodeBase):
|
||||
name: Optional[str] = schema_field(SchemaMultilineStr, default=None)
|
||||
desc: Optional[str] = schema_field(SchemaMultilineStr, default=None)
|
||||
metadata: Optional[Dict[str, Any]] = schema_field(SchemaLLSD, default=None, include_none=True)
|
||||
"""Specifically for script metadata, generally just experience info"""
|
||||
thumbnail: Optional[Dict[str, Any]] = schema_field(SchemaLLSD, default=None, include_none=False)
|
||||
"""Generally just a dict with the thumbnail UUID in it"""
|
||||
creation_date: Optional[dt.datetime] = schema_field(SchemaDate, llsd_name="created_at", default=None)
|
||||
|
||||
__hash__ = InventoryNodeBase.__hash__
|
||||
|
||||
@@ -84,7 +84,7 @@ class Circuit:
|
||||
def send(self, message: Message, transport=None) -> UDPPacket:
|
||||
if self.prepare_message(message):
|
||||
# If the message originates from us then we're responsible for resends.
|
||||
if message.reliable and message.synthetic:
|
||||
if message.reliable and message.synthetic and not transport:
|
||||
self.unacked_reliable[(message.direction, message.packet_id)] = ReliableResendInfo(
|
||||
last_resent=dt.datetime.now(),
|
||||
message=message,
|
||||
|
||||
@@ -57,6 +57,7 @@ _ASSET_TYPE_BIDI: BiDiDict[str] = BiDiDict({
|
||||
@se.enum_field_serializer("AssetUploadComplete", "AssetBlock", "Type")
|
||||
@se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "Type")
|
||||
@se.enum_field_serializer("CreateInventoryItem", "InventoryBlock", "Type")
|
||||
@se.enum_field_serializer("LinkInventoryItem", "InventoryBlock", "Type")
|
||||
@se.enum_field_serializer("RezObject", "InventoryData", "Type")
|
||||
@se.enum_field_serializer("RezScript", "InventoryBlock", "Type")
|
||||
@se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "Type")
|
||||
@@ -143,6 +144,7 @@ _INV_TYPE_BIDI: BiDiDict[str] = BiDiDict({
|
||||
|
||||
@se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "InvType")
|
||||
@se.enum_field_serializer("CreateInventoryItem", "InventoryBlock", "InvType")
|
||||
@se.enum_field_serializer("LinkInventoryItem", "InventoryBlock", "InvType")
|
||||
@se.enum_field_serializer("RezObject", "InventoryData", "InvType")
|
||||
@se.enum_field_serializer("RezScript", "InventoryBlock", "InvType")
|
||||
@se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "InvType")
|
||||
@@ -1982,6 +1984,7 @@ class RegionFlags(IntFlag):
|
||||
ALLOW_VOICE = 1 << 28
|
||||
BLOCK_PARCEL_SEARCH = 1 << 29
|
||||
DENY_AGEUNVERIFIED = 1 << 30
|
||||
DENY_BOTS = 1 << 31
|
||||
|
||||
|
||||
@se.flag_field_serializer("RegionHandshakeReply", "RegionInfo", "Flags")
|
||||
|
||||
@@ -330,7 +330,7 @@ class HippoClientSession(BaseClientSession):
|
||||
super().__init__(id, secure_session_id, agent_id, circuit_code, session_manager, login_data=login_data)
|
||||
self.http_session = session_manager.http_session
|
||||
self.objects = ClientWorldObjectManager(proxify(self), session_manager.settings, None)
|
||||
self.inventory_manager = InventoryManager(proxify(self))
|
||||
self.inventory = InventoryManager(proxify(self))
|
||||
self.transport: Optional[SocketUDPTransport] = None
|
||||
self.protocol: Optional[HippoClientProtocol] = None
|
||||
self.message_handler.take_by_default = False
|
||||
|
||||
@@ -6,16 +6,18 @@ import gzip
|
||||
import itertools
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Union, List, Tuple, Set, Sequence
|
||||
from typing import Union, List, Tuple, Set, Sequence, Dict, TYPE_CHECKING
|
||||
|
||||
from hippolyzer.lib.base import llsd
|
||||
from hippolyzer.lib.base.datatypes import UUID
|
||||
from hippolyzer.lib.base.inventory import InventoryModel, InventoryCategory, InventoryItem, InventoryNodeBase
|
||||
from hippolyzer.lib.base.message.message import Message, Block
|
||||
from hippolyzer.lib.base.templates import AssetType, FolderType, InventoryType, Permissions
|
||||
from hippolyzer.lib.client.state import BaseClientSession
|
||||
from hippolyzer.lib.base.templates import WearableType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from hippolyzer.lib.client.state import BaseClientSession
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -38,6 +40,7 @@ class InventoryManager:
|
||||
self._session.message_handler.subscribe("BulkUpdateInventory", self._handle_bulk_update_inventory)
|
||||
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("RemoveInventoryObjects", self._handle_remove_inventory_objects)
|
||||
self._session.message_handler.subscribe("RemoveInventoryFolder", self._handle_remove_inventory_folder)
|
||||
self._session.message_handler.subscribe("MoveInventoryItem", self._handle_move_inventory_item)
|
||||
self._session.message_handler.subscribe("MoveInventoryFolder", self._handle_move_inventory_folder)
|
||||
@@ -117,6 +120,7 @@ class InventoryManager:
|
||||
# Line-delimited LLSD notation!
|
||||
for line in f.readlines():
|
||||
# TODO: Parsing of invcache is dominated by `parse_notation()`. It's stupidly inefficient.
|
||||
# TODO: sniff out binary LLSD invcaches
|
||||
node_llsd = llsd.parse_notation(line)
|
||||
if first_line:
|
||||
# First line is the file header
|
||||
@@ -137,7 +141,7 @@ class InventoryManager:
|
||||
|
||||
def _handle_bulk_update_inventory(self, msg: Message):
|
||||
any_cats = False
|
||||
for folder_block in msg["FolderData"]:
|
||||
for folder_block in msg.get_blocks("FolderData", ()):
|
||||
if folder_block["FolderID"] == UUID.ZERO:
|
||||
continue
|
||||
any_cats = True
|
||||
@@ -147,7 +151,7 @@ class InventoryManager:
|
||||
# and hasn't just moved.
|
||||
update_fields={"parent_id", "name", "pref_type"},
|
||||
)
|
||||
for item_block in msg["ItemData"]:
|
||||
for item_block in msg.get_blocks("ItemData", ()):
|
||||
if item_block["ItemID"] == UUID.ZERO:
|
||||
continue
|
||||
self.model.upsert(InventoryItem.from_inventory_data(item_block))
|
||||
@@ -178,6 +182,17 @@ class InventoryManager:
|
||||
if node:
|
||||
self.model.unlink(node)
|
||||
|
||||
def _handle_remove_inventory_objects(self, msg: Message):
|
||||
self._validate_recipient(msg["AgentData"]["AgentID"])
|
||||
for item_block in msg.get_blocks("ItemData", []):
|
||||
node = self.model.get(item_block["ItemID"])
|
||||
if node:
|
||||
self.model.unlink(node)
|
||||
for folder_block in msg.get_blocks("FolderData", []):
|
||||
node = self.model.get(folder_block["FolderID"])
|
||||
if node:
|
||||
self.model.unlink(node)
|
||||
|
||||
def _handle_move_inventory_item(self, msg: Message):
|
||||
for inventory_block in msg["InventoryData"]:
|
||||
node = self.model.get(inventory_block["ItemID"])
|
||||
@@ -341,6 +356,58 @@ class InventoryManager:
|
||||
await self._session.main_region.circuit.send_reliable(msg)
|
||||
node.parent_id = new_parent
|
||||
|
||||
async def copy(self, node: InventoryNodeBase, destination: UUID | InventoryCategory, contents: bool = True)\
|
||||
-> InventoryItem | InventoryCategory:
|
||||
destination = _get_node_id(destination)
|
||||
if isinstance(node, InventoryItem):
|
||||
with self._session.main_region.message_handler.subscribe_async(
|
||||
("BulkUpdateInventory",),
|
||||
# Not ideal, but there doesn't seem to be an easy way to determine the transaction ID,
|
||||
# and using the callback ID seems a bit crap.
|
||||
predicate=lambda x: x["ItemData"]["Name"] == node.name,
|
||||
take=False,
|
||||
) as get_msg:
|
||||
await self._session.main_region.circuit.send_reliable(Message(
|
||||
'CopyInventoryItem',
|
||||
Block('AgentData', AgentID=self._session.agent_id, SessionID=self._session.id),
|
||||
Block(
|
||||
'InventoryData',
|
||||
CallbackID=0,
|
||||
OldAgentID=self._session.agent_id,
|
||||
OldItemID=node.item_id,
|
||||
NewFolderID=destination,
|
||||
NewName=b''
|
||||
)
|
||||
))
|
||||
msg = await asyncio.wait_for(get_msg(), 5.0)
|
||||
# BulkInventoryUpdate message may not have already been handled internally, do it manually.
|
||||
self._handle_bulk_update_inventory(msg)
|
||||
|
||||
# Now pull the item out of the inventory
|
||||
new_item = self.model.get(msg["ItemData"]["ItemID"])
|
||||
assert new_item is not None
|
||||
return new_item # type: ignore
|
||||
elif isinstance(node, InventoryCategory):
|
||||
# Keep a list of the original descendents in case we're copy a folder within itself
|
||||
to_copy = list(node.descendents)
|
||||
# There's not really any way to "copy" a category, we just create a new one with the same properties.
|
||||
new_cat = await self.create_folder(destination, node.name, node.pref_type)
|
||||
if contents:
|
||||
cat_lookup: Dict[UUID, UUID] = {node.node_id: new_cat.node_id}
|
||||
# Recreate the category hierarchy first, keeping note of the new category IDs.
|
||||
for node in to_copy:
|
||||
if isinstance(node, InventoryCategory):
|
||||
new_parent = cat_lookup[node.parent_id]
|
||||
cat_lookup[node.node_id] = (await self.copy(node, new_parent, contents=False)).node_id
|
||||
# Items have to be explicitly copied individually
|
||||
for node in to_copy:
|
||||
if isinstance(node, InventoryItem):
|
||||
new_parent = cat_lookup[node.parent_id]
|
||||
await self.copy(node, new_parent, contents=False)
|
||||
return new_cat
|
||||
else:
|
||||
raise ValueError(f"Unknown node type: {node!r}")
|
||||
|
||||
async def update(self, node: InventoryNodeBase, data: dict) -> None:
|
||||
path = f"/category/{node.node_id}"
|
||||
if isinstance(node, InventoryItem):
|
||||
|
||||
@@ -15,7 +15,7 @@ from typing import *
|
||||
|
||||
from hippolyzer.lib.base.datatypes import UUID, Vector3
|
||||
from hippolyzer.lib.base.helpers import proxify
|
||||
from hippolyzer.lib.base.inventory import InventoryItem, InventoryModel
|
||||
from hippolyzer.lib.base.inventory import InventoryItem, InventoryModel, InventoryObject
|
||||
from hippolyzer.lib.base.message.message import Block, Message
|
||||
from hippolyzer.lib.base.message.message_handler import MessageHandler
|
||||
from hippolyzer.lib.base.message.msgtypes import PacketFlags
|
||||
@@ -229,7 +229,18 @@ class ClientObjectManager:
|
||||
async def request_object_inv_via_cap(self, obj: Object) -> List[InventoryItem]:
|
||||
async with self._region.caps_client.get("RequestTaskInventory", params={"task_id": obj.FullID}) as resp:
|
||||
resp.raise_for_status()
|
||||
return [InventoryItem.from_llsd(x) for x in (await resp.read_llsd())["contents"]]
|
||||
all_items = [InventoryItem.from_llsd(x) for x in (await resp.read_llsd())["contents"]]
|
||||
# Synthesize the Contents directory so the items can have a parent
|
||||
parent = InventoryObject(
|
||||
obj_id=obj.FullID,
|
||||
name="Contents",
|
||||
)
|
||||
model = InventoryModel()
|
||||
model.add(parent)
|
||||
for item in all_items:
|
||||
model.add(item)
|
||||
|
||||
return all_items
|
||||
|
||||
async def request_object_inv_via_xfer(self, obj: Object) -> List[InventoryItem]:
|
||||
session = self._region.session()
|
||||
@@ -370,6 +381,10 @@ class ClientWorldObjectManager:
|
||||
futs.extend(region_mgr.request_object_properties(region_objs))
|
||||
return futs
|
||||
|
||||
async def request_object_inv(self, obj: Object) -> List[InventoryItem]:
|
||||
region_mgr = self._get_region_manager(obj.RegionHandle)
|
||||
return await region_mgr.request_object_inv(obj)
|
||||
|
||||
async def load_ancestors(self, obj: Object, wait_time: float = 1.0):
|
||||
"""
|
||||
Ensure that the entire chain of parents above this object is loaded
|
||||
|
||||
@@ -18,6 +18,7 @@ from hippolyzer.lib.base.network.caps_client import CapsClient
|
||||
from hippolyzer.lib.base.network.transport import ADDR_TUPLE
|
||||
from hippolyzer.lib.base.objects import handle_to_global_pos
|
||||
from hippolyzer.lib.base.xfer_manager import XferManager
|
||||
from hippolyzer.lib.client.inventory_manager import InventoryManager
|
||||
|
||||
from hippolyzer.lib.client.object_manager import ClientObjectManager, ClientWorldObjectManager
|
||||
|
||||
@@ -91,6 +92,7 @@ class BaseClientSession(abc.ABC):
|
||||
region_by_handle: Callable[[int], Optional[BaseClientRegion]]
|
||||
region_by_circuit_addr: Callable[[ADDR_TUPLE], Optional[BaseClientRegion]]
|
||||
objects: ClientWorldObjectManager
|
||||
inventory: InventoryManager
|
||||
login_data: Dict[str, Any]
|
||||
REGION_CLS = Type[BaseClientRegion]
|
||||
|
||||
|
||||
@@ -341,10 +341,15 @@ class MITMProxyEventManager:
|
||||
msg.sender = region.circuit_addr
|
||||
msg.direction = Direction.IN
|
||||
|
||||
try:
|
||||
session.message_handler.handle(msg)
|
||||
except:
|
||||
LOG.exception("Failed while handling EQ message for session")
|
||||
|
||||
try:
|
||||
region.message_handler.handle(msg)
|
||||
except:
|
||||
LOG.exception("Failed while handling EQ message")
|
||||
LOG.exception("Failed while handling EQ message for region")
|
||||
|
||||
handle_event = AddonManager.handle_eq_event(session, region, event)
|
||||
if handle_event is True:
|
||||
|
||||
@@ -158,6 +158,12 @@ class ProxyInventoryManager(InventoryManager):
|
||||
await super().move(node, new_parent)
|
||||
await self._session.main_region.circuit.send_reliable(self._craft_update_message(node))
|
||||
|
||||
async def copy(self, node: InventoryNodeBase, destination: UUID | InventoryCategory, contents: bool = True)\
|
||||
-> InventoryCategory | InventoryItem:
|
||||
ret_node = await super().copy(node, destination, contents)
|
||||
await self._session.main_region.circuit.send_reliable(self._craft_update_message(node))
|
||||
return ret_node
|
||||
|
||||
def _craft_removal_message(self, node: InventoryNodeBase) -> Message:
|
||||
is_folder = True
|
||||
if isinstance(node, InventoryItem):
|
||||
|
||||
@@ -7,6 +7,7 @@ import copy
|
||||
import fnmatch
|
||||
import gzip
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import pickle
|
||||
import re
|
||||
@@ -507,6 +508,8 @@ class HTTPMessageLogEntry(AbstractMessageLogEntry):
|
||||
raise
|
||||
elif any(content_type.startswith(x) for x in ("application/xml", "text/xml")):
|
||||
beautified = self._format_xml(message.content)
|
||||
elif "json" in content_type:
|
||||
beautified = json.dumps(json.loads(message.content), indent=2)
|
||||
except:
|
||||
LOG.exception("Failed to beautify message")
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class Session(BaseClientSession):
|
||||
regions: MutableSequence[ProxiedRegion]
|
||||
inventory: ProxyInventoryManager
|
||||
region_by_handle: Callable[[int], Optional[ProxiedRegion]]
|
||||
region_by_circuit_addr: Callable[[ADDR_TUPLE], Optional[ProxiedRegion]]
|
||||
main_region: Optional[ProxiedRegion]
|
||||
|
||||
@@ -26,24 +26,31 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"aiohttp<4.0.0",
|
||||
"arpeggio",
|
||||
"defusedxml",
|
||||
"gltflib",
|
||||
"Glymur<0.9.7",
|
||||
"idna<3,>=2.5",
|
||||
"lazy-object-proxy",
|
||||
"llsd<1.1.0",
|
||||
"mitmproxy>=11.0.0,<12",
|
||||
"numpy<2.0",
|
||||
"outleap<1.0",
|
||||
"ptpython<4.0",
|
||||
"pycollada",
|
||||
"pyside6-essentials",
|
||||
"qasync",
|
||||
"recordclass>=0.23.1,<0.24",
|
||||
"transformations",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
proxy = [
|
||||
"arpeggio",
|
||||
"mitmproxy>=11.0.0,<12",
|
||||
"outleap<1.0",
|
||||
"ptpython<4.0",
|
||||
"Werkzeug<4.0",
|
||||
]
|
||||
gui = [
|
||||
"hippolyzer[proxy]",
|
||||
"pyside6-essentials",
|
||||
"qasync",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
|
||||
@@ -159,7 +159,7 @@ class TestHippoClient(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def test_inventory_manager(self):
|
||||
await self._log_client_in(self.client)
|
||||
self.assertEqual(self.client.session.inventory_manager.model.root.node_id, UUID(int=4))
|
||||
self.assertEqual(self.client.session.inventory.model.root.node_id, UUID(int=4))
|
||||
|
||||
async def test_resend_suppression(self):
|
||||
"""Make sure the client only handles the first seen copy of a reliable message"""
|
||||
|
||||
Reference in New Issue
Block a user