OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / hyper-social / keet-social / addons / keet_social / keet_client.gd

151 lines · gd ref:
class_name KeetClient
extends Node
## Core HTTP client for keet-daemon (localhost:3117).
## Provides JSON-RPC 2.0 calls and connection management.

signal connected
signal disconnected
signal error_occurred(message: String)
signal status_updated(status: Dictionary)

@export var host: String = "127.0.0.1"
@export var port: int = 3117
@export var auto_connect: bool = true
@export_range(1.0, 30.0) var health_interval: float = 10.0

var is_connected: bool = false

var _base_url: String
var _health_timer: Timer
var _pending_requests: int = 0


func _ready() -> void:
	_base_url = "http://%s:%d" % [host, port]
	_health_timer = Timer.new()
	_health_timer.wait_time = health_interval
	_health_timer.timeout.connect(_check_health)
	add_child(_health_timer)
	if auto_connect:
		check_connection()
		_health_timer.start()


## Make a JSON-RPC call to keet-daemon.
## Returns result via callback(result, error).
func rpc(method: String, params: Dictionary = {}, callback: Callable = Callable()) -> void:
	var req := HTTPRequest.new()
	req.timeout = 10.0
	add_child(req)
	var body := JSON.stringify({"method": method, "params": params})
	req.request_completed.connect(_on_rpc_response.bind(callback, req))
	_pending_requests += 1
	var err := req.request(
		_base_url + "/rpc",
		["Content-Type: application/json"],
		HTTPClient.METHOD_POST,
		body
	)
	if err != OK:
		_pending_requests -= 1
		req.queue_free()
		if callback.is_valid():
			callback.call(null, "Request failed: %d" % err)


## Convenience: async RPC via await. Returns {"result": ..., "error": ...}.
func rpc_async(method: String, params: Dictionary = {}) -> Dictionary:
	var response := {"result": null, "error": null}
	var done := false
	rpc(method, params, func(result, err):
		response.result = result
		response.error = err
		done = true
	)
	while not done:
		await get_tree().process_frame
	return response


## GET a convenience endpoint (e.g. "/rpc/rooms", "/rpc/identity").
func get_endpoint(path: String, callback: Callable = Callable()) -> void:
	var req := HTTPRequest.new()
	req.timeout = 10.0
	add_child(req)
	req.request_completed.connect(_on_get_response.bind(callback, req))
	var err := req.request(_base_url + path)
	if err != OK:
		req.queue_free()
		if callback.is_valid():
			callback.call(null, "GET failed: %d" % err)


## Check if keet-daemon is reachable.
func check_connection() -> void:
	rpc("status", {}, func(result, err):
		var was_connected := is_connected
		if err != null:
			is_connected = false
			if was_connected:
				disconnected.emit()
		else:
			is_connected = true
			if not was_connected:
				connected.emit()
			status_updated.emit(result if result is Dictionary else {})
	)


func _on_rpc_response(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray, callback: Callable, req: HTTPRequest) -> void:
	_pending_requests -= 1
	req.queue_free()

	if result != HTTPRequest.RESULT_SUCCESS:
		_handle_disconnect()
		if callback.is_valid():
			callback.call(null, "HTTP error: %d" % result)
		return

	if response_code < 200 or response_code >= 300:
		if callback.is_valid():
			callback.call(null, "HTTP %d" % response_code)
		return

	var text := body.get_string_from_utf8()
	var data = JSON.parse_string(text)
	if data == null:
		if callback.is_valid():
			callback.call(null, "Invalid JSON")
		return

	if not is_connected:
		is_connected = true
		connected.emit()

	if callback.is_valid():
		callback.call(data.get("result"), data.get("error"))


func _on_get_response(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray, callback: Callable, req: HTTPRequest) -> void:
	req.queue_free()

	if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
		if callback.is_valid():
			callback.call(null, "GET failed")
		return

	var text := body.get_string_from_utf8()
	var data = JSON.parse_string(text)
	if callback.is_valid():
		callback.call(data, null)


func _check_health() -> void:
	check_connection()


func _handle_disconnect() -> void:
	if is_connected:
		is_connected = false
		disconnected.emit()