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:
+80
-5
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user