class_name PlayerRelationshipStore extends Node signal relationship_changed(fingerprint: String) const FORMAT_VERSION := 2 const MAX_RECORDS := 500 const MAX_FRIENDS := 200 const SOCIAL_TOKEN_LENGTH := 64 const PRESENCE_CHANNEL_DOMAIN := "straywild_PRESENCE_V1:" const INVITE_INBOX_DOMAIN := "straywild_INVITE_V1:" var _records: Dictionary = {} var _loaded := false var _write_blocked := false var _store_path := "" var _expected_hash := "" var _data_root: PlayerDataRoot func configure_storage(path: String, data_root: PlayerDataRoot) -> void: _store_path = path _data_root = data_root _records.clear() _loaded = false _write_blocked = false _expected_hash = "" func is_muted(fingerprint: String) -> bool: _ensure_loaded() return bool(_records.get(fingerprint, {}).get("muted", false)) func is_blocked(fingerprint: String) -> bool: _ensure_loaded() return bool(_records.get(fingerprint, {}).get("blocked", false)) func is_friend(fingerprint: String) -> bool: _ensure_loaded() return bool(_records.get(fingerprint, {}).get("friend", false)) func set_muted(fingerprint: String, display_name: String, value: bool) -> bool: if not _valid_target(fingerprint, display_name): return false _ensure_loaded() var record := _record(fingerprint, display_name) record["muted"] = value or bool(record.get("blocked", false)) return _commit(fingerprint, record) func set_blocked(fingerprint: String, display_name: String, value: bool) -> bool: if not _valid_target(fingerprint, display_name): return false _ensure_loaded() var record := _record(fingerprint, display_name) record["blocked"] = value # Blocking owns the accompanying mute. Removing the block establishes a # fresh visibility boundary for future messages; already suppressed # messages retain their immutable local suppression flag. record["muted"] = value if value: _clear_friend_fields(record) return _commit(fingerprint, record) func create_friend_capabilities() -> Dictionary: var presence_write_token := NetworkIdentityCrypto.secure_id(32) var invite_token := NetworkIdentityCrypto.secure_id(32) if not _valid_social_token(presence_write_token) or not _valid_social_token(invite_token): return {} return { "local_presence_write_token": presence_write_token, "local_invite_token": invite_token, "presence_channel": presence_channel_for_write_token( presence_write_token ), "invite_token": invite_token, } func add_friend( fingerprint: String, display_name: String, local_capabilities: Dictionary, remote_capabilities: Dictionary, ) -> bool: if ( not _valid_target(fingerprint, display_name) or is_blocked(fingerprint) or not _valid_local_capabilities(local_capabilities) or not _valid_public_capabilities(remote_capabilities) ): return false _ensure_loaded() if not is_friend(fingerprint) and get_friends().size() >= MAX_FRIENDS: return false var record := _record(fingerprint, display_name) var now := int(Time.get_unix_time_from_system()) record["friend"] = true record["friend_since_unix"] = int(record.get("friend_since_unix", now)) record["local_presence_write_token"] = str( local_capabilities["local_presence_write_token"] ) record["local_invite_token"] = str( local_capabilities["local_invite_token"] ) record["remote_presence_channel"] = str( remote_capabilities["presence_channel"] ) record["remote_invite_token"] = str( remote_capabilities["invite_token"] ) return _commit(fingerprint, record) func remove_friend(fingerprint: String, display_name: String) -> bool: if not _valid_target(fingerprint, display_name): return false _ensure_loaded() var record := _record(fingerprint, display_name) _clear_friend_fields(record) return _commit(fingerprint, record) func get_friend_record(fingerprint: String) -> Dictionary: _ensure_loaded() var record: Dictionary = _records.get(fingerprint, {}) return record.duplicate(true) if bool(record.get("friend", false)) else {} func get_friends() -> Array[Dictionary]: _ensure_loaded() var result: Array[Dictionary] = [] for value: Dictionary in _records.values(): if bool(value.get("friend", false)): result.append(value.duplicate(true)) _sort_records(result) return result static func presence_channel_for_write_token(write_token: String) -> String: return (PRESENCE_CHANNEL_DOMAIN + write_token).sha256_text() static func invite_inbox_id(invite_token: String) -> String: return (INVITE_INBOX_DOMAIN + invite_token).sha256_text() func get_records() -> Array[Dictionary]: _ensure_loaded() var result: Array[Dictionary] = [] for value: Dictionary in _records.values(): if bool(value.get("muted", false)) or bool(value.get("blocked", false)): result.append(value.duplicate(true)) _sort_records(result) return result func _record(fingerprint: String, display_name: String) -> Dictionary: var now := int(Time.get_unix_time_from_system()) var record: Dictionary = _records.get(fingerprint, { "fingerprint": fingerprint, "created_unix": now, "muted": false, "blocked": false, "friend": false, }) record["last_known_display_name"] = display_name record["updated_unix"] = now return record func _commit(fingerprint: String, record: Dictionary) -> bool: if _write_blocked: return false var previous: Dictionary = _records.duplicate(true) if ( not bool(record.get("muted", false)) and not bool(record.get("blocked", false)) and not bool(record.get("friend", false)) ): _records.erase(fingerprint) else: _records[fingerprint] = record while _records.size() > MAX_RECORDS: _records.erase(_records.keys().front()) if not _save(): _records = previous return false relationship_changed.emit(fingerprint) return true func _valid_target(fingerprint: String, display_name: String) -> bool: return ( NetworkIdentityCrypto.valid_fingerprint(fingerprint) and NetworkProfilePreferences.is_valid_display_name(display_name) ) func _ensure_loaded() -> void: if _loaded: return _loaded = true if _store_path.is_empty() or not FileAccess.file_exists(_store_path): return var file := FileAccess.open(_store_path, FileAccess.READ) if file == null: return var json := JSON.new() if json.parse(file.get_as_text()) != OK or typeof(json.data) != TYPE_DICTIONARY: return var data: Dictionary = json.data var format_version: int = int(data.get("format_version", 0)) if format_version not in [1, FORMAT_VERSION]: _write_blocked = true return for value: Variant in data.get("records", []): if typeof(value) != TYPE_DICTIONARY: continue var record: Dictionary = value var fingerprint := str(record.get("fingerprint", "")) if not NetworkIdentityCrypto.valid_fingerprint(fingerprint): continue record["blocked"] = bool(record.get("blocked", false)) record["muted"] = bool(record.get("muted", false)) or record["blocked"] record["friend"] = ( bool(record.get("friend", false)) and not record["blocked"] and _valid_stored_friend_capabilities(record) ) if not record["friend"]: _clear_friend_fields(record) _records[fingerprint] = record.duplicate(true) _expected_hash = PortableFileGuard.hash_file(_store_path) func _save() -> bool: var bytes := JSON.stringify({ "format_version": FORMAT_VERSION, "records": _records.values(), }, "\t").to_utf8_buffer() var result := PortableFileGuard.write_guarded( _store_path, bytes, _expected_hash, _data_root.conflict_directory(), _data_root.device_id, ) if bool(result.get("conflict", false)): _data_root.report_conflict(str(result.get("message", "")), str(result.get("conflict_path", ""))) if bool(result.get("ok", false)): _expected_hash = str(result["hash"]) return bool(result.get("ok", false)) func _clear_friend_fields(record: Dictionary) -> void: record["friend"] = false for key: String in [ "friend_since_unix", "local_presence_write_token", "local_invite_token", "remote_presence_channel", "remote_invite_token", ]: record.erase(key) func _valid_local_capabilities(value: Dictionary) -> bool: var write_token := str(value.get("local_presence_write_token", "")) var invite_token := str(value.get("local_invite_token", "")) return ( _valid_social_token(write_token) and _valid_social_token(invite_token) and str(value.get("presence_channel", "")) == presence_channel_for_write_token(write_token) and str(value.get("invite_token", "")) == invite_token ) func _valid_public_capabilities(value: Dictionary) -> bool: return ( _valid_social_token(str(value.get("presence_channel", ""))) and _valid_social_token(str(value.get("invite_token", ""))) ) func _valid_stored_friend_capabilities(record: Dictionary) -> bool: var local: Dictionary = { "local_presence_write_token": str( record.get("local_presence_write_token", "") ), "local_invite_token": str(record.get("local_invite_token", "")), "presence_channel": presence_channel_for_write_token(str( record.get("local_presence_write_token", "") )), "invite_token": str(record.get("local_invite_token", "")), } var remote: Dictionary = { "presence_channel": str(record.get("remote_presence_channel", "")), "invite_token": str(record.get("remote_invite_token", "")), } return _valid_local_capabilities(local) and _valid_public_capabilities(remote) func _valid_social_token(value: String) -> bool: if value.length() != SOCIAL_TOKEN_LENGTH: return false for character: String in value: if character not in "0123456789abcdef": return false return true func _sort_records(records: Array[Dictionary]) -> void: records.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return str(a.get("last_known_display_name", "")).naturalnocasecmp_to( str(b.get("last_known_display_name", "")) ) < 0 )