OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / autoloads / keet_service.gd

182 lines · gd ref:
extends Node
## Global singleton owning the UDS client and all managers.
## Daemon lifecycle is handled by the Pear launcher (index.js).
## Godot just connects to the socket with auto-reconnect.

# --- Global Registry (hardcoded) ---
# The public "discovery" registry every user auto-joins on first launch.
# Anyone holding GLOBAL_REGISTRY_INVITE can subscribe (read-only). To list
# new rooms in the global registry, the curator hands out a separate WRITE
# invite. To rotate the registry, ship a new build with a new invite string.
# Bootstrap on tabertron: see docs/bootstrap-global-registry.md.
const GLOBAL_REGISTRY_ROOM_ID := "8yphx5a55cen3cwzt34z5mcoc5bi31agguuaijjjqmnbimr676eo"
const GLOBAL_REGISTRY_INVITE  := "dnyuog687hp7srbcskmah7m7isegpo4h3cddj3hkwwwzfty4i1xq9rmboacsam5wdxcy8zp4wobfkzooqqojuoxb4db699a1m1wrcgrzjgwqif6hptmtetbgnrebe3mssnbeixda61kkzowfhi4tpsjsa63khymdrhsft8m8uq8p4mwghmsa5o1ikcmci8g397n31wb9mzzkbsostybitto3itghfndqdffwiowagz98qmg4"

signal room_selected(room_id: String, title: String)
signal daemon_status_changed(status: String)  # "connecting", "running", "disconnected"
signal group_view_changed(group_id: String)  # "" = home view, else registry key

var _global_registry_attempted: bool = false

var client: KeetUdsClient
var room_mgr: KeetRoom
var chat: KeetChat
var identity: KeetIdentity
var identity_mgr: KeetIdentityManager
var feed: KeetFeed
var file_mgr: KeetFile
var discovery: KeetDiscovery
var dm: KeetDM
var graph_mgr: KeetGraph

var _room_cache: Dictionary = {}  # room_id -> title
var _setup_shown: bool = false


func _ready() -> void:
	client = KeetUdsClient.new()
	client.name = "Client"
	client.auto_connect = true  # Auto-connect + retry every 5s
	add_child(client)

	room_mgr = KeetRoom.new()
	room_mgr.name = "RoomMgr"
	room_mgr.client = client
	add_child(room_mgr)

	chat = KeetChat.new()
	chat.name = "Chat"
	chat.client = client
	add_child(chat)

	identity = KeetIdentity.new()
	identity.name = "Identity"
	identity.client = client
	add_child(identity)

	identity_mgr = KeetIdentityManager.new()
	identity_mgr.name = "IdentityMgr"
	identity_mgr.client = client
	add_child(identity_mgr)

	feed = KeetFeed.new()
	feed.name = "Feed"
	feed.client = client
	add_child(feed)

	file_mgr = KeetFile.new()
	file_mgr.name = "FileMgr"
	file_mgr.client = client
	add_child(file_mgr)

	discovery = KeetDiscovery.new()
	discovery.name = "Discovery"
	discovery.client = client
	add_child(discovery)

	dm = KeetDM.new()
	dm.name = "DM"
	dm.client = client
	add_child(dm)

	graph_mgr = KeetGraph.new()
	graph_mgr.name = "GraphMgr"
	graph_mgr.client = client
	add_child(graph_mgr)

	client.connected.connect(_on_connected)
	client.disconnected.connect(_on_disconnected)
	client.event_received.connect(_on_event)
	room_mgr.rooms_loaded.connect(_on_rooms_loaded)

	daemon_status_changed.emit("connecting")


func select_room(room_id: String) -> void:
	var title: String = _room_cache.get(room_id, "(untitled)")
	room_selected.emit(room_id, title)


func _on_connected() -> void:
	daemon_status_changed.emit("running")
	room_mgr.list_rooms()
	_maybe_join_global_registry()
	if _setup_shown:
		return
	# Check if identity needs first-run or vault setup
	identity.get_identity(func(ident):
		if _setup_shown:
			return
		var username: String = ident.get("username", "") if ident is Dictionary else ""
		var display_name: String = ident.get("displayName", "") if ident is Dictionary else ""
		print("[keet] identity check: username=%s displayName=%s" % [username, display_name])
		# ALWAYS check vault — legacy identities may have username but no vault
		identity.vault_status(func(vault):
			if _setup_shown:
				return
			var has_vault: bool = vault.get("exists", false) if vault is Dictionary else false
			print("[keet] vault check: exists=%s username=%s → show_dialog=%s" % [str(has_vault), username, str(username == "" or not has_vault)])
			if username == "" or not has_vault:
				_show_setup_dialog(has_vault, username)
		)
	)


## Try to join the hardcoded global registry room exactly once per session.
## No-op if either constant is empty (pre-bootstrap), or if the user has
## already joined. Failures are non-fatal — logged + swallowed.
func _maybe_join_global_registry() -> void:
	if _global_registry_attempted:
		return
	if GLOBAL_REGISTRY_ROOM_ID == "" or GLOBAL_REGISTRY_INVITE == "":
		return  # not bootstrapped yet
	_global_registry_attempted = true
	# Check whether we're already in the registry.
	discovery.list_registries(func(regs: Array):
		for r in regs:
			var rid: String = r.get("roomId", "") if r is Dictionary else ""
			if rid == GLOBAL_REGISTRY_ROOM_ID:
				return  # already joined
		# Not in — join via the readOnly invite.
		client.call_rpc("room.join", {"invite": GLOBAL_REGISTRY_INVITE}, func(_result, err):
			if err != null:
				print("[keet] global-registry join failed: ", err)
			else:
				print("[keet] joined global registry ", GLOBAL_REGISTRY_ROOM_ID)
				room_mgr.list_rooms()  # refresh so UI picks it up
		)
	)


func _show_setup_dialog(has_vault: bool = false, existing_username: String = "") -> void:
	if _setup_shown:
		return
	_setup_shown = true
	var dialog_scene := preload("res://scenes/identity_setup_dialog.tscn")
	var dialog: Window = dialog_scene.instantiate()
	dialog.has_vault = has_vault
	dialog.existing_username = existing_username
	get_tree().root.add_child(dialog)
	dialog.popup_centered()


func _on_disconnected() -> void:
	daemon_status_changed.emit("disconnected")


func _on_event(event_name: String, _data: Dictionary) -> void:
	if event_name == "core_swapped":
		# Identity was switched — re-fetch everything
		identity.get_identity()
		room_mgr.list_rooms()


func _on_rooms_loaded(rooms: Array) -> void:
	_room_cache.clear()
	for room: Variant in rooms:
		if room is Dictionary:
			var rid: String = room.get("roomId", "")
			var title: String = room.get("title", "(untitled)")
			if rid != "":
				_room_cache[rid] = title