feat: chat sidebar and inbox, feat: saved chats, fix: wake jobs, fix: sandbox sends, ux: negotiation replies and draft box

This commit is contained in:
2026-05-06 22:53:19 -04:00
parent 58a57ddc6a
commit 3b6e3c34d5
18 changed files with 1797 additions and 105 deletions
+80 -5
View File
@@ -129,9 +129,15 @@ UEX_DELETE_RESOURCES = {
UEX_RESOURCE_DESCRIPTIONS = {
"commodities_prices_history": "Historical commodity prices at a terminal. Requires id_terminal and id_commodity; accepts game_version. UEX limits this to 500 rows.",
"marketplace_prices_history": "Historical marketplace price snapshots, one row per listing per price change. Requires at least one filter; supports date_start/date_end and up to 1000 records.",
"marketplace_trends": "Current UEX marketplace trend metrics for an item. Use this when the user asks for trends, price movement, demand, or what the market is doing now.",
"currencies_index_history": "Historical UEX currency index snapshots with basket component detail. Supports currency, date_from, and date_to timestamps.",
}
UEX_PRODUCTION_WRITE_RESOURCES = {
"marketplace_advertise",
"marketplace_negotiations_messages",
}
@dataclass
class PendingAction:
@@ -266,7 +272,7 @@ class ToolRegistry:
"message": {"type": "string"},
"hash": {"type": "string"},
"id_negotiation": {"type": "integer"},
"is_production": {"type": "integer", "enum": [0, 1], "default": 0},
"is_production": {"type": "integer", "enum": [0, 1], "default": 1},
},
},
},
@@ -298,7 +304,7 @@ class ToolRegistry:
"in_stock": {"type": "integer"},
"hours_expiration": {"type": "integer"},
"is_hidden": {"type": "integer", "enum": [0, 1]},
"is_production": {"type": "integer", "enum": [0, 1], "default": 0},
"is_production": {"type": "integer", "enum": [0, 1], "default": 1},
},
},
},
@@ -382,7 +388,7 @@ class ToolRegistry:
return {"error": f"Pending action not found: {action_id}"}
if action.method == "DELETE":
return await self.uex.delete(action.endpoint, action.payload, authenticated=True)
return await self.uex.post(action.endpoint, action.payload, authenticated=True)
return await self.uex.post(action.endpoint, self._production_payload(action.endpoint, action.payload), authenticated=True)
async def decline(self, action_id: str) -> dict[str, Any]:
action = self.pending_actions.pop(action_id, None)
@@ -915,7 +921,13 @@ class ToolRegistry:
id_listing: int | None = None,
hash: str | None = None,
) -> dict[str, Any]:
return await self.uex.get("marketplace_negotiations", {"id": id, "id_listing": id_listing, "hash": hash}, authenticated=True)
response = await self.uex.get("marketplace_negotiations", {"id": id, "id_listing": id_listing, "hash": hash}, authenticated=True)
negotiations = [
self._summarize_negotiation(item)
for item in self._as_list(response.get("data"))
if isinstance(item, dict)
]
return {**response, "data": negotiations, "negotiations": negotiations}
async def get_negotiation_messages(self, hash: str | None = None, id_negotiation: int | None = None) -> dict[str, Any]:
return await self.uex.get("marketplace_negotiations_messages", {"hash": hash, "id_negotiation": id_negotiation}, authenticated=True)
@@ -925,7 +937,7 @@ class ToolRegistry:
message: str,
hash: str | None = None,
id_negotiation: int | None = None,
is_production: int = 0,
is_production: int = 1,
) -> dict[str, Any]:
payload = {"message": message, "hash": hash, "id_negotiation": id_negotiation, "is_production": is_production}
return self._pending("Send negotiation message", "marketplace_negotiations_messages", payload)
@@ -971,6 +983,7 @@ class ToolRegistry:
def _pending(self, label: str, endpoint: str, payload: dict[str, Any], method: str = "POST") -> dict[str, Any]:
action_id = str(uuid.uuid4())
payload = {key: value for key, value in payload.items() if value is not None}
payload = self._production_payload(endpoint, payload)
self.pending_actions[action_id] = PendingAction(action_id, label, endpoint, payload, method)
return {
"pending_action": {
@@ -983,6 +996,14 @@ class ToolRegistry:
}
}
@staticmethod
def _production_payload(endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
if endpoint not in UEX_PRODUCTION_WRITE_RESOURCES:
return payload
next_payload = dict(payload)
next_payload["is_production"] = 1
return next_payload
@staticmethod
def _validate_resource(resource: str, allowed: dict[str, Any] | set[str]) -> str:
normalized = resource.strip().strip("/").casefold()
@@ -1167,3 +1188,57 @@ class ToolRegistry:
"advertiser": listing.get("user_username"),
"expires_at": listing.get("date_expiration"),
}
@classmethod
def _summarize_negotiation(cls, negotiation: dict[str, Any]) -> dict[str, Any]:
summary = cls._project_item(negotiation, mode="summary")
state = cls._negotiation_state(negotiation)
summary.update(
{
"state": state["state"],
"is_open": state["is_open"],
"state_reason": state["reason"],
}
)
for key in ("hash", "id_listing", "id_user", "id_user_seller", "id_user_buyer", "date_closed"):
if key in negotiation and key not in summary:
summary[key] = negotiation.get(key)
return summary
@staticmethod
def _negotiation_state(negotiation: dict[str, Any]) -> dict[str, Any]:
closed_flags = [
"is_closed",
"closed",
"is_cancelled",
"is_canceled",
"is_archived",
"marked_closed",
]
for key in closed_flags:
value = negotiation.get(key)
if value in (True, 1, "1", "true", "True", "yes", "closed"):
return {"state": "closed", "is_open": False, "reason": f"{key} is set"}
closed_dates = [
"date_closed",
"date_completed",
"date_cancelled",
"date_canceled",
"closed_at",
"completed_at",
"cancelled_at",
"canceled_at",
]
for key in closed_dates:
value = negotiation.get(key)
if value not in (None, "", 0, "0", False):
return {"state": "closed", "is_open": False, "reason": f"{key} is populated"}
status = str(negotiation.get("status") or negotiation.get("state") or "").casefold()
if status in {"closed", "cancelled", "canceled", "completed", "declined", "accepted", "rejected"}:
return {"state": "closed", "is_open": False, "reason": f"status is {status}"}
if status in {"open", "active", "pending", "new"}:
return {"state": "open", "is_open": True, "reason": f"status is {status}"}
return {"state": "open", "is_open": True, "reason": "no closed flag, closed date, or closed status was present"}