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