extends RefCounted class_name DiscoveryApiClient ## Helper client for AGP Discovery REST endpoints (Browse, Search, Categories, Visit tracking). ## ## docs/planning/discovery.md (task 56c49440d0a6). class WorldSummary extends RefCounted: var id: int = 0 var name: String = "" var description: String = "" var category: String = "" var tags: Array = [] var play_count: int = 0 var created_at: String = "" var content_guidelines: String = "" var owner_id: int = 0 var owner_username: String = "" static func from_dict(d: Dictionary) -> WorldSummary: var w := WorldSummary.new() w.id = int(d.get("id", 0)) w.name = str(d.get("name", "")) w.description = str(d.get("description", "")) if d.get("description") != null else "" w.category = str(d.get("category", "")) if d.get("category") != null else "" w.content_guidelines = str(d.get("content_guidelines", "")) if d.get("content_guidelines") != null else "" var raw_tags = d.get("tags", []) w.tags = raw_tags if raw_tags is Array else [] w.play_count = int(d.get("play_count", 0)) w.created_at = str(d.get("created_at", "")) if d.get("created_at") != null else "" var owner: Dictionary = d.get("owner", {}) if d.get("owner") is Dictionary else {} w.owner_id = int(owner.get("id", 0)) w.owner_username = str(owner.get("username", "")) return w class DiscoveryResult extends RefCounted: var items: Array[WorldSummary] = [] var total: int = 0 var skip: int = 0 var limit: int = 50 static func from_dict(d: Dictionary) -> DiscoveryResult: var res := DiscoveryResult.new() res.total = int(d.get("total", 0)) res.skip = int(d.get("skip", 0)) res.limit = int(d.get("limit", 50)) var raw_items = d.get("items", []) if raw_items is Array: for item in raw_items: if item is Dictionary: res.items.append(WorldSummary.from_dict(item)) return res static func get_api_client(node: Node) -> Node: if node.has_node("/root/ApiClient"): return node.get_node("/root/ApiClient") return null ## Fetch worlds with search, tags, category filter, sort, and pagination. ## Attaches JWT token if client has one; works anonymously without token. ## Surfaces transport/HTTP errors in the standard result dictionary. static func get_worlds( api: Node, q: String = "", tags: Array = [], category: String = "", sort: String = "newest", skip: int = 0, limit: int = 50 ) -> Dictionary: var params: Array[String] = [] if q != "": params.append("q=" + q.uri_encode()) if tags.size() > 0: var tag_strs: Array[String] = [] for t in tags: tag_strs.append(str(t)) params.append("tags=" + ",".join(tag_strs).uri_encode()) if category != "": params.append("category=" + category.uri_encode()) if sort != "": params.append("sort=" + sort.uri_encode()) params.append("skip=%d" % skip) params.append("limit=%d" % limit) var url := "/api/discovery/worlds" if params.size() > 0: url += "?" + "&".join(params) var custom_headers := PackedStringArray() if api != null and api.has_method("has_token") and api.has_token(): custom_headers.append("Authorization: " + api.bearer_token) var res: Dictionary = await api.request( HTTPClient.METHOD_GET, url, "", "application/json", false, custom_headers ) if res.ok and res.data is Dictionary: res["parsed"] = DiscoveryResult.from_dict(res.data) return res ## Fetch distinct categories of public worlds. static func get_categories(api: Node) -> Dictionary: var custom_headers := PackedStringArray() if api != null and api.has_method("has_token") and api.has_token(): custom_headers.append("Authorization: " + api.bearer_token) return await api.request( HTTPClient.METHOD_GET, "/api/discovery/categories", "", "application/json", false, custom_headers ) ## Record a visit to a world. Supports optional anonymous token or logged in user. static func record_visit(api: Node, world_id: int, anonymous_token: String = "") -> Dictionary: var custom_headers := PackedStringArray() if api != null and api.has_method("has_token") and api.has_token(): custom_headers.append("Authorization: " + api.bearer_token) var payload := {} if anonymous_token != "": payload["anonymous_token"] = anonymous_token return await api.request( HTTPClient.METHOD_POST, "/api/discovery/worlds/%d/visit" % world_id, JSON.stringify(payload) if payload.size() > 0 else "", "application/json", false, custom_headers )